At Eye Security, we constantly seek out new threats and detection methods to protect our customers. In the past months, we have observed a rising trend in Business Email Compromises (BECs). In this instance, threat actors use Attacker-in-the-Middle (AITM) phishing kits that lure users, via links in email messages, to phishing pages. These phishing pages are pixel-for-pixel the same as the regular Microsoft login pages. However, the users' interaction is proxied to a server controlled by the threat actor. This allows the threat actor to grab not only the username and password, but also the session token, which, in turn, bypasses multi-factor authentication (MFA). Read on and find out how to stay protected.
We protect customers not only through prevention but also by detecting and responding to suspicious signins. In order to do this at scale, we employ Microsoft Sentinel for our Microsoft 365 customers. This approach allows for tight integration with the Microsoft ecosystem, along with the flexibility to create custom rules and threat hunt at scale.
Sneaky2FA: new threat exploits vulnerabilities in Microsoft 365 accounts
In our opinion, the interesting thing about Sneaky2FA is the way it rotates through user agents during the signin process when a user is phished. Sneaky2FA uses a set of five common and slightly outdated yet distinct user agents. Here is an example:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0
Developing a KQL query to detect suspicious device shifts during signin
We wrote a generic KQL query that detects, as Sekoia dubs it, "an impossible device shift" during signin. This query can be deployed in Sentinel workspaces as a scheduled search with a short lookback window. We are gladly sharing it with the community. Our in-house version adds a few KQL tricks to further reduce the already low false positive rate. Furthermore, the jaccardIndex
parameter can be tuned to weed out false positives due to browser version updates.
Here is the KQL query in all its glory:
// Eye Security 2025
let suspiciousSignins = SigninLogs
| where DeviceDetail.deviceId == ''
| summarize userAgents = make_set(UserAgent) by CorrelationId
| where array_length(userAgents) > 2;
let correlationIds = suspiciousSignins
| mv-expand i = range(0, array_length(userAgents) - 2)
| mv-expand j = range(i + 1, array_length(userAgents) - 1)
| extend userAgent1 = tostring(userAgents[toint(i)])
| extend userAgent2 = tostring(userAgents[toint(j)])
| extend array1 = to_utf8(userAgent1)
| extend array2 = to_utf8(userAgent2)
| extend jaccardIndex = jaccard_index(array1, array2)
| where jaccardIndex < 0.8
| summarize by CorrelationId;
SigninLogs
| where ResultType == 0
| where CorrelationId in (correlationIds)
Breakdown of the query
Let's break the query down. First, the query takes the SigninLogs
where there's no Entra-registered device:
let suspiciousSignins = SigninLogs
| where DeviceDetail.deviceId == ''
We do this because a threat actor cannot have an Entra registered or joined device during the initial compromise. This reduces the false positive rate tremendously.
Then, the query takes all the UserAgents
associated with the same CorrelationId
and only keeps the rows that have three or more User Agents:
| summarize userAgents = make_set(UserAgent) by CorrelationId
| where array_length(userAgents) > 2;
Microsoft describes the CorrelationId
as follows:
Sign-in logs contain several unique identifiers that provide further insight into the sign-in attempt. Correlation ID: The correlation ID groups sign-ins from the same sign-in session. The value is based on parameters passed by a client, so Microsoft Entra ID can't guarantee its accuracy.
We indeed observe outliers with respect to the CorrelationId
. For instance, in some cases, the same CorrelationId
is attached to multiple cloud identies, which should not happen. We have yet to uncover the cause of this discrepancy. Further, this part of the query also gets results where Chrome/130
is updated to Chrome/131
. We need to filter these out to prevent false positives. This is what the next part of the query does.
Using the Jaccard index KQL function
The first part of the query yields an array of UserAgents
per CorrelationId
. We take this information and transform it so it can be fed to a KQL function called jaccard_index
. This Jaccard index can be used to quantify the similarity of two sets.
The query does this by generating all pair-wise combinations of the User Agents, first generating two helper columns:
let correlationIds = suspiciousSignins
| mv-expand i = range(0, array_length(userAgents) - 2)
| mv-expand j = range(i + 1, array_length(userAgents) - 1)
Then, it grabs the corresponding user agents from the array userAgents
:
| extend userAgent1 = tostring(userAgents[toint(i)])
| extend userAgent2 = tostring(userAgents[toint(j)])
When shown to a colleague, his response was "I would have written this for i / for j
loop differently in a SQL-like language; it works fine, but it feels like someone who wants to write C in a KQL query". Guilty as charged!
This yields two strings, but the jaccard_index
function operates on sets, so we transform the strings to an array of byte values using KQL's to_utf8
:
// create inputs for jaccard_index function
| extend array1 = to_utf8(userAgent1)
| extend array2 = to_utf8(userAgent2)
Finally, we are able to calculate the Jaccard index and filter out the values that are too similar (e.g. Chrome/130
and Chrome/131
), giving us a list of correlationIds
that we can cross-reference with the SigninLogs
:
| extend jaccardIndex = jaccard_index(array1, array2)
| where jaccardIndex < 0.8 // cut-off
| summarize by CorrelationId; // leave only suspicious correlationIds
The cut-off point of 0.8 is determined by taking the user agents that Sneaky2FA uses and calculating the corresponding Jaccard indices:
let userAgents = dynamic([
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0 OS/10.0.22635",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0"]
);
print dummy = 0
| mv-expand i = range(0, array_length(userAgents) - 2)
| mv-expand j = range(i + 1, array_length(userAgents) - 1)
| extend userAgent1 = tostring(userAgents[toint(i)])
| extend userAgent2 = tostring(userAgents[toint(j)])
| extend array1 = to_utf8(userAgent1)
| extend array2 = to_utf8(userAgent2)
| extend jaccardIndex = jaccard_index(array1, array2)
| project userAgent1, userAgent2, jaccardIndex
Image 1. Sneaky 2FA: user agent Jaccard indices
Based on these Jaccard indices, we took 0.8 as an appropriate threshold value.
Sneaky 2FA: putting it all together
The final piece of the puzzle is taking the suspicious correlationIds
and cross-referencing these with the SigninLogs
where the authentication succeeded:
...
SigninLogs
| where ResultType == 0 // authentication succeeded
| where CorrelationId in (correlationIds)
This query yields a surprising fidelity already, but we've tuned it in-house to further reduce the false positive rate using knowledge that we've picked up from various internal and external research.
Here, you can see it in full swing, where it correctly identifies a Sneaky2FA login. Notice that the query correctly detects the suspicious Firefox/115
user agent without having to hardcode it in the query.
Image 2. Sneaky 2FA: displaying the query results
Conclusion
We hope this KQL query helps companies in the constant struggle against AITM phishing, especially with emerging threats such as Sneaky2FA.
To help companies in the fight against login spoofing pages, we have developed the Eye Anti-Spoofing Tool (EAST). This is our advanced cybersecurity tool especially designed for combating Microsoft login spoofing. EAST can be used a separate measure to alert users when they are entering their credentials on certain phishing pages.
If you'd like to know how Eye Security can help you protect your company, reach out to us using the form below: