Detection rules › Kusto
New executable via Office FileUploaded Operation
Identifies when executable file types are uploaded to Office services such as SharePoint and OneDrive. List currently includes 'exe', 'inf', 'gzip', 'cmd', 'bat' file extensions. Additionally, identifies when a given user is uploading these files to another users workspace. This may be indication of a staging location for malware or other malicious activity.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement | |
| Command & Control |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | FileUploaded |
Rules detecting the same action
These rules filter on the same operation.
Rule body
id: d722831e-88f5-4e25-b106-4ef6e29f8c13
name: New executable via Office FileUploaded Operation
description: |
'Identifies when executable file types are uploaded to Office services such as SharePoint and OneDrive.
List currently includes 'exe', 'inf', 'gzip', 'cmd', 'bat' file extensions.
Additionally, identifies when a given user is uploading these files to another users workspace.
This may be indication of a staging location for malware or other malicious activity.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity (SharePoint)
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- LateralMovement
relevantTechniques:
- T1105
- T1570
query: |
// a threshold can be enabled, see commented line below for PrevSeenCount
let threshold = 2;
let uploadOp = 'FileUploaded';
// Extensions that are interesting. Add/Remove to this list as you see fit
let execExt = dynamic(['exe', 'inf', 'gzip', 'cmd', 'bat']);
let starttime = 8d;
let endtime = 1d;
OfficeActivity | where TimeGenerated >= ago(endtime)
// Limited to File Uploads due to potential noise, comment out the Operation statement below to include any operation type
// Additional, but potentially noisy operation types that include Uploads and Downloads can be included by adding the following - Operation contains "upload" or Operation contains "download"
| where Operation =~ uploadOp
| where SourceFileExtension has_any (execExt)
| project TimeGenerated, OfficeId, OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, UserAgent, Site_Url, SourceRelativeUrl, SourceFileName
| join kind= leftanti (
OfficeActivity | where TimeGenerated between (ago(starttime) .. ago(endtime))
| where Operation =~ uploadOp
| where SourceFileExtension has_any (execExt)
| summarize SourceRelativeUrl = make_set(SourceRelativeUrl, 100000), UserId = make_set(UserId, 100000) , PrevSeenCount = count() by SourceFileName
// To exclude previous matches when only above a specific count, change threshold above and uncomment the line below
//| where PrevSeenCount > threshold
| mvexpand SourceRelativeUrl, UserId
| extend SourceRelativeUrl = tostring(SourceRelativeUrl), UserId = tostring(UserId)
) on SourceFileName, SourceRelativeUrl, UserId
| extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2])
| extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_'))
// identify when UserId is not a match to the specific site url personal folder reference
| extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true , false )
| summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated),
UserAgents = make_list(UserAgent, 100000), OfficeIds = make_list(OfficeId, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000)
by OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, Site_Url, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Site_Url
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileNames
version: 2.0.5
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 2;
let uploadOp = 'FileUploaded';
let execExt = dynamic(['exe', 'inf', 'gzip', 'cmd', 'bat']);
let starttime = 8d;
let endtime = 1d;
Stage 1: source
OfficeActivity
Stage 2: where
| where TimeGenerated >= ago(endtime)
Stage 3: where
| where Operation =~ uploadOp
Stage 4: where
| where SourceFileExtension has_any (execExt)
Stage 5: project
| project TimeGenerated, OfficeId, OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, UserAgent, Site_Url, SourceRelativeUrl, SourceFileName
Stage 6: join (negated)
| join kind= leftanti (
OfficeActivity | where TimeGenerated between (ago(starttime) .. ago(endtime))
| where Operation =~ uploadOp
| where SourceFileExtension has_any (execExt)
| summarize SourceRelativeUrl = make_set(SourceRelativeUrl, 100000), UserId = make_set(UserId, 100000) , PrevSeenCount = count() by SourceFileName
| mvexpand SourceRelativeUrl, UserId
| extend SourceRelativeUrl = tostring(SourceRelativeUrl), UserId = tostring(UserId)
) on SourceFileName, SourceRelativeUrl, UserId
Stage 7: extend (3 consecutive steps)
| extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2])
| extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_'))
| extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true , false )
Stage 8: summarize
| summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated),
UserAgents = make_list(UserAgent, 100000), OfficeIds = make_list(OfficeId, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000)
by OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, Site_Url, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder
Stage 9: extend
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Operation | eq | FileUploaded | excludes:Operation field:"Operation" value:"FileUploaded" |
SourceFileExtension | match | exe, inf, gzip, cmd, bat | excludes:SourceFileExtension |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Operation | eq |
| field:"Operation" kind:eq value:"FileUploaded" |
SourceFileExtension | match |
| field:"SourceFileExtension" kind:match |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIP | summarize |
EndTime | summarize |
FileNames | summarize |
OfficeIds | summarize |
OfficeWorkload | summarize |
Operation | summarize |
RecordType | summarize |
SiteUrlUserFolder | summarize |
Site_Url | summarize |
SourceRelativeUrls | summarize |
StartTime | summarize |
TimeGenerated | summarize |
UserAgents | summarize |
UserId | summarize |
UserIdDiffThanUserFolder | summarize |
UserIdUserFolderFormat | summarize |
UserKey | summarize |
UserType | summarize |
AccountName | extend |
AccountUPNSuffix | extend |