Linked Malicious Storage Artifacts
Id | b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d |
Rulename | Linked Malicious Storage Artifacts |
Description | This query identifies the additional files uploaded by the same IP address which triggered a malware alert for malicious content upload on Azure Blob or File Storage Container. |
Severity | Medium |
Tactics | CommandAndControl Exfiltration |
Techniques | T1071 T1567 |
Required data connectors | MicrosoftCloudAppSecurity |
Kind | Scheduled |
Query frequency | 1d |
Query period | 1d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender for Cloud Apps/Analytic Rules/AdditionalFilesUploadedByActor.yaml |
Version | 1.0.3 |
Arm template | b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d.json |
//Collect the alert events
let alertData = SecurityAlert
| where DisplayName has "Potential malware uploaded to"
| extend Entities = parse_json(Entities)
| mv-expand Entities;
//Parse the IP address data
let ipData = alertData
| where Entities['Type'] =~ "ip"
| extend AttackerIP = tostring(Entities['Address']), AttackerCountry = tostring(Entities['Location']['CountryName']);
//Parse the file data
let FileData = alertData
| where Entities['Type'] =~ "file"
| extend MaliciousFileDirectory = tostring(Entities['Directory']), MaliciousFileName = tostring(Entities['Name']), MaliciousFileHashes = tostring(Entities['FileHashes']);
//Combine the File and IP data together
ipData
| join (FileData) on VendorOriginalId
| summarize by TimeGenerated, AttackerIP, AttackerCountry, DisplayName, ResourceId, AlertType, MaliciousFileDirectory, MaliciousFileName, MaliciousFileHashes
//Create a type column so we can track if it was a File storage or blobl storage upload
| extend type = iff(DisplayName has "file", "File", "Blob")
| join (
union
StorageFileLogs,
StorageBlobLogs
//File upload operations
| where OperationName =~ "PutBlob" or OperationName =~ "PutRange"
//Parse out the uploader IP
| extend ClientIP = tostring(split(CallerIpAddress, ":", 0)[0])
//Extract the filename from the Uri
| extend FileName = extract(@"\/([\w\-. ]+)\?", 1, Uri)
//Base64 decode the MD5 filehash, we will encounter non-ascii hex so string operations don't work
//We can work around this by making it an array then converting it to hex from an int
| extend base64Char = base64_decode_toarray(ResponseMd5)
| mv-expand base64Char
| extend hexChar = tohex(toint(base64Char))
| extend hexChar = iff(strlen(hexChar) < 2, strcat("0", hexChar), hexChar)
| extend SourceTable = iff(OperationName has "range", "StorageFileLogs", "StorageBlobLogs")
| summarize make_list(hexChar, 1000) by CorrelationId, ResponseMd5, FileName, AccountName, TimeGenerated, RequestBodySize, ClientIP, SourceTable
| extend Md5Hash = strcat_array(list_hexChar, "")
//Pack the file information the summarise into a ClientIP row
| extend p = pack("FileName", FileName, "FileSize", RequestBodySize, "Md5Hash", Md5Hash, "Time", TimeGenerated, "SourceTable", SourceTable)
| summarize UploadedFileInfo=make_list(p, 10000), FilesUploaded=count() by ClientIP
| join kind=leftouter (
union
StorageFileLogs,
StorageBlobLogs
| where OperationName =~ "DeleteFile" or OperationName =~ "DeleteBlob"
| extend ClientIP = tostring(split(CallerIpAddress, ":", 0)[0])
| extend FileName = extract(@"\/([\w\-. ]+)\?", 1, Uri)
| extend SourceTable = iff(OperationName has "range", "StorageFileLogs", "StorageBlobLogs")
| extend p = pack("FileName", FileName, "Time", TimeGenerated, "SourceTable", SourceTable)
| summarize DeletedFileInfo=make_list(p, 10000), FilesDeleted=count() by ClientIP
) on ClientIP
) on $left.AttackerIP == $right.ClientIP
| mvexpand UploadedFileInfo
| extend LinkedMaliciousFileName = tostring(UploadedFileInfo.FileName)
| extend LinkedMaliciousFileHash = tostring(UploadedFileInfo.Md5Hash)
| extend HashAlgorithm = "MD5"
| project AlertTimeGenerated = TimeGenerated, LinkedMaliciousFileName, LinkedMaliciousFileHash, HashAlgorithm, AlertType, AttackerIP, AttackerCountry, MaliciousFileDirectory, MaliciousFileName, FilesUploaded, UploadedFileInfo
relevantTechniques:
- T1071
- T1567
name: Linked Malicious Storage Artifacts
requiredDataConnectors:
- dataTypes:
- SecurityAlert
connectorId: MicrosoftCloudAppSecurity
entityMappings:
- fieldMappings:
- identifier: Address
columnName: AttackerIP
entityType: IP
- fieldMappings:
- identifier: Algorithm
columnName: HashAlgorithm
- identifier: Value
columnName: LinkedMaliciousFileHash
entityType: FileHash
triggerThreshold: 0
id: b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d
tactics:
- CommandAndControl
- Exfiltration
version: 1.0.3
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender for Cloud Apps/Analytic Rules/AdditionalFilesUploadedByActor.yaml
queryPeriod: 1d
kind: Scheduled
queryFrequency: 1d
severity: Medium
status: Available
description: |
'This query identifies the additional files uploaded by the same IP address which triggered a malware alert for malicious content upload on Azure Blob or File Storage Container.'
query: |
//Collect the alert events
let alertData = SecurityAlert
| where DisplayName has "Potential malware uploaded to"
| extend Entities = parse_json(Entities)
| mv-expand Entities;
//Parse the IP address data
let ipData = alertData
| where Entities['Type'] =~ "ip"
| extend AttackerIP = tostring(Entities['Address']), AttackerCountry = tostring(Entities['Location']['CountryName']);
//Parse the file data
let FileData = alertData
| where Entities['Type'] =~ "file"
| extend MaliciousFileDirectory = tostring(Entities['Directory']), MaliciousFileName = tostring(Entities['Name']), MaliciousFileHashes = tostring(Entities['FileHashes']);
//Combine the File and IP data together
ipData
| join (FileData) on VendorOriginalId
| summarize by TimeGenerated, AttackerIP, AttackerCountry, DisplayName, ResourceId, AlertType, MaliciousFileDirectory, MaliciousFileName, MaliciousFileHashes
//Create a type column so we can track if it was a File storage or blobl storage upload
| extend type = iff(DisplayName has "file", "File", "Blob")
| join (
union
StorageFileLogs,
StorageBlobLogs
//File upload operations
| where OperationName =~ "PutBlob" or OperationName =~ "PutRange"
//Parse out the uploader IP
| extend ClientIP = tostring(split(CallerIpAddress, ":", 0)[0])
//Extract the filename from the Uri
| extend FileName = extract(@"\/([\w\-. ]+)\?", 1, Uri)
//Base64 decode the MD5 filehash, we will encounter non-ascii hex so string operations don't work
//We can work around this by making it an array then converting it to hex from an int
| extend base64Char = base64_decode_toarray(ResponseMd5)
| mv-expand base64Char
| extend hexChar = tohex(toint(base64Char))
| extend hexChar = iff(strlen(hexChar) < 2, strcat("0", hexChar), hexChar)
| extend SourceTable = iff(OperationName has "range", "StorageFileLogs", "StorageBlobLogs")
| summarize make_list(hexChar, 1000) by CorrelationId, ResponseMd5, FileName, AccountName, TimeGenerated, RequestBodySize, ClientIP, SourceTable
| extend Md5Hash = strcat_array(list_hexChar, "")
//Pack the file information the summarise into a ClientIP row
| extend p = pack("FileName", FileName, "FileSize", RequestBodySize, "Md5Hash", Md5Hash, "Time", TimeGenerated, "SourceTable", SourceTable)
| summarize UploadedFileInfo=make_list(p, 10000), FilesUploaded=count() by ClientIP
| join kind=leftouter (
union
StorageFileLogs,
StorageBlobLogs
| where OperationName =~ "DeleteFile" or OperationName =~ "DeleteBlob"
| extend ClientIP = tostring(split(CallerIpAddress, ":", 0)[0])
| extend FileName = extract(@"\/([\w\-. ]+)\?", 1, Uri)
| extend SourceTable = iff(OperationName has "range", "StorageFileLogs", "StorageBlobLogs")
| extend p = pack("FileName", FileName, "Time", TimeGenerated, "SourceTable", SourceTable)
| summarize DeletedFileInfo=make_list(p, 10000), FilesDeleted=count() by ClientIP
) on ClientIP
) on $left.AttackerIP == $right.ClientIP
| mvexpand UploadedFileInfo
| extend LinkedMaliciousFileName = tostring(UploadedFileInfo.FileName)
| extend LinkedMaliciousFileHash = tostring(UploadedFileInfo.Md5Hash)
| extend HashAlgorithm = "MD5"
| project AlertTimeGenerated = TimeGenerated, LinkedMaliciousFileName, LinkedMaliciousFileHash, HashAlgorithm, AlertType, AttackerIP, AttackerCountry, MaliciousFileDirectory, MaliciousFileName, FilesUploaded, UploadedFileInfo
triggerOperator: gt
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d')]",
"properties": {
"alertRuleTemplateName": "b9e3b9f8-a406-4151-9891-e5ff1ddd8c1d",
"customDetails": null,
"description": "'This query identifies the additional files uploaded by the same IP address which triggered a malware alert for malicious content upload on Azure Blob or File Storage Container.'\n",
"displayName": "Linked Malicious Storage Artifacts",
"enabled": true,
"entityMappings": [
{
"entityType": "IP",
"fieldMappings": [
{
"columnName": "AttackerIP",
"identifier": "Address"
}
]
},
{
"entityType": "FileHash",
"fieldMappings": [
{
"columnName": "HashAlgorithm",
"identifier": "Algorithm"
},
{
"columnName": "LinkedMaliciousFileHash",
"identifier": "Value"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender for Cloud Apps/Analytic Rules/AdditionalFilesUploadedByActor.yaml",
"query": "//Collect the alert events\nlet alertData = SecurityAlert\n| where DisplayName has \"Potential malware uploaded to\"\n| extend Entities = parse_json(Entities)\n| mv-expand Entities;\n//Parse the IP address data\nlet ipData = alertData\n| where Entities['Type'] =~ \"ip\"\n| extend AttackerIP = tostring(Entities['Address']), AttackerCountry = tostring(Entities['Location']['CountryName']);\n//Parse the file data\nlet FileData = alertData\n| where Entities['Type'] =~ \"file\"\n| extend MaliciousFileDirectory = tostring(Entities['Directory']), MaliciousFileName = tostring(Entities['Name']), MaliciousFileHashes = tostring(Entities['FileHashes']);\n//Combine the File and IP data together\nipData\n| join (FileData) on VendorOriginalId\n| summarize by TimeGenerated, AttackerIP, AttackerCountry, DisplayName, ResourceId, AlertType, MaliciousFileDirectory, MaliciousFileName, MaliciousFileHashes\n//Create a type column so we can track if it was a File storage or blobl storage upload\n| extend type = iff(DisplayName has \"file\", \"File\", \"Blob\")\n| join (\n union\n StorageFileLogs,\n StorageBlobLogs\n //File upload operations\n | where OperationName =~ \"PutBlob\" or OperationName =~ \"PutRange\"\n //Parse out the uploader IP\n | extend ClientIP = tostring(split(CallerIpAddress, \":\", 0)[0])\n //Extract the filename from the Uri\n | extend FileName = extract(@\"\\/([\\w\\-. ]+)\\?\", 1, Uri)\n //Base64 decode the MD5 filehash, we will encounter non-ascii hex so string operations don't work\n //We can work around this by making it an array then converting it to hex from an int\n | extend base64Char = base64_decode_toarray(ResponseMd5)\n | mv-expand base64Char\n | extend hexChar = tohex(toint(base64Char))\n | extend hexChar = iff(strlen(hexChar) < 2, strcat(\"0\", hexChar), hexChar)\n | extend SourceTable = iff(OperationName has \"range\", \"StorageFileLogs\", \"StorageBlobLogs\")\n | summarize make_list(hexChar, 1000) by CorrelationId, ResponseMd5, FileName, AccountName, TimeGenerated, RequestBodySize, ClientIP, SourceTable\n | extend Md5Hash = strcat_array(list_hexChar, \"\")\n //Pack the file information the summarise into a ClientIP row\n | extend p = pack(\"FileName\", FileName, \"FileSize\", RequestBodySize, \"Md5Hash\", Md5Hash, \"Time\", TimeGenerated, \"SourceTable\", SourceTable)\n | summarize UploadedFileInfo=make_list(p, 10000), FilesUploaded=count() by ClientIP\n | join kind=leftouter (\n union\n StorageFileLogs,\n StorageBlobLogs\n | where OperationName =~ \"DeleteFile\" or OperationName =~ \"DeleteBlob\"\n | extend ClientIP = tostring(split(CallerIpAddress, \":\", 0)[0])\n | extend FileName = extract(@\"\\/([\\w\\-. ]+)\\?\", 1, Uri)\n | extend SourceTable = iff(OperationName has \"range\", \"StorageFileLogs\", \"StorageBlobLogs\")\n | extend p = pack(\"FileName\", FileName, \"Time\", TimeGenerated, \"SourceTable\", SourceTable)\n | summarize DeletedFileInfo=make_list(p, 10000), FilesDeleted=count() by ClientIP\n ) on ClientIP\n ) on $left.AttackerIP == $right.ClientIP\n| mvexpand UploadedFileInfo\n| extend LinkedMaliciousFileName = tostring(UploadedFileInfo.FileName)\n| extend LinkedMaliciousFileHash = tostring(UploadedFileInfo.Md5Hash)\n| extend HashAlgorithm = \"MD5\"\n| project AlertTimeGenerated = TimeGenerated, LinkedMaliciousFileName, LinkedMaliciousFileHash, HashAlgorithm, AlertType, AttackerIP, AttackerCountry, MaliciousFileDirectory, MaliciousFileName, FilesUploaded, UploadedFileInfo\n",
"queryFrequency": "P1D",
"queryPeriod": "P1D",
"severity": "Medium",
"status": "Available",
"subTechniques": [],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"CommandAndControl",
"Exfiltration"
],
"techniques": [
"T1071",
"T1567"
],
"templateVersion": "1.0.3",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}