Previously, On Kusto Detective Agency

An update from Agent Stas Fistuko! Agents found Krypto exactly where we said, waiting in line for his post run refreshment. Maybe he was hungry for a hotdog and a shake?. Anyway, Krypto was trailed to his secret hideout where plot twist What’s this? Professor Smoke? Locked up in the basement?

See, told you #TeamProfSmoke FTW.

After watching Prof Smoke’s video, we now know Krypto is in our network, looking to bring KDA down from the inside!

So time to dive into our network logs and see what we can find.

The Challenge

Krypto may have exploited vulnerabilities in our network. We need to find out if any of our Admin machines were indirectly compromised by Krypto, using Prof Smoke’s brand new graph functions.

The Data

Let’s start off looking at the data.

MachineLogs
| count

2,179,574 rows. What do they look like?

MachineLogs
| take 100 

So we have a typical log file. Timestamp, Machine, EventType and Message. Looking at the EventType we can see a few different categories we’re going to have to parse in order to make our graph. First, let’s look at the PeriodicScan events for vulnerable machines, and identify their MachineType. We’ll use the KQL parse function:

MachineLogs
| where EventType == "PeriodicScan"
| parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
| where Vulnerabilities > 0
| take 100

But wait, we don’t need all of this data. Each machine is scanned several times, so let’s just grab distinct machines:

MachineLogs
| where EventType == "PeriodicScan"
| parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
| where Vulnerabilities > 0
| distinct Machine, MachineType
| take 100

That’s better.

Now, let’s have a look at the IncomingRequest events. We’re only interested in vulnerable machines so we’ll filter to our VulnerableMachines query then parse the TaskID from the Message:

let VulnerableMachines =
    MachineLogs
    | where EventType == "PeriodicScan"
    | parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
    | where Vulnerabilities > 0
    | distinct Machine, MachineType;
MachineLogs
| where EventType == "IncomingRequest"
| lookup kind=inner VulnerableMachines on Machine
| parse  Message with  * "TaskID=" TaskID:guid *
| take 100

This gives us our requests:

Finally, we need to link our tasks together to form a graph, and this is where the SpawnTask events come in. We’ll use the same VulnerableMachines query to filter our MachineLogs and then parse the TaskID and ChildTaskID from the Message:

let VulnerableMachines =
MachineLogs
| where EventType == "PeriodicScan"
| parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
| where Vulnerabilities > 0
| distinct Machine, MachineType;
MachineLogs
| where EventType == "SpawnTask"
| lookup kind=inner VulnerableMachines on Machine
| parse  Message with  * "TaskID=" TaskID:guid * "TaskID=" ChildTaskID:guid * "on " ChildMachine
| take 100

Finally, we can put it all together:

let VulnerableMachines =
    MachineLogs
    | where EventType == "PeriodicScan"
    | parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
    | where Vulnerabilities > 0
    | distinct Machine, MachineType;
let Machines = 
    MachineLogs
    | where EventType == "IncomingRequest"
    | lookup kind=inner VulnerableMachines on Machine
    | parse  Message with  * "TaskID=" TaskID:guid *;
let Tasks = 
    MachineLogs
    | where EventType == "SpawnTask"
    | lookup kind=inner VulnerableMachines on Machine
    | parse  Message with  * "TaskID=" TaskID:guid * "TaskID=" ChildTaskID:guid * "on " ChildMachine;
Tasks
| make-graph TaskID-->ChildTaskID with Machines on TaskID
...

Using the new graph query syntax we can match from a starting node, through many edges to an ending node. In the query, (gateway) etc are just labels for the nodes, and hop is a label for the edges.

Since we’ve already filtered to vulnerable machines, our where clause is simply:

where gateway.MachineType == "Gateway" and target.MachineType == "Admin"

We can finally project the results to see our path:

...
| graph-match (gateway)-[hop*1..25]->(target)
    where gateway.MachineType == "Gateway" and target.MachineType == "Admin"
    project Start=gateway.Machine, End=target.Machine, path=hop.Machine

The complete query:

let VulnerableMachines =
    MachineLogs
    | where EventType == "PeriodicScan"
    | parse Message with MachineType:string " periodic scan completed, " Vulnerabilities:long * 
    | where Vulnerabilities > 0
    | distinct Machine, MachineType;
let Machines = 
    MachineLogs
    | where EventType == "IncomingRequest"
    | lookup kind=inner VulnerableMachines on Machine
    | parse  Message with  * "TaskID=" TaskID:guid *;
let Tasks = 
    MachineLogs
    | where EventType == "SpawnTask"
    | lookup kind=inner VulnerableMachines on Machine
    | parse  Message with  * "TaskID=" TaskID:guid * "TaskID=" ChildTaskID:guid * "on " ChildMachine;
Tasks
| make-graph TaskID-->ChildTaskID with Machines on TaskID
| graph-match (gateway)-[hop*1..25]->(target)
    where gateway.MachineType == "Gateway" and target.MachineType == "Admin"
    project Start=gateway.Machine, End=target.Machine, path=hop.Machine

Case solved. It’s a piece of schnitzel I tells ya.

Actually, this one took me sooo long to figure out. Again, classic overthinking and chasing rabbits down holes. So a big thanks to the Kusto team for some hints and keeping me from screaming at the PC.