
We are seeing more and more Device Code Phishing cases, and we already have good reports, TI feeds and IOC lists with URLs and domains used in these campaigns. They are very useful, but there is always the same limitation when we hunt using IOCs: someone needs to discover and report the infrastructure first.
xWhat happens if the attacker registered the phishing domain today? What if the URL has never been reported and our user is one of the first targets?
So I wanted to try something different. Instead of starting with “Do I know this URL is malicious?”, I started from something we already know happened: a Device Code authentication. From there, my question was simple: What was the user doing immediately before it?
Detecting the behaviour
Using the Cmsi:Cmsi event in EntraIdSignInEvents as the reference point, I look 5 minutes backwards in DeviceNetworkEvents for the same user and collect the last three URLs visited before the authentication. I also enrich the remote IPs with their countries. The idea is to reconstruct a very small piece of the user’s activity:
URL → URL → URL → Device Code authentication
Why only three URLs and five minutes? Because I am not interested in reconstructing the complete browsing history of the user. I want to understand what happened immediately before the Device Code flow and base on some results the last three sites say what I am looking for.
The KQL first creates a unique DeviceCodeId for every authentication. Then it joins each Device Code event with network activity from the same account, keeps only connections generated during the previous five minutes, and uses partition with top 3 to preserve the three most recent URLs for each authentication.
After that, the URLs and their countries are converted into three simple pairs:
- FirstURL / FirstURLCountry
- SecondURL / SecondURLCountry
- ThirdURL / ThirdURLCountry
Now we can start removing what we expect to see: Microsoft services, known websites and countries that make sense for our environment. What remains is where the hunt becomes interesting.
Imagine one of those URLs is a completely new domain. No reputation. No match in your TI platform. No report mentioning it. Looking only at IOCs, we probably have nothing. But if that website was visited before a Device Code authentication from an unfamiliar country and unknown site, we have behaviour and context worth investigating.
// Whitelisted Countries
let CountryList=dynamic([“Switzerland”,“France”]);
// Whitelisted Sites
let WebSiteList=dynamic([“login.windows.net”,“mobile.events.data.microsoft.com”]);
let DeviceCode =
EntraIdSignInEvents
| where EndpointCall =~ “Cmsi:Cmsi”
| extend DeviceCodeId=strcat(AccountUpn,“_”,tostring(Timestamp)),SignInCountry=tostring(geo_info_from_ip_address(IPAddress).country)
| summarize by TimeGenerated,AccountUpn,DeviceCodeId,DeviceCodeTime=Timestamp,SignInIP=IPAddress,SignInCountry,Application;
let LastUrls =
DeviceCode
| join kind=leftouter (DeviceNetworkEvents| where isnotempty(RemoteUrl)| extend WebCountry=tostring(geo_info_from_ip_address(RemoteIP).country)
| summarize by AccountUpn=InitiatingProcessAccountUpn,WebTime=Timestamp,RemoteUrl,RemoteIP,WebCountry
) on AccountUpn
| where isnull(WebTime) or (WebTime >= DeviceCodeTime - 5m and WebTime < DeviceCodeTime) | partition by DeviceCodeId (top 3 by WebTime desc)
| summarize URLs=make_list(RemoteUrl,3),Countries=make_list(WebCountry,3) by DeviceCodeId;
DeviceCode
| join kind=leftouter LastUrls on DeviceCodeId
| extend FirstURL=tostring(URLs[0]),
SecondURL=tostring(URLs[1]),
ThirdURL=tostring(URLs[2]),
FirstURLCountry=tostring(Countries[0]),
SecondURLCountry=tostring(Countries[1]),
ThirdURLCountry=tostring(Countries[2])
| extend FirstURL=iff(isempty(FirstURL),“NA”,FirstURL),
SecondURL=iff(isempty(SecondURL),“NA”,SecondURL),
ThirdURL=iff(isempty(ThirdURL),“NA”,ThirdURL),
FirstURLCountry=iff(isempty(FirstURLCountry),“NA”,FirstURLCountry),
SecondURLCountry=iff(isempty(SecondURLCountry),“NA”,SecondURLCountry),
ThirdURLCountry=iff(isempty(ThirdURLCountry),“NA”,ThirdURLCountry)
| where FirstURL != “NA” and SecondURL != “NA” and ThirdURL != “NA”
// Optional: show cases where at least one previous URL is hosted outside expected countries
//| where FirstURLCountry !in (CountryList) or SecondURLCountry !in (CountryList) or ThirdURLCountry !in (CountryList)
// Optional: show cases where at least one previous URL is not expected
//| where FirstURL !in (WebSiteList) or SecondURL !in (WebSiteList) or ThirdURL !in (WebSiteList)
| summarize by TimeGenerated,DeviceCodeTime,AccountUpn,SignInIP,SignInCountry,FirstURL,FirstURLCountry,SecondURL,SecondURLCountry,ThirdURL,ThirdURLCountry,DeviceCodeId
Summary
And this is the important point: I am not trying to detect Device Code authentication itself. Device Code can be completely legitimate. I am trying to understand the behaviour around it and, especially, what happened immediately before.
Threat Intelligence is still extremely valuable and I use it every day. But I don’t want to depend only on someone else finding the bad infrastructure before me. Sometimes the behaviour can give us the first clue.
The IOC can change, the domain can be new and the URL can have zero reputation. But the attack still needs a sequence.
That sequence is what I want to hunt.
Hunting Web Activity Before a Device Code Authentication (KQL Query) was originally published in Detect FYI on Medium, where people are continuing the conversation by highlighting and responding to this story.
Introduction to Malware Binary Triage (IMBT) Course
Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.
Enroll Now and Save 10%: Coupon Code MWNEWS10
Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.