Azure CLI ROPC Password-Spray — LSHIY Campaign (Entra ID / Microsoft 365)

A large-scale, automated identity password-spray campaign (reported by Huntress, surfaced 2026-07) that abuses the deprecated OAuth 2.0 Resource Owner Password Credentials (ROPC) flow via the Microsoft Azure CLI public client to validate previously-breached credential pairs against Entra ID / Microsoft 365. ROPC sends the password straight to the /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.
Threat
Azure CLI ROPC password-spray · LSHIY / FranTech / 3xK Tech
Severity
IDENTITY · ACTIVE ITW
Type
Password spray · ROPC MFA-gap · cloud identity / Entra ID
Access
Breached credential replay via Azure CLI ROPC /token
Infra
IPv6-only BYOIP (LSHIY AS32167) → FranTech AS53667 → 3xK Tech AS200373
Version
v0.2 · 2026-07-22
Author
HuntPack
Confidence
High (Huntress primary + multi-source, July 2026)
01

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.

Defender priority: This lives in identity telemetry (Entra sign-in logs), not on the endpoint — Falcon endpoint EDR will not see it. Hunt N1 (Azure CLI ROPC sign-ins) and N4 (successful ROPC where Conditional Access was notApplied) first: a successful ROPC token with no CA enforcement is the compromise. Do not triage by spray volume — that points at the most-sprayed, least-compromised tenants. Prioritize by credential validity (failure→success transitions). Chasing the source ASN is whack-a-mole; the durable fix is blocking ROPC/legacy auth and requiring MFA for All Users + All Cloud Apps + All Client Apps unconditionally.
02

Source Review & Web Hunter Notes

TierSourceKey FindingCarry
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; userStrongAuthClientAuthNRequiredyes
1 · NewsThe Hacker News — Azure CLI Password Spray Hits at Least 78 AccountsCorroborates counts, timeline, all three ASNs and IPv6 ranges; ROPC /token mechanics; migration timelineyes
1 · NewsSecurityWeek — Massive Password Spray Campaign Targeting Azure CLIAS32167/AS955 attribution; 155× rise in credential-spray volume; combo-list targeting; abuse-report non-responseyes
2 · NewsSecurity Affairs — Azure CLI Targeted in LSHIY Campaign Across 64 OrgsIndependent corroboration of LSHIY infra, ROPC method, userStrongAuthClientAuthNRequired mitigationpartial
2 · NewsTechRepublic — Azure CLI Password Spray Exposes M365 MFA GapMFA-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).

03

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.

StepBehaviorTelemetryHunt Angle
1 · Credential sourcingBreached, never-rotated username/password combo listsExternal (leaked-cred feeds)GAP · enrich (leaked-cred monitoring)
2 · Spray infrastructureIPv6-only BYOIP (LSHIY) → FranTech → IPv4 low-and-slow (3xK)Entra sign-in ip / asnN3, Q3
3 · ROPC authenticationAzure CLI public client → ROPC grant at /token, no MFA promptEntra sign-in (protocol/app)N1, Q1
4 · Spray patternMany failures (invalid creds) then occasional success per identityEntra sign-in result/errorN2, N5, Q2, Q5
5 · CA/MFA bypassSuccessful ROPC token where Conditional Access did not applyEntra sign-in ca_statusN4, Q4
6 · Detection evasionU.S.-geo-mislabeled IPs; ~12,800-IP rotation @ ~900/IP (3xK phase)Entra sign-in geo/ipN5, Q6
7 · Post-compromiseToken use for mail/data access (out of scope of the spray itself)UAL / Graph activityN6 (out-of-band)
04

Consolidated IOC Table

Infrastructure is perishable — the actor moved ASNs three times in weeks (LSHIY → FranTech → 3xK Tech). Use the IPv6 ranges/ASNs below as a corroborating signal, not a primary detection; anchor on the ROPC-flow behavior. All atomic values below appear verbatim in the saved source snapshots. The Azure CLI application ID is a documented Microsoft public-client constant, not a campaign-attributed IOC.
TypeValueConfActionContext
IPv6 range2a0a:d683::/32mediumhuntLSHIY LLC, AS32167 (initial wave, Jun 12–26) — IPv6-only, no IPv4
IPv6 range2605:6400::/32mediumhuntFranTech, AS53667 (second wave, post-Jul 2)
IPv6 range2605:6404::/32mediumhuntFranTech, AS53667 (second wave)
ASNAS32167 / AS955mediumhuntLSHIY LLC (HK / Wuhan / NY shared office); China-origin IPv6
ASNAS53667mediumhuntFranTech — prior password-spray history
ASNAS200373mediumhunt3xK Tech GmbH (surge from Jul 15) — IPv4 low-and-slow
Behavior (auth)Azure CLI ROPC sign-in (protocol=ropc) minting token with no MFAhighhuntCore detection — flagship
Behavior (auth)Successful ROPC sign-in where Conditional Access = notAppliedhighhuntMFA/CAP-gap compromise
Behavior (auth)Failure→success spray: many error 50126 then result 0 per identityhighhuntCredential-validity signal (triage by this, not volume)
Behavior (infra)~12,800 rotating source IPs @ ~900 attempts/IP (IPv4, 3xK phase)mediumhuntLow-and-slow evasion pattern
Reference constantapp "Microsoft Azure CLI" · appId 04b07795-8ddb-461a-bbee-02f9e1bf7b46n/aenrichDocumented Microsoft public client — anchor, NOT a campaign IOC
05

Affected Surface & Telemetry Matrix

SurfaceRequired TelemetryPriorityGap Risk
Entra ID sign-in (interactive + non-interactive)SigninLogs / AADNonInteractiveUserSignInLogs (app, protocol, CA status, result, IP, ASN)CriticalMedium — non-interactive log stream must be enabled & retained
Conditional Access evaluationSigninLogs conditionalAccessStatus + applied-policy detailCriticalMedium — report-only policies mask true enforcement
Entra logs forwarded to Falcon NG-SIEMEntra ID / Azure AD sign-in connector into Next-Gen SIEMHighHigh — parser field names are tenant/parser-specific; validate
M365 Unified Audit Log (post-compromise)MailItemsAccessed, inbox rules, app consents after a successful ROPC tokenHighMedium — mailbox auditing must be on
Managed endpoints (Falcon EDR)Process/network telemetryLowGAP — endpoint EDR does not observe cloud ROPC auth
This is an IdP-telemetry hunt. If your Entra sign-in logs are not flowing into Falcon Next-Gen SIEM, run §7 natively in Sentinel / Defender Advanced Hunting / 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.
06

ATT&CK Mapping

TacticTechniqueObserved BehaviorQuery / Control
Credential AccessT1110.003 — Brute Force: Password Spraying81M+ ROPC login attempts against many identities from few sourcesN2, N5, Q2, Q5
Initial AccessT1078.004 — Valid Accounts: Cloud AccountsReplay of breached, never-rotated credential pairsN1, N4, Q1, Q4
Defense EvasionT1556 — Modify Authentication Process (legacy/ROPC flow)ROPC grant bypasses the authorization endpoint where CA/MFA is enforcedN1, N4, Q1, Q4 · §12
Defense EvasionT1550.001 — Use Alternate Auth Material: App Access TokenUser-delegated token minted at /token with no interactive promptN4, Q4
Defense EvasionT1535 — Unused/Unsupported Cloud Regions / geo-mislabelU.S.-geo-mislabeled source IPs evade trusted-location CAN5, Q6
Command & ControlT1090.003 — Proxy: Multi-hop (bulletproof BYOIP)ASN rotation across LSHIY / FranTech / 3xK Tech hostingN3, Q3
07

Native Audit-Log Hunts (Entra ID sign-in — KQL starters)

This is the highest-value section for this threat — the compromise lives in Entra ID sign-in telemetry. Run these as KQL in Microsoft Sentinel / Defender Advanced Hunting. ROPC sign-ins frequently appear in 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).

08

CrowdStrike LogScale CQL Hunt Queries (Next-Gen SIEM — Entra sign-in)

Pick your tenant's cloud first — every "Open in Falcon" button below uses this selection.
Parser-dependent fields. These queries run over Microsoft Entra ID sign-in logs forwarded to Falcon Next-Gen SIEM, not endpoint telemetry — Falcon EDR does not see cloud ROPC auth. SaaS/IdP field names are parser-prefixed per tenant (e.g. 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.
Q1 · Azure CLI ROPC sign-ins (core detection)
CONF HIGHFP LOWCOST LOW

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).

Q2 · Failure → success spray transition per identity
CONF HIGHFP LOWCOST MED

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)
TUNING: this shape counts activity per identity; pair with N2 (KQL) for the exact failure-then-success ordering, which is cleaner to express against the native ResultType field.
Q3 · Sign-ins from campaign infrastructure (ASN / IPv6)
CONF MEDFP LOWCOST LOW

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.

Q4 · Successful ROPC where Conditional Access not applied
CONF HIGHFP LOWCOST LOW

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)
Q5 · Password-spray fan-out (few IPs → many identities)
CONF MEDFP MEDCOST MED

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)
Q6 · Low-and-slow IP rotation per identity (3xK phase)
CONF MEDFP MEDCOST MED

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)
09

CrowdStrike Custom IOA Recommendations

There is no endpoint Custom IOA for this threat — it is a cloud identity / IdP attack that never touches a managed endpoint. "Detection" here means Next-Gen SIEM scheduled searches / correlation rules over Entra sign-in telemetry, plus Entra ID Protection risk policies. The table maps the promotable hunts to that surface.
Rule (NG-SIEM / Entra)LogicBenign ExclusionsDeployment Path
Azure CLI ROPC — successful token, CA not appliedQ4 / 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 identityPassword-churn users below failure floorNG-SIEM scheduled search → alert on match
Any Azure CLI ROPC sign-in (posture)Q1 / N1 — app=Azure CLI, protocol=ropcKnown automation; otherwise ROPC should not occurNG-SIEM scheduled search → weekly posture review
Entra ID Protection — leaked-credential / password-spray riskNative risk detections tied to these identitiesTune per Identity Protection baselineEntra ID Protection → risk-based CA policy
10

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.

Falcon IOC Management CSVbulk import
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
ASNs — campaign infrastructureblock / named-location
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)
Entra Sign-in Hunt ChecklistIdP telemetry
# 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
Behavioral Signaturesdetection logic
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
Reference constant (NOT a campaign IOC)Microsoft public client
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.
Hardening / Conditional Access Auditdefense-in-depth
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
11

Detection Validation Gates

GateCheckPass Criterion
Telemetry readyBoth SigninLogs and AADNonInteractiveUserSignInLogs enabled and retainedROPC 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 rowapp_display_name, authentication_protocol, result_type, conditional_access_status, src_ip, asn resolve to populated fields
Field mappingResult codes map correctly (0=success, 50126=bad creds, 50053=locked)N2/Q2 fire on a known benign failed sign-in in lab
Benign baselineEnumerate any sanctioned ROPC automation service accountsAllow-list built before promoting Q1/Q4 to alerts
Positive testGenerate a controlled Azure CLI ROPC sign-in against a test identityN1/Q1 fire; N4/Q4 fire if CA is intentionally unscoped for that test
PromotionPromote Q4/N4 (successful ROPC + CA-notApplied) and Q2/N2 (validity) to alerts; keep Q3/Q5/Q6 as tuned huntsAlerts wired with documented exclusions; ASN hunt treated as enrichment
12

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.

Immediate — Close the ROPC / MFA gap
  • Conditional Access: require MFA (or block) for All Users + All Cloud Apps + All Client App types, unconditionallyM1032 / M1036. Removes the app-, group-, and location-scoping gaps this campaign exploited.
  • Block legacy authentication clients via Conditional AccessM1042. 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-inM1027. 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.
Near term — Enforce strong client auth (pilot first)
  • Enforce userStrongAuthClientAuthNRequiredM1032. 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 listsM1027 / M1036. Raises the cost of the spray.
  • Re-audit named/trusted locationsM1037. The attacker's U.S.-geo-mislabeled IPs evaded trusted-location exclusions; do not treat geo as an MFA exemption.
Strategic — Retire legacy auth surface
  • Phase out ROPC-capable public-client usage org-wide; migrate automation to workload identities / managed identities with certificatesM1042.
  • Phishing-resistant MFA (FIDO2 / passkeys) for privileged and high-value identitiesM1032.
  • Continuous leaked-credential monitoring + mandatory rotation policyM1027 / M1047. Breaks the breached-combo replay at the source.
  • Standing Entra sign-in monitoring with credential-validity (not volume) alertingM1047.
13

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.
14

Containment Runbook

PhaseActionsOwnerEvidence
IsolateRevoke 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 changeSOC L2 / IAMRevocation records; N1–N5 exports; blocked-location config
TriageEnumerate 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 identityIRCompromised-identity list; sign-in timeline; UAL activity
EradicateForce 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 grantsIR + IAMReset log; CA policy diffs; removed rules/grants
RecoverRestore 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 + ITRe-enable tickets; CA coverage test; new-ASN watch
HardenRestrict Azure CLI app; deploy Identity Protection risk policies; smart lockout + banned-password lists; stand up credential-validity alerting (not volume-based)Detection EngPolicy state; alerts wired; posture review scheduled
⚠ Do not rely on blocking the source ASN as containment — the actor rotated LSHIY → FranTech → 3xK Tech in weeks and re-sprayed 87% of the same accounts. The durable containment is credential rotation + ROPC/legacy-auth blocking + unconditional MFA, which makes a valid stolen credential insufficient regardless of source.
15

Detection Coverage Map

TechniqueBehaviorCQLNative / RuleCoverage
T1078.004 / T1556Azure CLI ROPC sign-in (posture)Q1N1Good (IdP telemetry)
T1110.003 / T1078.004Failure→success credential validityQ2N2Good — triage anchor
T1556 / T1550.001Successful ROPC + CA notAppliedQ4N4Good — highest fidelity
T1110.003Spray fan-out (one IP → many users)Q5N5Partial needs NAT/proxy exclusions
T1110.003 / T1535Low-and-slow IP rotation (3xK)Q6N5Partial roaming-user FP
T1090.003Campaign ASN / IPv6 infraQ3N3Partial perishable — enrichment only
Post-compromise mail/data accessN6 (UAL, out-of-band)Partial needs mailbox auditing on
Breached-credential sourcingIdentity Protection (leaked-cred)GAP external — leaked-cred monitoring
Endpoint EDR visibility of ROPC authN/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.

16

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
17

Changelog

v0.22026-07-24CQL correctness pass. All 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.
v0.12026-07-22Initial pack — Azure CLI ROPC Password-Spray (LSHIY / FranTech / 3xK Tech campaign). Built on the review-v2 gold layout (fixed left-sidebar scrollspy TOC, collapsible, Falcon cloud selector inside the CQL section, per-card Copy/Open-in-Falcon, Grouped IOC Quick-Copy grid, coverage map, validation gates, .ticket). Identity-first: 6 native Entra sign-in KQL hunts (N1–N6) plus 6 Falcon Next-Gen SIEM CQL hunts (Q1–Q6) over Entra sign-in telemetry — anchored on ROPC-flow behavior (Azure CLI public client, protocol=ropc, failure→success validity, successful token with Conditional Access notApplied). Atomic infra IOCs: 3 IPv6 ranges (2a0a:d683::/32, 2605:6400::/32, 2605:6404::/32) + 4 ASNs (AS32167/AS955/AS53667/AS200373), all traced to saved source snapshots; Azure CLI app ID included as a labelled Microsoft public-client constant, not a campaign IOC. Endpoint EDR documented as N/A (cloud identity attack). Sourced from Huntress (primary), The Hacker News, SecurityWeek, Security Affairs (July 2026).
18

References

TierSourceUsed ForAccess Date
1Huntress — No (Bad) CAP: Inside an Ongoing LSHIY Password Spray AttackPrimary: counts, timeline, ROPC mechanics, CAP/MFA gaps, LSHIY/FranTech/3xK infra, userStrongAuthClientAuthNRequired2026-07-22
1The Hacker News — Azure CLI Password Spray Hits at Least 78 Accounts in 81M+ AttemptsCorroboration of counts, all ASNs + IPv6 ranges, ROPC /token mechanics, migration timeline2026-07-22
1SecurityWeek — Massive Password Spray Campaign Targeting Azure CLIAS32167/AS955 attribution; 155× credential-spray rise; combo-list targeting2026-07-22
2Security Affairs — Azure CLI Targeted in LSHIY Campaign Across 64 OrgsIndependent corroboration of LSHIY infra, ROPC method, mitigation2026-07-22
2MITRE ATT&CK T1110.003 — Brute Force: Password SprayingTechnique mapping (spray) + M1032/M1036/M1027 mitigations2026-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.