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.