|
| 1 | +print Topic = "l33tSpeak: Advanced hunting in Microsoft 365 Defender" |
| 2 | + , Presenters = pack_array("Sebastien Molendijk, Michael Melone, Tali Ash") |
| 3 | + , Company = "Microsoft" |
| 4 | + , Date = todatetime("10 MAY 2021") |
| 5 | + |
| 6 | +/////////////////////// |
| 7 | +// Working with the dynamic type |
| 8 | +// ref: https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/dynamic |
| 9 | +/////////////////////// |
| 10 | + |
| 11 | +// Dynamic is an object oriented format for storing structured data. |
| 12 | +// Dynamics are usually converted from JSON strings using either todynamic() or parse_json() (same function) |
| 13 | +// You can also convert XML into a dynamic by using parse_xml() |
| 14 | + |
| 15 | +let JsonString = '{"hello": 1337, "world": ["wibble","wobble","wubble"]}'; |
| 16 | +print todynamic(JsonString) |
| 17 | + |
| 18 | +// There are a number of ways you can interact with elements stored as dynamic() typed objects. |
| 19 | +// Interacting with child elements |
| 20 | +// - Column.Child |
| 21 | +// - Column[“Child”] |
| 22 | + |
| 23 | +let JsonString = '{"hello": 1337, "world": ["wibble","wobble","wubble"]}'; |
| 24 | +print x = todynamic(JsonString) |
| 25 | +| extend hello = x.hello, world = x["world"] |
| 26 | + |
| 27 | +// Interacting with lists \ arrays |
| 28 | +// Column[ElementNumber] |
| 29 | + |
| 30 | +let JsonString = '{"hello": 1337, "world": ["wibble","wobble","wubble"]}'; |
| 31 | +print x = todynamic(JsonString) |
| 32 | +| extend hello = x.hello, world = x["world"] |
| 33 | +| extend FirstElement = world[0], SecondElement = world[1] |
| 34 | + |
| 35 | +// The information on the actor initiating the activities can be found in AccountObjectId and AccountDisplayName columns. |
| 36 | +// To get the target account and the activities were performed we can extract information from RawEventData |
| 37 | + |
| 38 | +CloudAppEvents |
| 39 | +| where ActionType == "AddedToGroup" |
| 40 | +| take 50 |
| 41 | +| project-reorder AccountObjectId, AccountDisplayName, RawEventData |
| 42 | + |
| 43 | +CloudAppEvents |
| 44 | +| where ActionType == "AddedToGroup" |
| 45 | +| project Timestamp, Application, IPAddress, Actor = AccountDisplayName, AddedUser = RawEventData.TargetUserOrGroupName |
| 46 | + |
| 47 | +CloudAppEvents |
| 48 | +| where ActionType == "AddedToGroup" |
| 49 | +| project Timestamp, Application, IPAddress, Actor = AccountDisplayName, AddedUser = RawEventData["TargetUserOrGroupName"] |
| 50 | + |
| 51 | + |
| 52 | +////////////////////////////// |
| 53 | +// pack_array() |
| 54 | +// ref: https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/packarrayfunction |
| 55 | +// Creates a dynamic array |
| 56 | +////////////////////////////// |
| 57 | + |
| 58 | +// Lists, sets, and arrays in KQL are stored as dynamics and can be created |
| 59 | +// with functions such as pack_array() |
| 60 | + |
| 61 | +print pack_array('foo','bar','baz') |
| 62 | + |
| 63 | +// Note that you cannot simply compare dynamic elements in KQL. To do this, |
| 64 | +// convert them back to another type using functions such as tostring() or toint() |
| 65 | + |
| 66 | +let JsonDynamic = todynamic('{"hello": 1337, "world": ["wibble","wobble","wubble"]}'); |
| 67 | +print tostring(JsonDynamic.hello) == tostring(JsonDynamic['hello']) |
| 68 | + |
| 69 | +//////////////////////// |
| 70 | +// bag_unpack() |
| 71 | +// ref: https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/bag-unpackplugin |
| 72 | +// Automatically unpacks the first level of a dynamic to a table |
| 73 | +//////////////////////// |
| 74 | + |
| 75 | +// Another option is to use bag_unpack() to turn JSON data directly into a table. |
| 76 | +// Note that bag_unpack() only processes the first level of JSON. If you have |
| 77 | +// multiple nested JSON elements you may need multiple calls to the function. |
| 78 | + |
| 79 | +let JsonDynamic = todynamic('{"hello": 1337, "world": ["wibble","wobble","wubble"]}'); |
| 80 | +print x = JsonDynamic |
| 81 | +| evaluate bag_unpack(x) |
| 82 | + |
| 83 | +///////////////////////////// |
| 84 | +// mv-expand |
| 85 | +// ref: https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/mvexpandoperator |
| 86 | +// Multiplies elements in a dynamic array across a tabular dataset |
| 87 | +///////////////////////////// |
| 88 | + |
| 89 | + |
| 90 | +// You can also use functions such as mv-expand to parse elements in a dynamic |
| 91 | +// across a table. This is very handy for efficiently analyzing lists of |
| 92 | +// elements at scale |
| 93 | + |
| 94 | +// Let’s put bag_unpack() and mv-expand together. |
| 95 | + |
| 96 | +let JsonDynamic = todynamic('{"hello": 1337, "world": ["wibble","wobble","wubble"]}'); |
| 97 | +print x = JsonDynamic |
| 98 | +| evaluate bag_unpack(x) |
| 99 | +| mv-expand world |
| 100 | + |
| 101 | +// With mv-expand we can extract the Group the user was added to. |
| 102 | +// The easiest is to extract it from ActivityObjects column |
| 103 | + |
| 104 | +CloudAppEvents |
| 105 | +| where ActionType == "AddedToGroup" |
| 106 | +| mv-expand ActivityObjects |
| 107 | +| where ActivityObjects['Type'] == ('Group') |
| 108 | +| project Timestamp, Application, IPAddress, Actor = AccountDisplayName, AddedUser = RawEventData.TargetUserOrGroupName, Group = ActivityObjects.Name |
| 109 | + |
| 110 | +//////////////////////////////////////////////////////////////////////////////////////// |
| 111 | + |
| 112 | + |
| 113 | +////////////////////////////// |
| 114 | +// startofday() function |
| 115 | +// ref: https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/startofdayfunction |
| 116 | +// Returns the time at the start of a day, with an optional time offset |
| 117 | +////////////////////////////// |
| 118 | + |
| 119 | +// KQL time filters and functions are UTC |
| 120 | +// Returns the start of the day containing the date, shifted by an offset of days, if provided. |
| 121 | + |
| 122 | +print startofday(datetime(2021-01-01 10:10:17)) |
| 123 | + |
| 124 | +IdentityInfo | where AccountUpn == '[email protected]' |
| 125 | + |
| 126 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 127 | +AADSignInEventsBeta |
| 128 | +| where AccountObjectId == 'eababd92-9dc7-40e3-9359-6c106522db19' and Timestamp >= timeToSearch |
| 129 | +| distinct Application, ResourceDisplayName, Country, City, IPAddress, DeviceName, DeviceTrustType, OSPlatform, IsManaged, IsCompliant, AuthenticationRequirement, RiskState, UserAgent, ClientAppUsed |
| 130 | + |
| 131 | +// Step 1: understand the performed actions |
| 132 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 133 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 134 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 135 | +CloudAppEvents |
| 136 | +| where AccountObjectId == accountId and CountryCode in (locations) and Timestamp >= timeToSearch |
| 137 | +| summarize by ActionType, CountryCode, AccountObjectId |
| 138 | +| sort by ActionType asc |
| 139 | + |
| 140 | +// Step 2: review the accessed emails |
| 141 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 142 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 143 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 144 | +CloudAppEvents |
| 145 | +| where ActionType == 'MailItemsAccessed' and CountryCode in (locations) and AccountObjectId == accountId and Timestamp >= timeToSearch |
| 146 | +| mv-expand todynamic(RawEventData.Folders) |
| 147 | +| extend Path = todynamic(RawEventData_Folders.Path), SessionId = tostring(RawEventData.SessionId) |
| 148 | +| mv-expand todynamic(RawEventData_Folders.FolderItems) |
| 149 | +| project SessionId, Timestamp, AccountObjectId, DeviceType, CountryCode, City, IPAddress, UserAgent, Path, Message = tostring(RawEventData_Folders_FolderItems.InternetMessageId) |
| 150 | +| join kind=leftouter ( |
| 151 | + EmailEvents |
| 152 | + | where RecipientObjectId == accountId |
| 153 | + | project Subject, RecipientEmailAddress, SenderMailFromAddress, DeliveryLocation, ThreatTypes, AttachmentCount, UrlCount, InternetMessageId |
| 154 | + ) |
| 155 | + on $left.Message == $right.InternetMessageId |
| 156 | +| sort by Timestamp desc |
| 157 | + |
| 158 | + |
| 159 | +// BONUS: get message details using Graph: |
| 160 | +// https://graph.microsoft.com/v1.0/users/[email protected]/messages?filter=internetMessageId eq '<b4acafd9-d086-4de0-8deb-c83118dae907@az.centralus.production.microsoft.com>'&select=subject,from,hasAttachments |
| 161 | + |
| 162 | +// Step 3: review the accessed FolderItems |
| 163 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 164 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 165 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 166 | +CloudAppEvents |
| 167 | +| where ActionType == 'FilePreviewed' or ActionType == 'FileDownloaded' and CountryCode in (locations) and AccountObjectId == accountId and Timestamp >= timeToSearch |
| 168 | +| project Timestamp, CountryCode, IPAddress, ISP, UserAgent, Application, ActivityObjects, AccountObjectId |
| 169 | +| mv-expand ActivityObjects |
| 170 | +| where ActivityObjects['Type'] in ('File', 'Folder') and ActivityObjects['Role'] == 'Target object' |
| 171 | +| evaluate bag_unpack(ActivityObjects) |
| 172 | + |
| 173 | + |
| 174 | +// Step 4: review deleted emails |
| 175 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 176 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 177 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 178 | +CloudAppEvents |
| 179 | +| where ActionType in~ ('MoveToDeletedItems', 'SoftDelete', 'HardDelete') and CountryCode in (locations) and AccountObjectId == accountId and Timestamp >= timeToSearch |
| 180 | +| mv-expand ActivityObjects |
| 181 | +| where ActivityObjects['Type'] in ('Email', 'Folder') |
| 182 | +| evaluate bag_unpack(ActivityObjects) |
| 183 | +| distinct Timestamp, AccountObjectId, ActionType, CountryCode, IPAddress, Type, Name, Id |
| 184 | +| sort by Timestamp desc |
| 185 | + |
| 186 | + |
| 187 | +// Step 5: review the created inbox rules |
| 188 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 189 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 190 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 191 | +CloudAppEvents |
| 192 | +| where ActionType contains_cs 'InboxRule' and CountryCode in (locations) |
| 193 | +| extend RuleParameters = RawEventData.Parameters |
| 194 | +| project Timestamp, CountryCode, IPAddress, ISP, ActionType, ObjectName, RuleParameters |
| 195 | +| sort by Timestamp desc |
| 196 | + |
| 197 | +// Step 6: identify potential other victims |
| 198 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 199 | +let locations = pack_array('SG', 'DE', 'IE', 'AL', 'UK'); |
| 200 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 201 | +let ips = ( |
| 202 | + CloudAppEvents |
| 203 | + | where CountryCode in (locations) |
| 204 | + | distinct IPAddress, AccountObjectId |
| 205 | + ); |
| 206 | +ips |
| 207 | +| join (CloudAppEvents | project ActivityIP = IPAddress, UserId = AccountObjectId) on $left.IPAddress == $right.ActivityIP |
| 208 | +| distinct UserId |
| 209 | +| join IdentityInfo on $left.UserId == $right.AccountObjectId |
| 210 | +| distinct AccountDisplayName, AccountUpn, Department, Country, City, AccountObjectId |
| 211 | + |
| 212 | +// Bonus: identify details sent by the malicious actorIdentityInfo |
| 213 | +let accountId = 'eababd92-9dc7-40e3-9359-6c106522db19'; |
| 214 | +let timeToSearch = startofday(datetime('2021-05-04')); |
| 215 | +CloudAppEvents |
| 216 | +| where ActionType =~ 'send' and AccountObjectId == accountId // apply the right filter |
| 217 | +| extend rawData = todynamic(RawEventData) |
| 218 | +| extend UserKey = rawData.UserKey, MessageId = tostring(rawData.Item.InternetMessageId), Subject = rawData.Item.Subject, Attachments = rawData.Item.Attachments |
| 219 | +| join ( |
| 220 | + EmailEvents |
| 221 | + ) |
| 222 | + on $left.MessageId == $right.InternetMessageId |
| 223 | +| sort by Timestamp desc |
| 224 | +| project UserKey, Timestamp, AccountObjectId, AccountDisplayName, DeviceType, CountryCode, City, ISP, IPAddress, SenderIPv4, SenderIPv6, UserAgent, Subject, InternetMessageId, Attachments, RecipientEmailAddress, SenderMailFromAddress, DeliveryLocation, ThreatTypes, ConfidenceLevel |
| 225 | +, AttachmentCount, UrlCount, MessageId |
0 commit comments