Azure CLI ROPC Password-Spray — LSHIY Campaign (Entra ID / Microsoft 365)
/token endpoint with no interactive MFA prompt, slipping past Conditional Access policies that don't cover the ROPC / Azure-CLI path. 81M+ login attempts, 78 accounts compromised across 64 organizations. This is an identity / IdP hunt — distinct from device-code phishing. Defensive hunt & harden pack — no offensive code.Executive Summary
Huntress is tracking a massive, ongoing, automated password-spray attack against Microsoft's Azure command-line interface. Between June 12 and June 26, 2026, the threat actor made more than 81 million login attempts and successfully compromised at least 78 Microsoft accounts across 64 organizations. Compromises ran at a steady 2–4 accounts/day (with 12 on June 19) before a sharp escalation on June 22 — 30 identities across 23 businesses in a single day.
The mechanism is the crux of this hunt: the actor replays old username/password pairs that were breached but never rotated, validating them through the OAuth ROPC (Resource Owner Password Credentials) grant. ROPC is deprecated in OAuth 2.1 and, critically, does not support modern auth flows like MFA or SSO. It takes a username/password at the tenant /token endpoint and mints a user-delegated token with no interactive MFA challenge. Because ROPC does not traverse the authorization endpoint where Conditional Access policies (CAPs) are evaluated, a misconfigured CAP is bypassed entirely.
Of the 23 businesses hit in the June 22 spike, 15 had MFA implemented and enforced via CAP yet were still breached — MFA didn't fire because it was scoped to specific apps (not All Cloud Apps), specific user groups (e.g., admins only), or only non-trusted locations (which the attacker's U.S.-geo-mislabeled IPs evaded), or was left in report-only mode. Eight impacted organizations had no MFA policy at all.
The infrastructure rotates. Activity originated from LSHIY LLC's IPv6-only range 2a0a:d683::/32 (AS32167). After Huntress reported the abuse and LSHIY suspended the BYOIP user (July 2), the actor moved to FranTech (AS53667, ranges 2605:6400::/32 and 2605:6404::/32) — re-spraying 87% of the same accounts — and by July 15 to 3xK Tech GmbH (AS200373) at ~1.5M attempts/day, this time over IPv4 in a low-and-slow pattern rotating ~12,800 IPs at ~900 attempts each.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry |
|---|---|---|---|
| 1 · Vendor (primary) | Huntress — "No (Bad) CAP: Inside an Ongoing LSHIY Password Spray Attack" | 81M+ attempts (Jun 12–26); 78 accts / 64 orgs; ROPC via Azure CLI; CAP/MFA gaps; LSHIY 2a0a:d683::/32 AS32167/AS955; FranTech 2605:6400::/32, 2605:6404::/32 AS53667; 3xK Tech AS200373; userStrongAuthClientAuthNRequired | yes |
| 1 · News | The Hacker News — Azure CLI Password Spray Hits at Least 78 Accounts | Corroborates counts, timeline, all three ASNs and IPv6 ranges; ROPC /token mechanics; migration timeline | yes |
| 1 · News | SecurityWeek — Massive Password Spray Campaign Targeting Azure CLI | AS32167/AS955 attribution; 155× rise in credential-spray volume; combo-list targeting; abuse-report non-response | yes |
| 2 · News | Security Affairs — Azure CLI Targeted in LSHIY Campaign Across 64 Orgs | Independent corroboration of LSHIY infra, ROPC method, userStrongAuthClientAuthNRequired mitigation | partial |
| 2 · News | TechRepublic — Azure CLI Password Spray Exposes M365 MFA Gap | MFA-scoping gap detail (apps/groups/trusted-location/report-only). Page returned HTTP 403 at fetch time — used only where corroborated by Tier-1 snapshots; no IOC sourced solely from it. | partial |
Decisions: identity-first. The durable signal is in Entra ID sign-in telemetry, so §7 native hunts (SigninLogs/KQL) carry the most weight and §8 CQL targets the same telemetry after it is forwarded to Falcon Next-Gen SIEM. Atomic infrastructure IOCs (IPv6 ranges + ASNs) are provided but the actor rotates ASNs with little effort — so the pack anchors on behavior (Azure CLI ROPC sign-ins, failure→success spray transitions, CA-notApplied successes) rather than on chasing hosting IPs. Every atomic IOC in §4/§10 traces to a saved source snapshot; the Azure CLI application ID is a documented Microsoft public-client constant (labelled, not a campaign IOC).
Hunt Brief & Attack Chain
Working hypothesis: The actor obtained breached-but-unrotated credential pairs from combo lists, then automated ROPC sign-ins against tenant /token endpoints via the Azure CLI public client from IPv6/IPv4 bulletproof-hosting infrastructure. Where a tenant's Conditional Access did not cover the ROPC/Azure-CLI path, a valid pair minted a user-delegated token with no MFA — a silent compromise. Volume and source ASN shift over time (LSHIY → FranTech → 3xK Tech), but the ROPC-flow behavior is constant.
| Step | Behavior | Telemetry | Hunt Angle |
|---|---|---|---|
| 1 · Credential sourcing | Breached, never-rotated username/password combo lists | External (leaked-cred feeds) | GAP · enrich (leaked-cred monitoring) |
| 2 · Spray infrastructure | IPv6-only BYOIP (LSHIY) → FranTech → IPv4 low-and-slow (3xK) | Entra sign-in ip / asn | N3, Q3 |
| 3 · ROPC authentication | Azure CLI public client → ROPC grant at /token, no MFA prompt | Entra sign-in (protocol/app) | N1, Q1 |
| 4 · Spray pattern | Many failures (invalid creds) then occasional success per identity | Entra sign-in result/error | N2, N5, Q2, Q5 |
| 5 · CA/MFA bypass | Successful ROPC token where Conditional Access did not apply | Entra sign-in ca_status | N4, Q4 |
| 6 · Detection evasion | U.S.-geo-mislabeled IPs; ~12,800-IP rotation @ ~900/IP (3xK phase) | Entra sign-in geo/ip | N5, Q6 |
| 7 · Post-compromise | Token use for mail/data access (out of scope of the spray itself) | UAL / Graph activity | N6 (out-of-band) |
Consolidated IOC Table
| Type | Value | Conf | Action | Context |
|---|---|---|---|---|
| IPv6 range | 2a0a:d683::/32 | medium | hunt | LSHIY LLC, AS32167 (initial wave, Jun 12–26) — IPv6-only, no IPv4 |
| IPv6 range | 2605:6400::/32 | medium | hunt | FranTech, AS53667 (second wave, post-Jul 2) |
| IPv6 range | 2605:6404::/32 | medium | hunt | FranTech, AS53667 (second wave) |
| ASN | AS32167 / AS955 | medium | hunt | LSHIY LLC (HK / Wuhan / NY shared office); China-origin IPv6 |
| ASN | AS53667 | medium | hunt | FranTech — prior password-spray history |
| ASN | AS200373 | medium | hunt | 3xK Tech GmbH (surge from Jul 15) — IPv4 low-and-slow |
| Behavior (auth) | Azure CLI ROPC sign-in (protocol=ropc) minting token with no MFA | high | hunt | Core detection — flagship |
| Behavior (auth) | Successful ROPC sign-in where Conditional Access = notApplied | high | hunt | MFA/CAP-gap compromise |
| Behavior (auth) | Failure→success spray: many error 50126 then result 0 per identity | high | hunt | Credential-validity signal (triage by this, not volume) |
| Behavior (infra) | ~12,800 rotating source IPs @ ~900 attempts/IP (IPv4, 3xK phase) | medium | hunt | Low-and-slow evasion pattern |
| Reference constant | app "Microsoft Azure CLI" · appId 04b07795-8ddb-461a-bbee-02f9e1bf7b46 | n/a | enrich | Documented Microsoft public client — anchor, NOT a campaign IOC |
Affected Surface & Telemetry Matrix
| Surface | Required Telemetry | Priority | Gap Risk |
|---|---|---|---|
| Entra ID sign-in (interactive + non-interactive) | SigninLogs / AADNonInteractiveUserSignInLogs (app, protocol, CA status, result, IP, ASN) | Critical | Medium — non-interactive log stream must be enabled & retained |
| Conditional Access evaluation | SigninLogs conditionalAccessStatus + applied-policy detail | Critical | Medium — report-only policies mask true enforcement |
| Entra logs forwarded to Falcon NG-SIEM | Entra ID / Azure AD sign-in connector into Next-Gen SIEM | High | High — parser field names are tenant/parser-specific; validate |
| M365 Unified Audit Log (post-compromise) | MailItemsAccessed, inbox rules, app consents after a successful ROPC token | High | Medium — mailbox auditing must be on |
| Managed endpoints (Falcon EDR) | Process/network telemetry | Low | GAP — endpoint EDR does not observe cloud ROPC auth |
Search-UnifiedAuditLog. §8 CQL assumes the Entra sign-in stream is present in NG-SIEM; the field names are parser-dependent and must be validated against a real sample row.ATT&CK Mapping
| Tactic | Technique | Observed Behavior | Query / Control |
|---|---|---|---|
| Credential Access | T1110.003 — Brute Force: Password Spraying | 81M+ ROPC login attempts against many identities from few sources | N2, N5, Q2, Q5 |
| Initial Access | T1078.004 — Valid Accounts: Cloud Accounts | Replay of breached, never-rotated credential pairs | N1, N4, Q1, Q4 |
| Defense Evasion | T1556 — Modify Authentication Process (legacy/ROPC flow) | ROPC grant bypasses the authorization endpoint where CA/MFA is enforced | N1, N4, Q1, Q4 · §12 |
| Defense Evasion | T1550.001 — Use Alternate Auth Material: App Access Token | User-delegated token minted at /token with no interactive prompt | N4, Q4 |
| Defense Evasion | T1535 — Unused/Unsupported Cloud Regions / geo-mislabel | U.S.-geo-mislabeled source IPs evade trusted-location CA | N5, Q6 |
| Command & Control | T1090.003 — Proxy: Multi-hop (bulletproof BYOIP) | ASN rotation across LSHIY / FranTech / 3xK Tech hosting | N3, Q3 |
Native Audit-Log Hunts (Entra ID sign-in — KQL starters)
AADNonInteractiveUserSignInLogs as well as SigninLogs — hunt both. Confirm both sign-in streams are enabled and retained before trusting an absence of hits. Result codes: 0 = success, 50126 = invalid username/password (spray failure), 50053 = account locked.N1 · Azure CLI ROPC sign-ins (core)
All sign-ins to the Microsoft Azure CLI public client using the ROPC grant. Any successful row here is a token minted with no interactive MFA. Anchor on app + protocol; the app ID 04b07795-8ddb-461a-bbee-02f9e1bf7b46 is the documented Azure CLI public client.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46" or AppDisplayName == "Microsoft Azure CLI"
| where AuthenticationProtocol =~ "ropc" or tostring(parse_json(AuthenticationProcessingDetails)) has "ropc"
| project TimeGenerated, UserPrincipalName, ResultType, ConditionalAccessStatus,
IPAddress, AutonomousSystemNumber, Location, ClientAppUsed
| order by TimeGenerated asc
N2 · Failure → success spray transition per identity
Prioritize by credential validity, not volume: identities that show many ROPC failures (50126) followed by a success (0). These are the confirmed compromises hiding in the noise.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where AppDisplayName == "Microsoft Azure CLI" or AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| summarize Failures = countif(ResultType == 50126),
Successes = countif(ResultType == 0),
FirstFail = minif(TimeGenerated, ResultType == 50126),
FirstSuccess = minif(TimeGenerated, ResultType == 0),
SrcIPs = dcount(IPAddress)
by UserPrincipalName
| where Failures >= 5 and Successes >= 1 and FirstSuccess > FirstFail
| order by Successes desc
N3 · Sign-ins from campaign infrastructure (ASN / IPv6)
Corroborating infra hunt. Perishable — the actor rotates ASNs — so treat hits as enrichment, not a standalone verdict.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where AutonomousSystemNumber in (32167, 955, 53667, 200373)
or ipv6_is_in_any_range(IPAddress, dynamic(["2a0a:d683::/32","2605:6400::/32","2605:6404::/32"]))
| summarize Attempts = count(), Successes = countif(ResultType == 0),
Users = dcount(UserPrincipalName)
by AutonomousSystemNumber, AppDisplayName
| order by Attempts desc
N4 · Successful ROPC where Conditional Access did NOT apply
The MFA/CAP-gap compromise. A successful Azure CLI ROPC sign-in with conditionalAccessStatus == "notApplied" (or failure) means policy never evaluated the flow — exactly the gap this campaign exploits.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where AppDisplayName == "Microsoft Azure CLI" or AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| where ResultType == 0
| where ConditionalAccessStatus in ("notApplied","failure")
| project TimeGenerated, UserPrincipalName, ConditionalAccessStatus,
IPAddress, AutonomousSystemNumber, Location
| order by TimeGenerated asc
N5 · Password-spray shape (few sources → many identities)
Classic spray fan-out: a small set of source IPs authenticating against many distinct identities with a high failure ratio. Catches the pattern even before the source ASN is known.
union SigninLogs, AADNonInteractiveUserSignInLogs
| where AppDisplayName == "Microsoft Azure CLI" or AppId == "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
| summarize DistinctUsers = dcount(UserPrincipalName), Attempts = count(),
Failures = countif(ResultType == 50126)
by IPAddress, bin(TimeGenerated, 1h)
| where DistinctUsers >= 20 and Failures * 1.0 / Attempts > 0.8
| order by DistinctUsers desc
N6 · Post-compromise & CAP-configuration review (runbook)
Not a single query — a runbook item. For every identity surfaced by N2/N4: (a) pull UAL MailItemsAccessed, new inbox rules, and app-consent grants after the success timestamp to scope post-compromise activity; (b) audit the CAP that should have covered Azure CLI — check for app-scoping (not All Cloud Apps), user-group scoping, trusted-location exclusions, and report-only mode. Confirm userStrongAuthClientAuthNRequired is enforced (see §12).
CrowdStrike LogScale CQL Hunt Queries (Next-Gen SIEM — Entra sign-in)
app_display_name vs Vendor.appDisplayName vs event.app). Snake_case field names below are placeholders for your Entra parser's normalized fields — run each query against a 1-hour window with | head(10) first to confirm the real field shape, then adjust. Replace the REPLACE_WITH_ENTRA_SIGNIN_SOURCE tag with your Entra sign-in repository / data-source selector.Looks for: any sign-in to the Microsoft Azure CLI public client using the ROPC grant. FP: low — legitimate ROPC use with Azure CLI is rare and should itself be reviewed; allow-list known automation service accounts that legitimately use ROPC.
// HUNT: Azure CLI ROPC sign-ins (Entra sign-in telemetry) // MITRE: T1078.004, T1556 | CONF: high FP: low COST: low // REQUIRES: Entra ID sign-in logs in Falcon Next-Gen SIEM (parser fields vary) // FALSE POSITIVES: sanctioned automation using ROPC; allow-list those service accounts // REPLACE_WITH_ENTRA_SIGNIN_SOURCE: scope to your Entra sign-in repo/parser, e.g. #repo="ngsiem-entra-signin" app_display_name = "Microsoft Azure CLI" | authentication_protocol = /ropc/i | table([@timestamp, user_principal_name, result_type, conditional_access_status, src_ip, asn, location, client_app_used], limit=200)
Anchor alternative: app_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" (documented Azure CLI public client).
Looks for: identities with many ROPC failures (50126) and at least one success (0) — the credential-validity signal. FP: low — a genuine user fat-fingering a password rarely reaches 5+ failures via the CLI ROPC path; tune the failure floor to your baseline.
// HUNT: ROPC failure->success spray transition (credential validity) // MITRE: T1110.003, T1078.004 | CONF: high FP: low COST: med // REQUIRES: Entra sign-in logs in NG-SIEM; result codes 0=success, 50126=bad creds // FALSE POSITIVES: user password churn; raise Failures floor to baseline // TUNING: allow-list automation service accounts; adjust Failures >= threshold per tenant app_display_name = "Microsoft Azure CLI" | authentication_protocol = /ropc/i | case { result_type = "0" | outcome := "success"; result_type = "50126" | outcome := "fail"; * | outcome := "other"; } | groupBy([user_principal_name], function=[count(outcome, as=Total), count(src_ip, distinct=true, as=SrcIPs), selectFromMax(field=@timestamp, include=[outcome])]) | Total >= 6 | table([user_principal_name, Total, SrcIPs], limit=200)
ResultType field.Looks for: any Entra sign-in sourced from the known campaign IPv6 ranges or ASNs. FP: low but perishable — infra rotates; a miss here does NOT clear the tenant. Use as corroboration for Q1/Q2 hits.
// HUNT: Entra sign-ins from LSHIY / FranTech / 3xK campaign infra // MITRE: T1090.003 | CONF: medium FP: low COST: low // REQUIRES: Entra sign-in logs in NG-SIEM with source ip + asn fields // FALSE POSITIVES: minimal; infra is perishable, treat as enrichment not verdict in(asn, values=["32167","955","53667","200373"]) | cidr(src_ip, subnet=["2a0a:d683::/32", "2605:6400::/32", "2605:6404::/32"]) | table([@timestamp, user_principal_name, app_display_name, authentication_protocol, result_type, src_ip, asn], limit=200)
Note: the in(asn,...) and cidr(...) lines are alternative anchors — run each separately if your parser populates only one of asn / src_ip.
Looks for: a successful Azure CLI ROPC token where Conditional Access was notApplied or failure — the exact MFA-gap compromise. FP: low — this is a high-fidelity outcome; every hit warrants identity triage (N6).
// HUNT: successful ROPC token with Conditional Access not enforced // MITRE: T1556, T1550.001 | CONF: high FP: low COST: low // REQUIRES: Entra sign-in logs in NG-SIEM incl. conditional_access_status // FALSE POSITIVES: sanctioned ROPC automation exempt from CA; allow-list explicitly app_display_name = "Microsoft Azure CLI" | authentication_protocol = /ropc/i | result_type = "0" | in(conditional_access_status, values=["notApplied", "failure"]) | table([@timestamp, user_principal_name, conditional_access_status, src_ip, asn, location], limit=200)
Looks for: a single source IP authenticating against many distinct identities with a high failure ratio (spray shape). FP: medium — shared egress NAT / gateways can look like fan-out; exclude your own outbound proxy ranges and known federation gateways.
// HUNT: ROPC spray fan-out - one IP against many identities // MITRE: T1110.003 | CONF: medium FP: medium COST: med // REQUIRES: Entra sign-in logs in NG-SIEM // FALSE POSITIVES: corporate NAT/proxy egress; exclude your gateway ranges (e.g. 192.0.2.0/24) // TUNING: raise DistinctUsers threshold; exclude known federation/proxy src_ip app_display_name = "Microsoft Azure CLI" | authentication_protocol = /ropc/i | groupBy([src_ip], function=[count(user_principal_name, distinct=true, as=DistinctUsers), count(as=Attempts)]) | DistinctUsers >= 20 | table([src_ip, DistinctUsers, Attempts], limit=200)
Looks for: the evasion pattern seen in the 3xK Tech phase — a single identity targeted from many distinct source IPs (attackers rotating ~12,800 IPs at ~900 attempts each to keep per-IP volume low). FP: medium — genuine roaming/VPN users hit multiple IPs; scope to the Azure CLI ROPC path and pair with N2 credential-validity.
// HUNT: many source IPs against one identity (low-and-slow rotation) // MITRE: T1110.003, T1535 | CONF: medium FP: medium COST: med // REQUIRES: Entra sign-in logs in NG-SIEM // FALSE POSITIVES: roaming/VPN users; scope to ROPC + correlate with N2 // TUNING: tune DistinctSrcIPs to your roaming baseline app_display_name = "Microsoft Azure CLI" | authentication_protocol = /ropc/i | groupBy([user_principal_name], function=[count(src_ip, distinct=true, as=DistinctSrcIPs), count(as=Attempts)]) | DistinctSrcIPs >= 15 | table([user_principal_name, DistinctSrcIPs, Attempts], limit=200)
CrowdStrike Custom IOA Recommendations
| Rule (NG-SIEM / Entra) | Logic | Benign Exclusions | Deployment Path |
|---|---|---|---|
| Azure CLI ROPC — successful token, CA not applied | Q4 / N4 — result=0, protocol=ropc, ca_status in (notApplied, failure) | Sanctioned ROPC automation service accounts (explicit allow-list) | NG-SIEM → Correlation Rule → alert; or Sentinel analytics rule |
| Azure CLI ROPC — credential-validity (fail→success) | Q2 / N2 — many 50126 then a 0 per identity | Password-churn users below failure floor | NG-SIEM scheduled search → alert on match |
| Any Azure CLI ROPC sign-in (posture) | Q1 / N1 — app=Azure CLI, protocol=ropc | Known automation; otherwise ROPC should not occur | NG-SIEM scheduled search → weekly posture review |
| Entra ID Protection — leaked-credential / password-spray risk | Native risk detections tied to these identities | Tune per Identity Protection baseline | Entra ID Protection → risk-based CA policy |
Machine-Readable IOC Appendix
Grouped IOC Quick-Copy
Infrastructure IOCs are perishable (three ASN rotations in weeks) — the durable surface is the ROPC-flow behavior. Every atomic value here appears verbatim in a saved source snapshot. The Azure CLI app ID is a documented Microsoft public-client constant, not a campaign IOC.
type,value,action,severity,expiration,description,tags ipv6,2a0a:d683::/32,detect,high,2026-10-20,LSHIY AS32167 ROPC spray infra (IPv6-only),campaign:azurecli-ropc-lshiy ipv6,2605:6400::/32,detect,high,2026-10-20,FranTech AS53667 ROPC spray infra,campaign:azurecli-ropc-lshiy ipv6,2605:6404::/32,detect,high,2026-10-20,FranTech AS53667 ROPC spray infra,campaign:azurecli-ropc-lshiy ipv4,REPLACE_WITH_3XK_IPV4,hunt,medium,2026-10-20,3xK Tech AS200373 IPv4 low-and-slow (rotating ~12800 IPs),campaign:azurecli-ropc-lshiy
AS32167 # LSHIY LLC (initial wave, Jun 12-26; IPv6-only 2a0a:d683::/32) AS955 # LSHIY LLC (secondary ASN, China-origin IPv6) AS53667 # FranTech (second wave; 2605:6400::/32, 2605:6404::/32) AS200373 # 3xK Tech GmbH (surge from Jul 15; IPv4 low-and-slow)
# Run in Sentinel / Defender Advanced Hunting (SigninLogs + AADNonInteractiveUserSignInLogs) N1 Azure CLI ROPC sign-ins (app=Microsoft Azure CLI, protocol=ropc) N2 Failure->success per identity (many 50126 then a 0) - triage by validity, NOT volume N3 Sign-ins from AS32167 / AS955 / AS53667 / AS200373 or the 3 IPv6 ranges (enrichment) N4 result=0 AND conditional_access_status in (notApplied, failure) = MFA-gap compromise N5 spray fan-out: one src_ip vs >= 20 identities with > 80% failures N6 (runbook) post-compromise UAL (MailItemsAccessed, inbox rules, app consent) + CAP config audit # Verify both sign-in log streams enabled + retained; confirm userStrongAuthClientAuthNRequired enforced
Sign-in to "Microsoft Azure CLI" public client using the ROPC (Resource Owner Password Credentials) grant Successful ROPC token (result 0) with conditional_access_status = notApplied or failure Per-identity many invalid-credential failures (50126) followed by a success (0) One source IP authenticating against many distinct identities (>80% failure ratio) One identity sprayed from many rotating source IPs at low per-IP volume (3xK phase) Source IP geo-mislabeled to U.S. while ASN resolves to China / bulletproof hosting
app_display_name = "Microsoft Azure CLI" app_id = 04b07795-8ddb-461a-bbee-02f9e1bf7b46 # documented Microsoft first-party public client authentication_protocol = ropc # deprecated OAuth 2.0 grant, no MFA/SSO support token_endpoint = https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token # These identify the flow being abused; they are legitimate Microsoft values, not attacker infrastructure.
Require MFA (or block) for All Users + All Cloud Apps + All Client App types, unconditionally Block legacy authentication clients via Conditional Access (ROPC rides the legacy path) Enforce userStrongAuthClientAuthNRequired to block ROPC flows at the client-auth level Restrict the "Microsoft Azure CLI" application to admins / required-assignment only Force password reset for any identity with a successful ROPC sign-in; assume breached combo Do NOT triage by spray volume; prioritize by credential validity (failure->success) Review CAPs for app-scoping, group-scoping, trusted-location exclusions, report-only mode
Detection Validation Gates
| Gate | Check | Pass Criterion |
|---|---|---|
| Telemetry ready | Both SigninLogs and AADNonInteractiveUserSignInLogs enabled and retained | ROPC rows visible; ≥ 90-day retention (campaign window Jun 12+) |
| Parser validated (NG-SIEM) | Entra sign-in stream present in Falcon NG-SIEM; snake_case field names confirmed on a real row | app_display_name, authentication_protocol, result_type, conditional_access_status, src_ip, asn resolve to populated fields |
| Field mapping | Result codes map correctly (0=success, 50126=bad creds, 50053=locked) | N2/Q2 fire on a known benign failed sign-in in lab |
| Benign baseline | Enumerate any sanctioned ROPC automation service accounts | Allow-list built before promoting Q1/Q4 to alerts |
| Positive test | Generate a controlled Azure CLI ROPC sign-in against a test identity | N1/Q1 fire; N4/Q4 fire if CA is intentionally unscoped for that test |
| Promotion | Promote Q4/N4 (successful ROPC + CA-notApplied) and Q2/N2 (validity) to alerts; keep Q3/Q5/Q6 as tuned hunts | Alerts wired with documented exclusions; ASN hunt treated as enrichment |
Hardening — Tiered & Deployable
The fix is closing the ROPC/MFA gap, not chasing the source ASN. Blocking legacy auth and requiring MFA unconditionally makes a valid stolen credential insufficient — which is the whole game here.
- Conditional Access: require MFA (or block) for All Users + All Cloud Apps + All Client App types, unconditionally — M1032 / M1036. Removes the app-, group-, and location-scoping gaps this campaign exploited.
- Block legacy authentication clients via Conditional Access — M1042. ROPC rides the legacy/other-clients path that bypasses the authorization endpoint.
- Force password reset + session revoke for any identity with a successful ROPC sign-in — M1027. Assume the credential pair is on a breached combo list.
- Restrict the "Microsoft Azure CLI" enterprise application to required-assignment (admins only) — M1018. Non-admins have no business authenticating the CLI.
- Enforce
userStrongAuthClientAuthNRequired— M1032. Requires strong authentication at the client-authentication level and blocks ROPC flows outright (Huntress-recommended). Pilot on a ring; some legacy automation may break. - Deploy Entra ID Protection risk-based CA (leaked-credential + password-spray risk) — M1032. Catches breached-combo replay independent of source infra.
- Enable Entra smart lockout + banned-password / custom-banned-password lists — M1027 / M1036. Raises the cost of the spray.
- Re-audit named/trusted locations — M1037. The attacker's U.S.-geo-mislabeled IPs evaded trusted-location exclusions; do not treat geo as an MFA exemption.
- Phase out ROPC-capable public-client usage org-wide; migrate automation to workload identities / managed identities with certificates — M1042.
- Phishing-resistant MFA (FIDO2 / passkeys) for privileged and high-value identities — M1032.
- Continuous leaked-credential monitoring + mandatory rotation policy — M1027 / M1047. Breaks the breached-combo replay at the source.
- Standing Entra sign-in monitoring with credential-validity (not volume) alerting — M1047.
Deployable Playbooks
1 · Block legacy authentication + require MFA for all cloud apps (Microsoft Graph PowerShell)
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess","Application.Read.All"
# Baseline policy: require MFA for ALL users / ALL apps / ALL client types (report-only first, then enforce)
$params = @{
DisplayName = "CA - Require MFA All Apps All Clients (ROPC gap closure)"
State = "enabledForReportingButNotEnforced" # flip to "enabled" after validation
Conditions = @{
Users = @{ IncludeUsers = @("All") }
Applications = @{ IncludeApplications = @("All") }
ClientAppTypes = @("all") # includes legacy/other-clients (ROPC path)
}
GrantControls = @{ Operator = "OR"; BuiltInControls = @("mfa") }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
# Separate policy to BLOCK legacy auth explicitly:
# ClientAppTypes = @("exchangeActiveSync","other"); GrantControls BuiltInControls = @("block")
2 · Restrict the Microsoft Azure CLI application to assigned admins
# Entra admin center -> Enterprise applications -> Microsoft Azure CLI # Properties: "Assignment required?" = Yes; "Enabled for users to sign-in?" = Yes (assigned only) # Users and groups: assign only the admin group that legitimately uses the CLI. # Graph equivalent (set appRoleAssignmentRequired on the service principal): Connect-MgGraph -Scopes "Application.ReadWrite.All" $sp = Get-MgServicePrincipal -Filter "appId eq '04b07795-8ddb-461a-bbee-02f9e1bf7b46'" Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AppRoleAssignmentRequired:$true
3 · Revoke sessions + force reset for identities with a successful ROPC sign-in
Connect-MgGraph -Scopes "User.ReadWrite.All"
# For each UPN surfaced by N2/N4 (successful ROPC):
Revoke-MgUserSignInSession -UserId user@contoso.example # invalidates refresh tokens
Update-MgUser -UserId user@contoso.example -PasswordProfile @{
ForceChangePasswordNextSignIn = $true # force rotation of the breached pair
}
# Then confirm the user is in scope of the MFA CA policy from playbook #1.
4 · Confirm ROPC is blocked at the client-auth level
# Enforce strong client authentication (blocks ROPC). Validate via a controlled ROPC attempt: # az login -u <test-upn> -p <pw> --allow-no-subscriptions # (lab/test tenant only) # Expected AFTER hardening: sign-in FAILS / is interrupted; N1/Q1 shows no successful ROPC token. # Re-run N4/Q4: zero successful ROPC + CA-notApplied rows for the test identity.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Revoke sign-in sessions/refresh tokens for every identity with a successful ROPC sign-in (N2/N4); add campaign ASNs/IPv6 ranges as blocked named locations; preserve SigninLogs + UAL before any change | SOC L2 / IAM | Revocation records; N1–N5 exports; blocked-location config |
| Triage | Enumerate all successful Azure CLI ROPC sign-ins from Jun 12 onward; map which had CA notApplied; scope post-compromise UAL (mail access, inbox rules, app consents) per identity | IR | Compromised-identity list; sign-in timeline; UAL activity |
| Eradicate | Force password reset for confirmed identities (breached combos); require MFA for All Users/Apps/Clients; block legacy auth; enforce userStrongAuthClientAuthNRequired; remove any attacker-created inbox rules / OAuth grants | IR + IAM | Reset log; CA policy diffs; removed rules/grants |
| Recover | Restore access with new credentials + registered MFA; validate CA now covers the ROPC/CLI path; monitor for re-spray from a new ASN (the actor moves providers quickly) | IR + IT | Re-enable tickets; CA coverage test; new-ASN watch |
| Harden | Restrict Azure CLI app; deploy Identity Protection risk policies; smart lockout + banned-password lists; stand up credential-validity alerting (not volume-based) | Detection Eng | Policy state; alerts wired; posture review scheduled |
Detection Coverage Map
| Technique | Behavior | CQL | Native / Rule | Coverage |
|---|---|---|---|---|
| T1078.004 / T1556 | Azure CLI ROPC sign-in (posture) | Q1 | N1 | Good (IdP telemetry) |
| T1110.003 / T1078.004 | Failure→success credential validity | Q2 | N2 | Good — triage anchor |
| T1556 / T1550.001 | Successful ROPC + CA notApplied | Q4 | N4 | Good — highest fidelity |
| T1110.003 | Spray fan-out (one IP → many users) | Q5 | N5 | Partial needs NAT/proxy exclusions |
| T1110.003 / T1535 | Low-and-slow IP rotation (3xK) | Q6 | N5 | Partial roaming-user FP |
| T1090.003 | Campaign ASN / IPv6 infra | Q3 | N3 | Partial perishable — enrichment only |
| — | Post-compromise mail/data access | — | N6 (UAL, out-of-band) | Partial needs mailbox auditing on |
| — | Breached-credential sourcing | — | Identity Protection (leaked-cred) | GAP external — leaked-cred monitoring |
| — | Endpoint EDR visibility of ROPC auth | — | — | N/A cloud identity — no endpoint signal |
Validation gates: confirm both sign-in log streams (interactive + non-interactive) are ON and retained across the campaign window (Jun 12+); validate the NG-SIEM Entra parser field names on a real row before trusting Q1–Q6; build the sanctioned-ROPC allow-list; lab-test a controlled ROPC sign-in for N1/Q1 and N4/Q4. Promote Q4/N4 and Q2/N2 to alerts; keep Q3/Q5/Q6 as tuned hunts. The infra IOC hunt (Q3/N3) is perishable — a clean result does not clear the tenant.
Hunt Summary Ticket
TITLE: Hunt — Azure CLI ROPC Password-Spray (LSHIY / FranTech / 3xK Tech campaign)
SEVERITY: High/Critical (identity; 81M+ attempts; 78 accts / 64 orgs; ROPC bypasses MFA/CAP)
SCOPE: Entra ID / Microsoft 365 identity; Azure CLI public client; ROPC /token flow
HYPOTHESIS: Breached, never-rotated credential pairs replayed via Azure CLI ROPC at the tenant
/token endpoint -> token minted with NO MFA where Conditional Access does not cover
the ROPC/CLI path -> silent cloud-account compromise. Source ASN rotates; behavior constant.
QUERIES RUN: Native N1 Azure CLI ROPC sign-ins | N2 failure->success validity | N3 campaign ASN/IPv6 |
N4 successful ROPC + CA notApplied | N5 spray fan-out | N6 post-compromise + CAP audit
CQL Q1 ROPC sign-ins | Q2 validity transition | Q3 campaign infra | Q4 ROPC+CA-notApplied |
Q5 fan-out (IP->users) | Q6 low-and-slow IP rotation
DO FIRST: N1 + N4 — a successful ROPC token with CA notApplied IS the compromise.
Triage by credential validity (N2), NOT by spray volume.
FINDINGS: <pending analyst execution>
GAPS: Infra IOCs perishable (LSHIY->FranTech->3xK, 3 rotations); breached-combo sourcing is
external; endpoint EDR has no visibility (cloud identity attack)
ACTIONS: Revoke sessions + reset for successful-ROPC identities; require MFA All Users/Apps/Clients;
block legacy auth; enforce userStrongAuthClientAuthNRequired; restrict Azure CLI app
OWNER: HuntPack
VERSION: v0.2 - 2026-07-22
Changelog
table() calls now carry an explicit row limit: the default is 200 and truncation is silent, so a capped result was indistinguishable from a complete one. Atomic-IOC sweeps (filters over 5+ hashes or C2 IPs) use limit=max so a wide infection is never silently under-scoped; behavioural hunts use limit=200, where exceeding the cap indicates the query needs tuning. Where present, event names that do not exist in the Falcon data model were corrected (e.g. ServiceInstalled is a Sysmon concept, not a Falcon event; ElfFileWritten is ELFFileWritten) — such queries could never return a row. No detection logic, fields, or IOCs changed.References
| Tier | Source | Used For | Access Date |
|---|---|---|---|
| 1 | Huntress — No (Bad) CAP: Inside an Ongoing LSHIY Password Spray Attack | Primary: counts, timeline, ROPC mechanics, CAP/MFA gaps, LSHIY/FranTech/3xK infra, userStrongAuthClientAuthNRequired | 2026-07-22 |
| 1 | The Hacker News — Azure CLI Password Spray Hits at Least 78 Accounts in 81M+ Attempts | Corroboration of counts, all ASNs + IPv6 ranges, ROPC /token mechanics, migration timeline | 2026-07-22 |
| 1 | SecurityWeek — Massive Password Spray Campaign Targeting Azure CLI | AS32167/AS955 attribution; 155× credential-spray rise; combo-list targeting | 2026-07-22 |
| 2 | Security Affairs — Azure CLI Targeted in LSHIY Campaign Across 64 Orgs | Independent corroboration of LSHIY infra, ROPC method, mitigation | 2026-07-22 |
| 2 | MITRE ATT&CK T1110.003 — Brute Force: Password Spraying | Technique mapping (spray) + M1032/M1036/M1027 mitigations | 2026-07-22 |
HuntPack v0.1 · Azure CLI ROPC Password-Spray (LSHIY campaign) · Generated 2026-07-22 · Defensive use only — no offensive code. This is an identity / IdP hunt: the compromise lives in Entra ID sign-in telemetry, not on the endpoint. Hunt the ROPC-flow behavior (N1/N4) first, triage by credential validity (N2) not spray volume, and close the gap with unconditional MFA + legacy-auth blocking + userStrongAuthClientAuthNRequired. NG-SIEM Entra parser field names are tenant-specific — validate before promoting any query.