{"id":9076,"date":"2026-08-10T09:00:00","date_gmt":"2026-08-10T09:00:00","guid":{"rendered":"https:\/\/cybersecurityinfocus.com\/?p=9076"},"modified":"2026-08-10T09:00:00","modified_gmt":"2026-08-10T09:00:00","slug":"4-million-fake-applications-and-one-blind-spot-a-soc-playbook-for-oauth-client-id-spoofing","status":"publish","type":"post","link":"https:\/\/cybersecurityinfocus.com\/?p=9076","title":{"rendered":"4 million fake applications and one blind spot: A SOC playbook for OAuth client ID spoofing"},"content":{"rendered":"<div>\n<div class=\"grid grid--cols-10@md grid--cols-8@lg article-column\">\n<div class=\"col-12 col-10@md col-6@lg col-start-3@lg\">\n<div class=\"article-column__content\">\n<div class=\"container\"><\/div>\n<h2 class=\"wp-block-heading\">Key takeaways<\/h2>\n<p>OAuth client ID spoofing defeats detections that key off application name or a known application ID, because the field itself is fabricated, rotated or blank.<\/p>\n<p>AADSTS700016 paired with an unrecognized client ID can mean valid credentials, not a broken app registration \u2014 treat it as a triage signal, not noise.<\/p>\n<p>Effective detection logic correlates client ID cardinality, missing application names and AADSTS error sequences across a time window, not any single event.<\/p>\n<p>A rehearsed response runbook (reset, revoke, review) matters as much as the detection query itself, and the cost of skipping it shows up as account takeover, not just alert fatigue.<\/p>\n<h2 class=\"wp-block-heading\">The signal hiding in plain sight<\/h2>\n<p class=\"wp-block-paragraph\">Picture a queue of failed sign-in alerts against Microsoft Entra ID. Each one points to a different application ID. None of the IDs are registered in the tenant. None crosses a volume threshold on its own. Nothing about the queue reads as a coordinated campaign; it reads like the normal debris of expired app registrations and forgotten test scripts that every tenant accumulates. An analyst scanning for a named application spiking in failures would scroll right past this queue without a second look. That is the point. Since December 2025, at least two threat actors have built enumeration campaigns specifically engineered to look like configuration noise instead of an attack, and the difference between the two is a detection-engineering question, not just a threat-intel one.<\/p>\n<h2 class=\"wp-block-heading\">The mechanism, in three error codes<\/h2>\n<p class=\"wp-block-paragraph\">The technique abuses the OAuth 2.0 Resource Owner Password Credentials (ROPC) flow, in which a single token request bundles a username, password, and client ID. Three Entra ID response codes carry the signal that matters.<\/p>\n<p class=\"wp-block-paragraph\"><strong>AADSTS50034<\/strong> means the username doesn\u2019t exist. <strong>AADSTS50126<\/strong> means the username exists, but the password is wrong. <strong>AADSTS700016<\/strong> fires when the username and password are both valid but the client ID isn\u2019t recognized. That last code is the one worth building a detection around: on its own, it looks like an app-registration problem an admin forgot to clean up. When paired with an unfamiliar, constantly rotating client ID, it can mean an attacker has already confirmed a working credential pair and is one step away from account takeover.<\/p>\n<p class=\"wp-block-paragraph\">ROPC persists in more environments than security teams expect. It survives in legacy scripts, CI pipelines and third-party integrations built before <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth-ropc\">Microsoft\u2019s guidance<\/a> to avoid it, precisely because it is the path of least resistance for developers who never need to open a browser. That installed base is what gives the technique room to operate. Attackers are not exploiting a rare misconfiguration; they are exploiting a flow that is still common enough to hide inside.<\/p>\n<div class=\"extendedBlock-wrapper block-coreImage undefined\">Figure 1. OAuth client ID spoofing attack and detection workflow.\n<p class=\"imageCredit\">Sunil Gentyala<\/p>\n<\/div>\n<h2 class=\"wp-block-heading\">Two campaigns, one detection gap<\/h2>\n<p class=\"wp-block-paragraph\"><strong>UNK_pyreq2323. <\/strong><a href=\"https:\/\/www.proofpoint.com\/us\/blog\/threat-insight\/oauth-client-id-spoofing-why-fake-client-ids-are-gaining-traction-stealthy\">Proofpoint<\/a> tracked this campaign running from AWS infrastructure, mutating the trailing digits of a real Exchange Online app ID across more than 700,000 variations, reusing each one against no more than a dozen accounts before discarding it. The campaign targeted over a million users across roughly 4,000 tenants, with about 28 percent of touched accounts hitting lockout.<\/p>\n<p class=\"wp-block-paragraph\"><strong>UNK_OutFlareAZ. <\/strong>Running mostly through Cloudflare, this campaign skipped mutation entirely and generated a fresh random UUID per request: 3.7 million spoofed IDs against more than two million accounts, peaking near 720,000 targeted users on March 15, 2026.<\/p>\n<p class=\"wp-block-paragraph\">Proofpoint\u2019s own researchers say they can\u2019t yet confirm whether this is one actor or two independent clusters converging on the same trick through forum discussion and trial and error, a point <a href=\"https:\/\/www.helpnetsecurity.com\/2026\/07\/13\/entra-id-oauth-client-id-spoofing\/\">Help Net Security<\/a> also covered in reporting on the research. For detection purposes, that ambiguity doesn\u2019t matter. Both campaigns defeat the same control, per-application volume thresholds, using the same gap: an uncorrelated, disposable identifier field that nobody is watching for cardinality.<\/p>\n<h2 class=\"wp-block-heading\">Why this is a CISO problem, not just a queue problem<\/h2>\n<p class=\"wp-block-paragraph\">A 28 percent lockout rate across a million-user campaign is not a rounding error. It is a helpdesk cost, a productivity hit and, for every account where AADSTS700016 fired before the lockout, a live credential an attacker has already validated and can use elsewhere: mailbox access, OAuth consent grants, lateral movement into connected SaaS. The detection gap here isn\u2019t cosmetic. It is the difference between catching a validated-credential event while it\u2019s still isolated to one authentication attempt and finding out about it three weeks later from a mailbox-forwarding rule or a fraudulent wire request. That is what makes this worth a rule, not just a footnote in a threat-intel digest.<\/p>\n<h2 class=\"wp-block-heading\">Building the detection<\/h2>\n<p class=\"wp-block-paragraph\">A workable rule reconstructs the request sequence instead of scoring one event in isolation. Over Entra ID sign-in logs, or the equivalent SIEM ingestion, the core logic looks like this:<\/p>\n<p>SigninLogs<br \/>\n| where TimeGenerated &gt; ago(1h)<br \/>\n| where ResultType in (&#8220;50034&#8221;, &#8220;50126&#8221;, &#8220;700016&#8221;) or isempty(AppDisplayName)<br \/>\n| summarize<br \/>\n    DistinctClientIDs = dcount(AppId),<br \/>\n    ResultCodes = make_set(ResultType),<br \/>\n    Usernames = make_set(UserPrincipalName)<br \/>\n  by SourceIPAddress, UserAgent, bin(TimeGenerated, 15m)<br \/>\n| where DistinctClientIDs &gt; 5<br \/>\n| where ResultCodes has &#8220;700016&#8221;<\/p>\n<p class=\"wp-block-paragraph\">The two variables that matter most are DistinctClientIDs, because a single source cycling through many unregistered app IDs is the tell that per-application thresholds miss, and the presence of AADSTS700016 in that same window, which elevates the event from configuration noise to possible credential validation in progress. Layer in username-pattern detection, alphabetic or dictionary progression across attempts from the same source, to catch the OutFlareAZ-style wordlist pattern specifically.<\/p>\n<p class=\"wp-block-paragraph\">Tune the DistinctClientIDs threshold against your own tenant\u2019s baseline before trusting it in production. A dev team running CI against a handful of test app registrations can produce a smaller version of the same shape, and Conditional Access policies scoped only to named applications will not catch a fabricated client ID that never matches an intended application scope in the first place.<\/p>\n<p class=\"wp-block-paragraph\">Wire the rule into existing SOAR or ticketing workflows rather than a standalone dashboard nobody checks on a Friday afternoon. A detection that fires into the same queue as password-spray and impossible-travel alerts gets triaged with the same urgency; one that lands in an isolated identity-hygiene report gets read weeks later, if at all.<\/p>\n<h2 class=\"wp-block-heading\">Separating signal from noise<\/h2>\n<p class=\"wp-block-paragraph\">Not every blank-app-name or 700016 event is an attack. Deleted app registrations, expired multi-tenant consent, wrong-tenant endpoint calls and CI pipelines pointed at the wrong client ID all produce similar-looking noise. Two filters cut the false-positive rate substantially. Legitimate misconfiguration is almost always low-cardinality, one or two client IDs, not dozens, and it repeats from known internal infrastructure. The attack pattern is high-cardinality and arrives from unfamiliar ASNs or hosting-provider ranges. Cross-referencing against existing password-spray and impossible-travel alerts before escalating avoids paging an analyst for a stale service principal, and tracking the rule\u2019s precision over its first few weeks, confirmed incidents versus total fires, is worth doing before treating it as production-grade. Document the triage criteria alongside the rule itself, since the next analyst on shift needs the same reasoning to close a ticket with confidence rather than escalate out of caution.<\/p>\n<h2 class=\"wp-block-heading\">The response runbook<\/h2>\n<p class=\"wp-block-paragraph\">When the correlated signal fires, the runbook should be short and rehearsed before it\u2019s needed, not improvised during the incident. Force a password reset on the affected account, revoke active sessions and refresh tokens, and review sign-in and mailbox-access activity for the days following the AADSTS700016 hit, since that is the window where a confirmed credential gets used elsewhere.<\/p>\n<p class=\"wp-block-paragraph\">Retiring ROPC entirely, migrating interactive apps to browser-based auth and service workloads to managed identities or certificate-based service principals, removes the flow this technique depends on. Until that migration is complete, the detection above is what stands between a ticket closed as configuration noise and a caught account-takeover attempt.<\/p>\n<h2 class=\"wp-block-heading\">The broader lesson for detection engineers<\/h2>\n<p class=\"wp-block-paragraph\">The reusable lesson here isn\u2019t about OAuth specifically. Any field a defender logs but doesn\u2019t correlate \u2014 application ID, user agent, ASN, whatever comes next \u2014 is a candidate for an attacker to fragment their traffic across until per-field thresholds stop tripping.<\/p>\n<p class=\"wp-block-paragraph\">Four million fake applications is what that gap looks like at scale when nobody is watching cardinality instead of volume, and it is the same underlying playbook as <a href=\"https:\/\/attack.mitre.org\/techniques\/T1078\/004\/\">MITRE ATT&amp;CK\u2019s Valid Accounts: Cloud Accounts<\/a>, T1078.004: an attacker using technically valid credentials. The fix generalizes regardless of which field gets exploited next. Build detections around behavioral sequences across multiple fields and a time window, not a single field crossing a static threshold.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Key takeaways OAuth client ID spoofing defeats detections that key off application name or a known application ID, because the field itself is fabricated, rotated or blank. AADSTS700016 paired with an unrecognized client ID can mean valid credentials, not a broken app registration \u2014 treat it as a triage signal, not noise. Effective detection logic [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":9077,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-9076","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-education"],"_links":{"self":[{"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=\/wp\/v2\/posts\/9076"}],"collection":[{"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=9076"}],"version-history":[{"count":0,"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=\/wp\/v2\/posts\/9076\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=\/wp\/v2\/media\/9077"}],"wp:attachment":[{"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=9076"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=9076"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cybersecurityinfocus.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=9076"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}