UNC5221 / VerdantBamboo — Brickstorm + Plenet + AgentPSD in Microsoft 365

A long-dwell Chinese espionage campaign (June 2026 reporting) that lived inside victim Microsoft 365 / Entra ID environments for roughly 18 months. UNC5221 plants its signature Brickstorm backdoor on internet-facing edge/appliance devices that don't run EDR, proxies through them to defeat Conditional Access, then deploys two newer families — Plenet (.NET cross-platform backdoor) and AgentPSD (Python reverse-shell backup) — and also compromises the victim's MSP for onward access. Defensive hunt & harden pack — no offensive code.
Threat
UNC5221 (VerdantBamboo) · Brickstorm / Plenet / AgentPSD
Severity
NATION-STATE · ACTIVE ITW
Type
Espionage · M365/cloud persistence · edge backdoor · MSP supply chain
Access
Edge-device 0-day → stolen creds → M365; ~18-month dwell
C2
Brickstorm WebSocket proxy; Plenet switchable C2; AgentPSD fallback domain
Version
v0.3 · 2026-06-29
Author
HuntPack
Confidence
High (multi-source vendor reporting, June 2026)
01

Executive Summary

UNC5221 (tracked as VerdantBamboo) is a Chinese espionage actor active since at least 2023, known for exploiting zero-days in internet-facing edge/appliance devices for initial access and then living in the cloud / Microsoft 365 for long-term collection. In the June 2026 campaign the actor maintained access to victim M365 / Entra ID tenants for roughly 18 months before detection, and additionally compromised the victim's managed service provider (MSP) to regain entry after partial remediation.

The hallmark tool is Brickstorm — a stealthy Go/Rust backdoor historically planted on devices that do not run EDR (SSL-VPNs, NAS, storage-sync appliances, retired email-archive servers). Brickstorm speaks WebSocket C2 and uses proxying to blend into legitimate traffic and to defeat Conditional Access from a trusted-looking source. Two newer families round out the arsenal: Plenet (a.k.a. Grimbolt), a cross-platform .NET backdoor with remote command execution, file management, interactive shell, and switchable C2 infrastructure; and AgentPSD, a lightweight Python reverse shell kept as a backup persistence mechanism on a separate domain.

Because the most durable access lives in M365/Entra audit telemetry and on EDR-less edge devices, this hunt deliberately balances native audit-log hunts (§7 — Entra/UAL anomalous OAuth consent, new service principals, mailbox/Graph collection, impossible travel, long-lived token reuse, MSP/delegated-admin anomalies) with a smaller set of endpoint CQL hunts (§8) for the parts that touch managed Windows/Linux hosts (Plenet .NET execution, AgentPSD python reverse shell, edge-adjacent egress, beaconing, persistence).

Defender priority: The crown-jewel persistence is in the cloud control plane, not on the endpoint. Hunt N1–N3 first (rogue OAuth app consent, new service principals / app registrations, mailbox & Graph collection) and N5 (MSP / delegated-admin anomalies) — these surface the 18-month dwell. Brickstorm on EDR-less edge devices is a documented detection GAP: you cannot rely on Falcon to see it — pivot from edge-adjacent host egress (Q3) and out-of-band appliance log review. Rotating credentials without evicting the cloud foothold and the MSP path will not remove this actor.

02

Source Review & Web Hunter Notes

TierSourceKey FindingCarry
1 · News / vendorBleepingComputer — Chinese APT new malware to keep accessBrickstorm (Go/Rust, WebSocket C2, proxy); Plenet/Grimbolt (.NET, switchable C2); AgentPSD (Python reverse shell, fallback domain); edge/NAS/MSP placement; CA evasion via proxyyes
1 · NewsIBTimes — hid inside M365 ~18 months with secret malware arsenal~18-month dwell inside M365; arsenal of three families; espionage motiveyes
2 · NewsAbijita — Brickstorm backdoor to access M365 environmentsBrickstorm as the access-maintenance tool into M365; edge-device originyes
2 · HistoricalUNC5221 / Brickstorm prior reporting (edge 0-days, 2023+)Pattern: edge-device 0-day → cloud/M365 espionage; Brickstorm on EDR-less appliancespartial

Decisions: cloud-first (the durable persistence is M365/Entra control-plane access, not on-host) — §7 native hunts carry the most weight. Atomic IOCs (hashes/domains) are sparse in public reporting and the actor rotates C2, so the pack anchors on behavior: anomalous OAuth/service-principal creation, long-lived token reuse, proxy-sourced auth, Plenet .NET / AgentPSD python execution, edge-adjacent egress. Brickstorm-on-edge is explicitly logged as a GAP (no EDR coverage) — handled via egress pivots and out-of-band appliance review rather than pretending endpoint telemetry exists.

03

Hunt Brief & Attack Chain

Working hypothesis: UNC5221 exploited an internet-facing edge/appliance device to plant Brickstorm, proxied through it to harvest and reuse credentials/tokens against M365, established cloud persistence via rogue OAuth apps / service principals, collected mail and data over Graph, dropped Plenet and AgentPSD on reachable hosts as backups, and pivoted to the MSP for resilient re-entry.

StepBehaviorTelemetryHunt Angle
1 · Initial access0-day on internet-facing edge/appliance (SSL-VPN, storage-sync, NAS)Appliance logs (out-of-band)N6; Q3 (edge-adjacent)
2 · Brickstorm implantGo/Rust backdoor on EDR-less device; WebSocket C2 + proxyNetflow; appliance logsQ3; N6 · GAP
3 · Cloud auth abuseProxied/stolen-cred logon to M365; CA evasionEntra sign-in; UALN1, N4
4 · Cloud persistenceRogue OAuth consent; new service principal / app registrationEntra audit; UALN1, N2
5 · CollectionMailbox / eDiscovery / Graph API mass read & exportUAL MailItemsAccessed; GraphN3
6 · Endpoint backupsPlenet (.NET) + AgentPSD (python reverse shell) on reachable hostsProcessRollup2Q1, Q2, Q5
7 · MSP pivotBSD Brickstorm on MSP infra; delegated-admin abuseUAL/partner; applianceN5; N6
8 · C2 / beaconWebSocket beaconing; Plenet C2 switching; AgentPSD fallback domainNetworkConnectIP4; DNSQ4
04

Consolidated IOC Table

Public atomic IOCs (hashes / C2 domains) are sparse and the actor rotates infrastructure — use the IOC-Management CSV in §10 with REPLACE_WITH_… placeholders and populate from the vendor GitHub IOC list during triage. The durable surface is the cloud-control-plane and execution behaviors below.
TypeValueConfActionContext
Behavior (cloud)Rogue OAuth app consent / new service principal granting Graph Mail.Read, full_access_as_apphighhuntCloud persistence — flagship
Behavior (cloud)Long-lived token reuse / proxied sign-in evading Conditional AccesshighhuntBrickstorm proxy auth
Behavior (cloud)Mass MailItemsAccessed / New-ComplianceSearch / Graph bulk exportmediumhuntCollection
MalwarePlenet / Grimbolt — cross-platform .NET backdoor (switchable C2)mediumhuntEndpoint backup
MalwareAgentPSD — Python reverse shell (fallback persistence, separate domain)mediumhuntEndpoint backup
MalwareBrickstorm — Go/Rust backdoor, WebSocket C2, proxy (edge/NAS/BSD/MSP)mediumhuntEdge — EDR-less GAP
NetworkWebSocket beaconing from edge-adjacent hosts; AgentPSD fallback C2 domainmediumhuntC2
IndicatorREPLACE_WITH_SHA256 / REPLACE_WITH_C2 (populate from vendor GitHub IOCs)lowenrichPerishable / rotating
05

Affected Surface & Telemetry Matrix

SurfaceRequired TelemetryPriorityGap Risk
M365 / Entra ID control planeUnified Audit Log + Entra sign-in/audit + Graph activityCriticalMedium — UAL/Graph audit must be ON & retained
Exchange Online mailboxesMailItemsAccessed, eDiscovery / ComplianceSearch auditCriticalMedium — mailbox auditing must be enabled
Internet-facing edge / appliancesAppliance/syslog + netflow (no EDR)HighGAP — Brickstorm runs where Falcon cannot
Managed Windows/Linux hostsProcess lineage + module + network (Falcon)HighLow — standard EDR telemetry
MSP / delegated admin (GDAP)Partner/UAL delegated-admin activityHighHigh — cross-tenant visibility often thin
Egress (C2 / WebSocket)Proxy / DNS / netflowMediumMedium — proxied / TLS-blended
06

ATT&CK Mapping

TacticTechniqueObserved BehaviorQuery / Control
Initial AccessT1190 — Exploit public-facing application0-day on internet-facing edge/applianceN6; Q3
Persistence (cloud)T1098.001 / T1136.003 — Additional cloud credentials / create cloud accountRogue OAuth app / new service principalN1, N2
Persistence (endpoint)T1543 / T1053 — Service / scheduled taskPlenet / AgentPSD persistence on hostsQ5
Defense EvasionT1550.001 / T1090 — App access token / proxyToken reuse + Brickstorm proxy evading CAN4, Q3
Credential AccessT1528 — Steal application access tokenLong-lived token harvest/reuseN4
CollectionT1114 / T1530 — Email & cloud data collectionMailItemsAccessed / Graph bulk exportN3
ExecutionT1059.006 / T1106 — Python / native APIAgentPSD python reverse shell; Plenet .NETQ1, Q2
Command & ControlT1071.001 / T1573 — Web (WebSocket) / encrypted C2Brickstorm WebSocket; Plenet switchable C2Q4
Initial Access (supply chain)T1199 — Trusted relationship (MSP)MSP / delegated-admin abuse for re-entryN5
07

Native Audit-Log Hunts (Entra ID / M365 UAL — KQL starters)

This is the highest-value section for this threat — the 18-month dwell lives in the cloud control plane. Run these as KQL in Microsoft Sentinel / Defender Advanced Hunting (or as UAL Search-UnifiedAuditLog queries). Confirm Unified Audit Log, mailbox auditing, and Graph activity logs are enabled and retained ≥ 1 year before trusting an absence of hits.

N1 · Anomalous OAuth app consent

Newly granted consent to apps requesting high-privilege Graph scopes (Mail.Read, Mail.ReadWrite, full_access_as_app, Directory.Read.All) — the classic cloud-persistence move.

AuditLogs
| where OperationName in ("Consent to application","Add app role assignment grant to user","Add delegated permission grant")
| extend props = TargetResources
| where tostring(props) has_any ("Mail.Read","Mail.ReadWrite","full_access_as_app","Directory.Read.All","Files.Read.All")
| project TimeGenerated, OperationName, InitiatedBy, TargetResources, Result

N2 · New service principal / app registration

Service principals or app registrations created then immediately granted credentials (secret/cert) — UNC5221 establishes app-only access that survives password resets.

AuditLogs
| where OperationName in ("Add service principal","Add application","Add application credentials",
    "Update application - Certificates and secrets management","Add service principal credentials")
| project TimeGenerated, OperationName, InitiatedBy, TargetResources, AdditionalDetails
| order by TimeGenerated asc

N3 · Mailbox / eDiscovery / Graph bulk collection

Mass mailbox reads, new compliance searches, or app-driven bulk Graph mail export — the espionage collection step.

OfficeActivity
| where Operation in ("MailItemsAccessed","New-ComplianceSearch","New-ComplianceSearchAction",
    "SearchCreated","Add-MailboxPermission")
| where ClientAppId != "" or AppId != ""
| summarize Mailboxes = dcount(MailboxOwnerUPN), Events = count() by ClientAppId, Operation, bin(TimeGenerated, 1h)
| where Mailboxes > 25 or Events > 500

N4 · Long-lived token reuse / impossible travel / proxied sign-in

Token reuse across distant geographies, or sign-ins from hosting/proxy ASNs that nonetheless satisfy Conditional Access (Brickstorm proxy blending). Flag refresh-token age and non-interactive sign-ins from new ASNs.

SigninLogs
| where ResultType == 0
| extend ASN = tostring(parse_json(tostring(AutonomousSystemNumber)))
| summarize Locations = make_set(Location), ASNs = make_set(ASN), Apps = make_set(AppDisplayName)
    by UserPrincipalName, bin(TimeGenerated, 1h)
| where array_length(Locations) > 1
| where ASNs has_any ("hosting","datacenter")  // tune to known proxy/VPS ASNs
Pair N4 with NonInteractiveUserSignInLogs to catch refresh-token reuse that never produces an interactive prompt — the actor's preferred long-dwell path.

N5 · MSP / delegated-admin (GDAP) access anomalies

Partner / delegated-admin actions from unusual times, ASNs, or against privileged objects — the MSP pivot used for resilient re-entry.

AuditLogs
| where InitiatedBy has "partner" or AdditionalDetails has "DelegatedAdmin" or LoggedByService == "Partner"
| where OperationName has_any ("Add member to role","Reset password","Update application","Add service principal")
| project TimeGenerated, OperationName, InitiatedBy, TargetResources, AdditionalDetails

N6 · Out-of-band edge / appliance review (no EDR)

Not a SIEM query — a runbook item. Pull SSL-VPN / NAS / storage-sync / email-archive appliance logs and netflow out of band; hunt for unexpected outbound WebSocket sessions, new listening services, and persistence on devices Falcon cannot see. This is the only reliable path to Brickstorm.

08

CrowdStrike LogScale CQL Hunt Queries

Pick your tenant's cloud first — every "Open in Falcon" button below uses this selection.
These cover the endpoint-observable parts only (Plenet/.NET, AgentPSD/python, edge-adjacent egress, beaconing, persistence). Brickstorm on EDR-less edge/appliances will NOT appear here — see §7 N6 and the coverage GAP in §15. Field names validated against the Falcon event reference; process hunts match SyntheticProcessRollup2.
Q1 · AgentPSD — python reverse shell spawning cmd/powershell
CONF HIGHFP MEDCOST LOW

Looks for: python.exe/python3 spawning a shell (cmd/powershell/bash) — the AgentPSD reverse-shell pattern. FP: dev tooling, build agents, and DevOps automation legitimately spawn shells from python — exclude known build/CI hosts and developer endpoints.

// HUNT: AgentPSD python reverse shell -> shell child
// MITRE: T1059.006, T1059.001 | CONF: high  FP: medium  COST: low
// REQUIRES: Falcon process telemetry (managed hosts)
// FALSE POSITIVES: dev/CI hosts, IT automation; exclude known build agents
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| ParentBaseFileName=/^python(3|\d\.\d+)?(\.exe)?$/i
| FileName=/^(cmd|powershell|pwsh|bash|sh)(\.exe)?$/i
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine], limit=200)
Q2 · Plenet — suspicious .NET backdoor execution
CONF MEDFP MEDCOST LOW

Looks for: a .NET assembly or dotnet.exe running from a user/temp/public path (Plenet is cross-platform .NET), or a non-standard binary loading the CLR from those paths. FP: some legitimate .NET apps run from AppData (installers/updaters) — flag user/temp/public + unsigned context and correlate to Q4 egress.

// HUNT: Plenet/.NET backdoor from user/temp/public path
// MITRE: T1059, T1106 | CONF: medium  FP: medium  COST: low
// REQUIRES: Falcon process telemetry
// FALSE POSITIVES: AppData updaters/installers; flag unsigned + correlate Q4
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| ImageFileName=/\\(Temp|AppData|ProgramData|Users\\Public)\\/i
| (FileName=/^dotnet(\.exe)?$/i or CommandLine=/(System\.Net\.WebSockets|ClientWebSocket|Assembly\.Load|\.dll["' ]+-)/i)
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, ImageFileName, FileName, CommandLine], limit=200)
Q3 · Suspicious outbound from edge-adjacent / appliance-proximate hosts
CONF MEDFP MEDCOST MED

Looks for: managed hosts in DMZ / edge subnets making unexpected outbound connections (proxy pivot from a Brickstorm-implanted appliance toward the cloud). FP: medium — you must scope to your real edge/DMZ subnets; baseline normal egress first. This is a proxy/pivot pivot, not direct Brickstorm visibility.

// HUNT: edge-adjacent host unexpected outbound (Brickstorm proxy pivot)
// MITRE: T1090, T1071.001 | CONF: medium  FP: medium  COST: med
// REQUIRES: Falcon network telemetry; edit DMZ/edge subnet regex below
// FALSE POSITIVES: legit edge mgmt traffic; baseline before alerting
#event_simpleName=NetworkConnectIP4
| LocalAddressIP4=/^10\.(0|1)\./
| in(RemotePort, values=[443, 80, 8443, 53])
| ContextImageFileName=/\\(Temp|AppData|ProgramData|Users\\Public)\\/i
| table([@timestamp, ComputerName, ContextImageFileName, LocalAddressIP4, RemoteAddressIP4, RemotePort], limit=200)
Q4 · C2 beaconing / Plenet C2 switching — periodic egress
CONF MEDFP MEDCOST MED

Looks for: a non-browser user/temp-path binary making repeated outbound connections to a small set of remote IPs (WebSocket beacon / Plenet C2 switching shows as a few rotating destinations). FP: medium — updaters and telemetry agents beacon too; exclude signed system/browser paths and known SaaS endpoints.

// HUNT: beaconing from user-path binary (WebSocket C2 / Plenet switching)
// MITRE: T1071.001, T1573 | CONF: medium  FP: medium  COST: med
// REQUIRES: Falcon network telemetry
// FALSE POSITIVES: updaters/telemetry agents; exclude signed system paths
#event_simpleName=NetworkConnectIP4
| ContextImageFileName=/\\(Temp|AppData|ProgramData|Users\\Public)\\/i
| ContextImageFileName!=/\\(chrome|msedge|firefox|brave|teams|onedrive)\.exe$/i
| groupBy([ComputerName, ContextImageFileName, RemoteAddressIP4], function=count(as=hits))
| hits >= 12
| table([ComputerName, ContextImageFileName, RemoteAddressIP4, hits], limit=200)
Q5 · Endpoint persistence — service / scheduled task from user path
CONF MED-HIGHFP LOWCOST LOW

Looks for: a newly registered service or scheduled task whose image lives in a user/temp/public path — Plenet/AgentPSD backup persistence. FP: low — few legitimate services run from those paths.

// HUNT: service/scheduled task persistence from user path
// MITRE: T1543, T1053 | CONF: med-high  FP: low  COST: low
// REQUIRES: Falcon service/scheduled-task telemetry
#event_simpleName=/CreateService|ModifyServiceBinary|ScheduledTaskRegistered/
| (ImageFileName=/\\(Temp|AppData|ProgramData|Users\\Public)\\/i
   or CommandLine=/\\(Temp|AppData|ProgramData|Users\\Public)\\/i
   or CommandLine=/(python|dotnet)/i)
| table([@timestamp, ComputerName, UserName, FileName, ImageFileName, CommandLine], limit=200)
Q6 · python.exe outbound network — AgentPSD egress
CONF MEDFP MEDCOST LOW

Looks for: python.exe making outbound connections (AgentPSD reverse shell calling home to its fallback domain). FP: medium — python services/agents make outbound calls; exclude known data-science/automation hosts and correlate to Q1.

// HUNT: python outbound (AgentPSD reverse-shell call-home)
// MITRE: T1059.006, T1071.001 | CONF: medium  FP: medium  COST: low
// REQUIRES: Falcon network telemetry; correlate to Q1 on same host
// FALSE POSITIVES: data-science / automation hosts; exclude & correlate Q1
#event_simpleName=NetworkConnectIP4
| ContextImageFileName=/\\python(3|\d\.\d+)?\.exe$/i
| RemotePort != 53
| table([@timestamp, ComputerName, ContextImageFileName, RemoteAddressIP4, RemotePort], limit=200)
09

CrowdStrike Custom IOA Recommendations

IOA NameField PatternsBenign ExclusionsDeployment Path
UNC5221 — python reverse shell (AgentPSD)Parent python(3).exe; Child cmd/powershell/bash/shCI/build agents; sanctioned IT automation (allow-list by host/grouping tag)Endpoint Security → Custom IOA → Process Creation, Detect
UNC5221 — service/task from user pathServiceInstalled / ScheduledTaskRegistered with image in Temp/AppData/ProgramData/Users\PublicVetted user-path agents (rare)Custom IOA → Process Creation, Detect+Block
UNC5221 — Plenet .NET from user pathdotnet.exe or CLR-loading binary from user/temp/public pathAppData updaters/installers (allow-list)Custom IOA → Process Creation, Detect
Endpoint IOAs only catch the backup families (Plenet/AgentPSD). The primary persistence is in M365 — there is no Falcon IOA for rogue OAuth/service-principal abuse; that is governed by Entra/Conditional-Access policy (see §12) and hunted in §7 (N1–N5).
10

Machine-Readable IOC Appendix

Grouped IOC Quick-Copy

Atomic IOCs are sparse/rotating — use placeholders and populate from the vendor GitHub IOC list during triage. The durable surface is the cloud-control-plane and execution behaviors.

Falcon IOC Management CSVbulk import
type,value,action,severity,expiration,description,tags
sha256,REPLACE_WITH_PLENET_SHA256,detect,critical,2026-12-17,Plenet .NET backdoor,campaign:UNC5221
sha256,REPLACE_WITH_AGENTPSD_SHA256,detect,high,2026-12-17,AgentPSD python reverse shell,campaign:UNC5221
sha256,REPLACE_WITH_BRICKSTORM_SHA256,detect,critical,2026-12-17,Brickstorm backdoor,campaign:UNC5221
domain,REPLACE_WITH_BRICKSTORM_C2,detect,high,2026-09-17,Brickstorm WebSocket C2,campaign:UNC5221
domain,REPLACE_WITH_AGENTPSD_FALLBACK_C2,detect,high,2026-09-17,AgentPSD fallback C2,campaign:UNC5221
ipv4,REPLACE_WITH_C2_IP,hunt,medium,2026-09-17,Rotating C2 host,campaign:UNC5221
Entra / M365 Hunt Checklistcloud control plane
# Run in Sentinel / Defender Advanced Hunting / Search-UnifiedAuditLog
N1 OAuth consent to apps requesting Mail.Read / full_access_as_app / Directory.Read.All
N2 New service principal or app registration + immediate secret/cert add
N3 Mass MailItemsAccessed / New-ComplianceSearch / app-driven Graph bulk export
N4 Token reuse across geographies; non-interactive sign-in from hosting/proxy ASN evading CA
N5 Partner / delegated-admin (GDAP) actions on privileged objects from unusual ASN/time
N6 (out-of-band) edge/NAS/SSL-VPN/email-archive appliance logs: outbound WebSocket, new services
# Verify UAL + mailbox auditing + Graph activity logs enabled and retained >= 1 year
Behavioral Signaturesdetection logic
Rogue OAuth app / new service principal granted high-privilege Graph scopes
Long-lived token / refresh-token reuse; proxied sign-in that satisfies Conditional Access
python.exe spawning cmd/powershell/bash (AgentPSD reverse shell)
dotnet/.NET assembly executing from Temp/AppData/ProgramData/Users\Public (Plenet)
Service or scheduled task registered with image in a user/temp/public path
Non-browser user-path binary beaconing to a small rotating set of remote IPs (WebSocket C2)
Edge/DMZ managed host making unexpected outbound (Brickstorm proxy pivot)
Named Tooling / ArsenalUNC5221 / VerdantBamboo
UNC5221 / VerdantBamboo  # Chinese espionage, active since 2023, edge-0day -> cloud/M365
Brickstorm   # Go/Rust backdoor, WebSocket C2 + proxy; EDR-less edge/NAS/BSD/MSP placement
Plenet / Grimbolt  # cross-platform .NET backdoor: shell, RCE, file mgmt, switchable C2
AgentPSD     # lightweight Python reverse shell; backup persistence, separate fallback domain
Access path: edge-device 0-day -> stolen creds/tokens -> M365 persistence -> MSP pivot
Dwell: ~18 months inside M365 before detection
Hardening / Conditional Access Auditdefense-in-depth
Restrict user OAuth consent; require admin consent for high-privilege Graph scopes
Audit ALL existing service principals / app registrations for unexpected credentials
Phishing-resistant MFA; revoke + reissue refresh tokens; cut token lifetimes
Conditional Access: block legacy auth; require compliant device / managed-network for admin
GDAP least-privilege for MSP; review + time-box delegated-admin; alert on partner actions
Patch + out-of-band review of internet-facing edge/appliances (no EDR) -> Brickstorm GAP
WDAC/app control: default-deny execution from Temp/AppData/Users\Public
11

Detection Validation Gates

GateCheckPass Criterion
Telemetry readyUAL + mailbox auditing + Graph activity + Entra sign-in logs ON and retained≥ 1 year retention; logs queryable in SIEM
Telemetry ready (endpoint)Falcon process + network + service/task telemetry on managed hostsCoverage gaps mapped; edge devices flagged as GAP
Benign baselineEnumerate sanctioned OAuth apps, service principals, CI/build hosts, AppData updatersAllow-lists built before promoting N1/N2/Q1/Q2
Positive testLab: benign python→cmd (Q1); benign user-path service (Q5); test consent grant (N1)Each query fires on the benign analog
PromotionPromote Q5 (low FP) + service-principal/consent alerting; keep Q1–Q4/Q6 as tuned huntsAlerts wired with documented exclusions
12

Hardening — Tiered & Deployable

The durable foothold is in the cloud. Prioritize evicting cloud persistence and closing the OAuth/token surface over endpoint controls.

Immediate — Evict cloud persistence
  • Audit & remove rogue OAuth apps and service principals; revoke their credentialsM1018 / M1047. The flagship eviction step.
  • Revoke and reissue all refresh tokens; shorten token lifetimesM1026. Kills long-lived token reuse (N4).
  • Restrict user consent; require admin approval for high-privilege Graph scopesM1018.
  • Phishing-resistant MFA for all admins; block legacy auth via Conditional AccessM1032 / M1036.
Near term — Close the access surface (pilot first)
  • GDAP least-privilege + time-boxed delegated admin for the MSP; alert on partner actionsM1018. Closes the MSP pivot (N5).
  • Conditional Access: require compliant device / managed network for privileged rolesM1035. Blunts proxy-blended sign-in.
  • WDAC/app control: default-deny execution from Temp/AppData/Users\PublicM1038. Blocks Plenet/AgentPSD staging.
  • Out-of-band patch & review of internet-facing edge/appliancesM1051. The only path at the Brickstorm GAP.
Strategic — Reduce the long-dwell surface
  • Continuous app-registration / service-principal governance & anomaly alertingM1047.
  • EDR/visibility for appliance-class devices where supported; segment edge from cloud-admin pathsM1030.
  • Mailbox + Graph data-access monitoring with bulk-export thresholdsM1047. Surfaces collection earlier than 18 months.
13

Deployable Playbooks

1 · Audit & revoke suspicious service principals (Microsoft Graph PowerShell)

Connect-MgGraph -Scopes "Application.Read.All","AuditLog.Read.All"
# List service principals with app-only credentials added recently
Get-MgServicePrincipal -All |
  Where-Object { $_.KeyCredentials.Count -gt 0 -or $_.PasswordCredentials.Count -gt 0 } |
  Select-Object DisplayName, AppId, Id |
  Format-Table
# After confirming a rogue SP, remove its credentials / disable it:
# Update-MgServicePrincipal -ServicePrincipalId <id> -AccountEnabled:$false

2 · Revoke refresh tokens for a compromised / suspect user

Connect-MgGraph -Scopes "User.ReadWrite.All"
Revoke-MgUserSignInSession -UserId user@contoso.example
# Forces re-auth and invalidates outstanding refresh tokens (kills token reuse, N4)

3 · Restrict user OAuth consent (admin consent workflow)

# Entra admin center -> Enterprise applications -> Consent and permissions
#   User consent settings: "Do not allow user consent"
#   Enable the admin consent request workflow for vetted approvals.
# Equivalent policy hardening blocks the rogue-consent persistence path (N1).

4 · Hunt + isolate an endpoint backdoor host (Falcon)

# Confirm with Q1/Q2/Q5/Q6, then network-contain via Falcon Host Management.
# Preserve memory + collect the user-path binary before remediation.
# Block C2 IPs/domains via IOC Management (see CSV in section 10).
14

Containment Runbook

PhaseActionsOwnerEvidence
IsolateNetwork-contain confirmed endpoint hosts; out-of-band isolate suspect edge/appliances; preserve UAL/Entra logs and memory before any resetSOC L2 / IRContainment timestamps; N1–N5 events; memory images
TriageMap the cloud foothold (rogue apps/SPs, token reuse), collection scope (mail/Graph export), endpoint backups (Plenet/AgentPSD), and MSP path; scope all tenants under shared GDAPIRSP/app inventory; sign-in timeline; export volumes
EradicateRemove rogue OAuth apps/SPs + their creds; revoke/reissue all refresh tokens; rotate admin + service creds; remove endpoint malware & persistence; close MSP pathIRRemoval log; token-revocation record; cred rotation
RecoverReimage backdoored hosts; rebuild/patch compromised edge appliances; verify clean before reconnecting; re-baseline CA policyIR + ITRebuild tickets; appliance firmware versions
HardenRestrict consent; admin-consent workflow; phishing-resistant MFA; GDAP least-privilege; WDAC user-path deny; promote endpoint IOAs; enable bulk-export alertingDetection EngPolicy diffs; IOAs enabled; alerts wired
⚠ Do not reset credentials before inventorying cloud persistence and the MSP path — UNC5221 re-enters via app-only tokens and delegated admin that survive password resets. Evict the control-plane foothold and the MSP pivot in the same window.

15

Detection Coverage Map

TechniqueBehaviorCQLNative / IOACoverage
T1098.001 / T1136.003Rogue OAuth / service principalN1, N2Good (cloud audit)
T1550.001 / T1528Token reuse / proxied auth (CA evasion)N4Partial needs ASN baseline
T1114 / T1530Mailbox / Graph bulk collectionN3Partial needs mailbox audit on
T1059.006AgentPSD python reverse shellQ1, Q6IOA-1Good on managed hosts
T1059 / T1106Plenet .NET executionQ2IOA-3Partial correlate Q4
T1543 / T1053Endpoint persistenceQ5IOA-2Good on managed hosts
T1071.001 / T1573WebSocket C2 / Plenet switchingQ4Partial beacon heuristic
T1199MSP / delegated-admin abuseN5Partial cross-tenant gap
T1190 / T1071.001Brickstorm on EDR-less edge deviceQ3 (pivot)N6 (out-of-band)GAP no EDR — appliance review only

Validation gates: confirm cloud audit + mailbox + Graph + Entra logs are ON and retained ≥ 1 year (otherwise N1–N5 silently under-report); baseline sanctioned OAuth apps, service principals, CI/build hosts and AppData updaters; lab-test benign analogs for Q1/Q5 and a test consent grant for N1; promote Q5 + consent/service-principal alerting and keep the rest as tuned hunts. The Brickstorm edge GAP cannot be closed with Falcon — it requires out-of-band appliance review (N6).

16

Hunt Summary Ticket

TITLE:        Hunt — UNC5221 / VerdantBamboo (Brickstorm + Plenet + AgentPSD in M365)
SEVERITY:     Critical (nation-state espionage; ~18-month M365 dwell; MSP supply-chain pivot)
SCOPE:        M365 / Entra control plane; Exchange Online; edge/appliances (no EDR); managed hosts; MSP/GDAP
HYPOTHESIS:   Edge 0-day -> Brickstorm on EDR-less appliance -> proxied/stolen-cred M365 access ->
              rogue OAuth/service-principal persistence -> mail/Graph collection ->
              Plenet (.NET) + AgentPSD (python) endpoint backups -> MSP pivot for re-entry.
QUERIES RUN:  Native N1 OAuth consent | N2 new service principal | N3 mailbox/Graph collection |
              N4 token reuse / proxied sign-in | N5 MSP/GDAP anomalies | N6 out-of-band appliance review
              CQL Q1 AgentPSD python->shell | Q2 Plenet .NET | Q3 edge-adjacent egress |
              Q4 beaconing/C2 switching | Q5 user-path persistence | Q6 python outbound
DO FIRST:     N1-N3 + N5 — the 18-month dwell lives in the cloud control plane, not the endpoint
FINDINGS:     <pending analyst execution>
GAPS:         Brickstorm on EDR-less edge devices (no Falcon visibility); cross-tenant MSP visibility;
              atomic IOCs sparse/rotating (populate from vendor GitHub IOC list)
ACTIONS:      Remove rogue apps/SPs; revoke+reissue refresh tokens; restrict consent; phishing-resistant MFA;
              GDAP least-privilege; WDAC user-path deny; promote Q5 + consent/SP alerting; patch edge appliances
OWNER:        HuntPack
VERSION:      v0.1 - 2026-06-17
17

Changelog

v0.32026-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.22026-06-29CQL syntax review (crowdstrike-logscale-v3): ImageFileName→ContextImageFileName on network events (Q3, Q4, Q6) and regex-on-numeric RemotePort fixes (Q3, Q6).
v0.12026-06-17Initial pack — UNC5221 / VerdantBamboo (Brickstorm + Plenet + AgentPSD in Microsoft 365). Built on the review-v2 gold layout (fixed left-sidebar scrollspy TOC, collapsible, cloud selector inside the CQL section, per-card Copy/Open-in-Falcon, Grouped IOC Quick-Copy grid, coverage map, .ticket). Cloud-first emphasis: 6 native Entra/UAL KQL hunts (N1–N6) for the ~18-month M365 dwell, plus 6 endpoint CQL hunts (Q1–Q6) for Plenet/.NET, AgentPSD/python, edge-adjacent egress, WebSocket beaconing, and user-path persistence. 3 custom IOA candidates. Brickstorm-on-EDR-less-edge documented as a coverage GAP (out-of-band appliance review only). Sourced from June 2026 vendor reporting (BleepingComputer, IBTimes, Abijita).
18

References

TierSourceUsed ForAccess Date
1BleepingComputer — Chinese APT deploys new malware to keep accessBrickstorm (Go/Rust, WebSocket, proxy), Plenet/Grimbolt (.NET), AgentPSD (python), edge/NAS/MSP placement, CA evasion2026-06-17
1IBTimes — Hid inside Microsoft 365 networks 18 months~18-month M365 dwell; secret malware arsenal; espionage motive2026-06-17
2Abijita — Brickstorm backdoor to access Microsoft 365Brickstorm as the M365 access-maintenance tool; edge-device origin2026-06-17
2MITRE ATT&CK T1098.001 — Additional Cloud CredentialsCloud-persistence technique mapping (rogue OAuth / service principal)2026-06-17

HuntPack v0.1 · UNC5221 / VerdantBamboo (Brickstorm + Plenet + AgentPSD in M365) · Generated 2026-06-17 · Defensive use only — no offensive code. The durable persistence is in the M365/Entra control plane and on EDR-less edge devices — hunt the cloud audit logs (N1–N5) first and review edge appliances out of band (N6). Validate field names before promoting any query.