top of page

Detecting Executive Impersonation Campaigns with KQL

  • Writer: Damien van der Linden
    Damien van der Linden
  • 11 minutes ago
  • 4 min read

Intro


If you’ve ever worked in a SOC (or honestly, you know, if you use e-mail), you’ve probably rolled your eyes at these emails. These e-mails pretend to be from your CEO, have your first name as the subject, and contain absolutely no links, no files, just text. The sender’s display name says “John CEO,” and the content? Something like “Hi Damien, are you available for a quick task?”


No attachments, no URLs, just pure social engineering designed to dodge detections and poke at human trust. It’s frustratingly simple, frustratingly effective… and yes, people still reply.


So, what do you do when your phishing radar starts tingling? You crack open Microsoft Defender, load up your KQL toolkit, and go hunting.


Here’s how we caught them.


The Attack Pattern (a.k.a. How to Pretend You Know Someone)


This campaign comes with a simple but effective M.O.:


  • First name in the subject – every subject has the recipient’s first name.

  • Sent from Gmail – always from a good ol’ @gmail.com. Because nothing says "business critical" like a free email provider.

  • No links, no attachments – pure social engineering.

  • They ask for a phone number - "Hey friend, got a quick task for you, help pls"

  • User replies with phone number - Anything for that raise!

  • Scammer calls or sends a text with task - This task is always the same, this 'CEO' is in need of some giftcards!

  • Consistent keywords in e-mail address - keywords such as "office", "executive", "director", "board" are often used in the e-mail address. However, in our detection we do not initially rely on these.


Accurate representation of the e-mail (this one could use some better OPSec..)
Accurate representation of the e-mail (this one could use some better OPSec..)

The Strategy


We knew we had to combine two worlds: email metadata and identity info. If we could check whether a subject line matched the user’s first name, and cross-reference that with the sender address and a lack of content; we would get very accurate results back.


Let’s get nerdy.


The Query

EmailEvents
| where Timestamp > ago(30d)
| where SenderFromAddress endswith "@gmail.com"
| where AttachmentCount == 0
| where UrlCount == 0
| join kind=leftouter (
    IdentityInfo
    | project AccountObjectId, GivenName, AccountDisplayName
    | summarize arg_max(AccountObjectId, GivenName, AccountDisplayName) by AccountObjectId
) on $left.RecipientObjectId == $right.AccountObjectId
| extend RecipientFirstName = case(
    isnotempty(GivenName), GivenName,
    isnotempty(AccountDisplayName), tostring(split(AccountDisplayName, " ")[0]),
    tostring(split(RecipientObjectId, "@")[0])
)
| where isnotempty(RecipientFirstName)
| where Subject =~ RecipientFirstName
| where DeliveryAction != "Blocked"
| project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, RecipientFirstName, DeliveryAction, DeliveryLocation, AuthenticationDetails, NetworkMessageId, ReportId
| sort by Timestamp desc

Step-by-Step Breakdown

  1. Time Filter - Only emails from the past 30 days. Enough to get a trend, not so much you crash Sentinel.

  2. Gmail Filter - Because nobody ever sends internal memos from executiveofficerceomanager9000@gmail.com.

  3. Content-Free Zone - If there’s an attachment or link, we’re not interested. These scammers just send empty stares and trust issues.

  4. Join with IdentityInfo - This is where the magic happens. We pull in identity data to actually know what a user’s first name is.

  5. Get That First Name -

    • Ideal: actual GivenName field

    • Backup: take first word of AccountDisplayName

    • Worst-case: chop the email address in half and pray

  6. Subject == First Name - This is the moment of truth. If an email from Gmail shows up with the subject “Damien,” and lands in Damien’s inbox, we raise an eyebrow.

  7. Blocked? Skip. - If the system already caught it, we don’t need to.

  8. Results Parade - All the juicy fields sorted by latest timestamp. Let the investigations begin.

Results

Across multiple organizations, over 30 days, we got:

  • 143 alerts

  • 141 were legit impersonation attempts

  • 2 false positives (sorry to the two marketing folks who like using names and using Gmail)

That’s 98.6% accuracy. I mean, come on. That’s better than my Spotify and Instagram algorithm.

Whitelisting and dealing with False-Positives


Since we don't neccesarily scope in on keywords in the query, a few false-positives could arise. An example is if your company requires customers to send an e-mail for a parking spot to parking@company.com, it's expected that you will see some Gmail addresses e-mailing this with subject "Parking". If you set that as the first name for the e-mail-address, then yes, this query will catch that. To avoid that, we can tune the query with a little whitelist as such:


let whitelist = dynamic(["Parking", "parking"]); // create the whitelist
EmailEvents
| where Timestamp > ago(30d)
| where SenderFromAddress endswith "@gmail.com"
| where AttachmentCount == 0
| where UrlCount == 0
| join kind=leftouter (
    IdentityInfo
    | project AccountObjectId, GivenName, AccountDisplayName
    | summarize arg_max(AccountObjectId, GivenName, AccountDisplayName) by AccountObjectId
) on $left.RecipientObjectId == $right.AccountObjectId
| extend RecipientFirstName = case(
    isnotempty(GivenName), GivenName,
    isnotempty(AccountDisplayName), tostring(split(AccountDisplayName, " ")[0]),
    tostring(split(RecipientObjectId, "@")[0])
)
| where isnotempty(RecipientFirstName)
| where Subject =~ RecipientFirstName and Subject !in (whitelist) // exclude
| where DeliveryAction != "Blocked"
| project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, RecipientFirstName, DeliveryAction, DeliveryLocation, AuthenticationDetails, NetworkMessageId, ReportId
| sort by Timestamp desc

Besides this, we could also scope in further by adding the keywords from the e-mail address. However, I do discourage doing this because while these keywords often stay the same, I am observing many, many variations lately.


Wrapping Up

By focusing on this very specific pattern: personal subject lines + Gmail + zero content. We managed to get accurate results using nothing but good ol’ KQL and some clever data wrangling.

It’s a reminder that:

  • Pattern recognition still rules

  • Joining multiple tables is worth the pain

  • You don’t need AI to catch every scam


You can use this query to hunt, you can deploy it as an ART with a playbook to automate soft-deletes. Or what I would recommend; a custom detection in XDR with some automated response to move these e-mails out of the inbox!


I hope this was helpful, as a SOC analyst we come across these e-mails very, very frequently and with this detection we can get ahead of them and remove them either automatically or manually.

2025-2026 LindenSec | ©
bottom of page