一、前言
关于SLP几个漏洞的成因与利用,网上已经有彭博士非常精彩且详细的分享(vSphere 攻防技法分享),这里不会做过多的赘述,写这个的意义只是记录一下针对特定低版本ESXi场景下32位SLP的利用心路。网上的一些公开利用,在低版本情况下皆无法成功。
安全研究人员在对某个目标进行研究时,经常会遇到一个场景,研究人员在一个正在开发的功能中发现漏洞,但因此功能未完善,无法进一步利用的遗憾。
笔者遇到的正是类似于此类场景,不过不是正在开发的功能场景,而是一个漏洞的触发路径上存在另一个漏洞,两个漏洞的相互影响下,导致利用无法完成的悲伤。
二、SLP历史漏洞
以下是SLP的历年漏洞
CVE编号 | 漏洞类型 |
---|---|
CVE-2019-5544 | 堆溢出 |
CVE-2020-3992 | UAF |
CVE-2021-21974 | 堆溢出 |
CVE-2022-31699 | 堆溢出 |
三、CVE-2021-21974漏洞利用
笔者最初选择的是CVE-2021-21974。
该漏洞存在于SLP的“目录代理通告”,目录代理 (DA) 是可选的 SLP 代理,用于存储和维护服务代理 (SA) 发送的服务广告的缓存。
在SLPParseSrvUrl
函数中,会解析“目录代理通告”中的URL字段,如下面代码所示
/* allocate space for structure + srvurl + longest net family tag */
*parsedurl = (SLPParsedSrvUrl *)xmalloc(sizeof(SLPParsedSrvUrl)
+ SLP_NF_MAX_SIZE /* long enough for longest net family */
+ srvurllen + 1); /* +1 for null-terminator */
if (*parsedurl == 0)
return ENOMEM;
/* point to family tag buffer, copy url to buffer space */
(*parsedurl)->family = (char *)*parsedurl + sizeof(SLPParsedSrvUrl);
slider1 = slider2 = (*parsedurl)->family + SLP_NF_MAX_SIZE;
memcpy(slider1, srvurl, srvurllen);
/* find end and terminate url copy */
endptr = slider1 + srvurllen;
*endptr = 0;
/* parse out the service type */
(*parsedurl)->srvtype = slider1;
slider2 = strstr(slider1, "://");
if (slider2 == 0)
{
xfree(*parsedurl);
*parsedurl = 0;
return EINVAL; /* ill-formatted URL - missing "://" */
}
问题出现在 slider2 = strstr(slider1, "://")
3.1 漏洞分析
要分析这个问题,先从wireshark的抓包看起,下图是发生“目录代理通告”时捕获的数据包
重点关注Scope List Length
字段,该字段的大小被定义为uint16,且紧跟在URL字段后面。
带着这些知识,来分析slider2 = strstr(slider1, "://")
的问题所在。
slider2 = strstr(slider1, "://")
的本意是解析URL字段,获取://
后面的内容。
URL的正常构造示例:service:daytime://www.mtjones.com
此时假设一个情况,如果发包时URL字段不带有://
,且Scope List Length
字段的值小于0x100。slider2 = strstr(slider1, "://")
遍历完URL后遇到Scope List Length
的\x00,将\x00视为URL字段的终止符,strstr
函数结束,未找到目标字符串,返回空。
如果Scope List Length
字段的值大于0x100,strstr
函数就会搜寻到Scope List Length
字段甚至Scope List
字段,在箭头所指的地方产生越界写
v4 = calloc(1u, srvurllen + 29);
result = 12;
if ( !v4 )
return result;
v5 = strstr(srvurl, ":/");
v21 = v5;
if ( !v5 )
{
free(v4);
return 22;
}
v6 = &srvurl[srvurllen];
haystack = (char *)(v5 - srvurl);
memcpy((char *)v4 + 21, srvurl, v5 - srvurl); <---------
3.2 悲剧的诞生
在走完有漏洞的SLPParseSrvUrl
函数后,紧跟着的是下面代码所示的SLPNetResolveHostToAddr
,该函数大意是将"www.test.com“之类的转为IP,这个Host从上述的URL
字段中获取
if ( SLPParseSrvUrl(a1[43], (const char *)a1[44], &ptr) )
goto LABEL_3;
if ( SLPNetResolveHostToAddr(*((_DWORD *)ptr + 1), v14) )
{
free(ptr);
}
else
{
......
}
正常URL如下
示例:
URL为service:daytime://www.mtjones.com
则获取www.mtjones.com作为参数传入
很不幸,此时触发漏洞需要的URL不是正常的构造,要想触发漏洞且进行正常利用,需要满足以下几个条件:
Scope List Length
需要大于等于0x100Scope List
中"://"
出现的位置不能过于后面,不然会导致越界写入过多的内存,破坏下一个堆的结构
SLP的处理代码简化为:
v4 = calloc(1u, srvurllen + 0x1D);
v5 = strstr(srvurl, ":/");
memcpy((char *)v4 + 0x15, srvurl, v5 - srvurl);
示例:
srvurl = b'A' * 24
scope_list = b'B' * 13 + struct.pack('<H', size + flag) + b':/' + b'C' * 647
会产生0x38的堆块(32位下),0x15+24+13+2=0x3c,下一个位置刚好是下一个堆块的size位
这就构成了一个悖论,要想利用,Scope List Length
必须>=0x100,要想>=0x100,”://“后的数据要足够长,数据足够长又导致无法通过SLPNetResolveHostToAddr
,导致程序执行流转入下面所示的LABEL_3
中进行SLPBufferFree
(还有一种可能是申请一个足够长的域名,携带在”://“后进行解析,但是在实战中,有泄露信息的风险,这里不予考虑)
LABEL_3:
SLPMessageFree(a1);
SLPBufferFree(a2);
SLPDatabaseClose(v12, v10);
LABEL_4:
HIDWORD(result) = __readgsdword(0x14u) ^ v15;
LODWORD(result) = v2;
return result;
接着又在下面进行了一次SLPBufferFree
,构成了无法避免的,完美的Double Free
if ( v21[1] == 8 || v21[1] == 3 )
{
if ( !v5 )
goto LABEL_27;
v17 = v3;
v3 = 0;
v20 = v7;
SLPBufferFree(v17);
v7 = v20;
}
SLPMessageFree(v7);
在稍微高一点的版本中进行了内部修复,在进行第一次SLPBufferFree
后对指针进行了置零操作
四、CVE-2019-5544漏洞利用
换个洞,开始第二次利用尝试,这次选择CVE-2019-5544
4.1 漏洞分析
Service Registration报文
该报文旨在注册相应的服务
Service Request报文
该报文旨在定位服务并查询他们的信息
正常交互逻辑是,先向SLP发送Service Registration报文,在SLP中注册某类服务。后续如果有用户想要查询该服务信息,就向SLP发送Service Request报文进行查询。而漏洞就诞生在向SLP进行查询时候的处理。
SLP会重新定义返回的信息包大小,该大小由你发送Service Request报文的langtaglen字段决定
/* reallocate the result buffer */
result = SLPBufferRealloc(result, size);
if (result == 0)
{
errorcode = SLP_ERROR_INTERNAL_ERROR;
goto FINISHED;
}
接着根据Service Request报文中请求的服务在SLPDataBase中查找,看该服务是否已经被注册,如果被注册就取出、读取服务信息到返回包中。
但很不幸,读取服务信息这一操作的读取大小,是由注册时服务的urllen决定的,其没有校验服务的urllen大小与返回包大小之间的差异。
PutUINT16(&result->curpos, db->urlcount);
for (i = 0; i < db->urlcount; i++)
{
/* urlentry is the url from the db result */
urlentry = db->urlarray[i];
#ifdef ENABLE_SLPv1
if (urlentry->opaque == 0)
{
/* url-entry reserved */
*result->curpos++ = 0;
/* url-entry lifetime */
PutUINT16(&result->curpos, urlentry->lifetime);
/* url-entry urllen */
PutUINT16(&result->curpos, urlentry->urllen);
/* url-entry url */
memcpy(result->curpos, urlentry->url, urlentry->urllen);
result->curpos += urlentry->urllen;
/* url-entry auths */
*result->curpos++ = 0;
}
4.2 漏洞利用
此部分很大程度上参考彭博士的利用手法,但不完全相同
思路
- 泄露libc
- 布局出任意写
- 通过任意写覆写free_hook
清理内存碎片
发送大量SLP的Service Request报文清理碎片
泄露libc
和彭博士的手法一样,要注意的是,在布局SLP SendBuffer和RecvBuffer的同时,也要一并布局SLPSocket结构体,在目标堆被放入LargeBin时立马修改对应SLPSocket的状态,将其转变为STREAM_WRITE_FIRST
,读回glibc地址
#define SOCKET_PENDING_IO 100
#define SOCKET_LISTEN 0
#define SOCKET_CLOSE 1
#define DATAGRAM_UNICAST 2
#define DATAGRAM_MULTICAST 3
#define DATAGRAM_BROADCAST 4
#define STREAM_CONNECT_IDLE 5
#define STREAM_CONNECT_BLOCK 6 + SOCKET_PENDING_IO
#define STREAM_CONNECT_CLOSE 7 + SOCKET_PENDING_IO
#define STREAM_READ 8 + SOCKET_PENDING_IO
#define STREAM_READ_FIRST 9 + SOCKET_PENDING_IO
#define STREAM_WRITE 10 + SOCKET_PENDING_IO
#define STREAM_WRITE_FIRST 11 + SOCKET_PENDING_IO
#define STREAM_WRITE_WAIT 12 + SOCKET_PENDING_IO
在泄露libc时,涉及到修改SLPSocket,这是一个需要关注的点,修改操作破坏了SLPSocket链表的完整性,导致链表被断链,一些Socket连接无法被搜寻到,且无法产生新的Socket连接。
所以在任意写操作进行前,需要修复SLPSocket链表,使其恢复正常。
以下是SLPSocket链表结构、插入、使用代码
typedef struct _SLPList
{
SLPListItem * head;
SLPListItem * tail;
int count;
} SLPList;
typedef struct _SLPListItem
{
struct _SLPListItem * previous;
struct _SLPListItem * next;
} SLPListItem;
typedef struct _SLPDSocket
{
SLPListItem listitem;
......
}
SLPListItem * SLPListLinkHead(SLPList * list, SLPListItem * item)
{
item->previous = 0;
item->next = list->head;
if (list->head)
list->head->previous = item;
list->head = item;
if (list->tail == 0)
list->tail = item;
list->count = list->count + 1;
return item;
}
void SLPDOutgoingHandler(int * fdcount, SLPD_fdset * fdset)
{
SLPDSocket * sock;
sock = (SLPDSocket *) G_OutgoingSocketList.head;
......
}
SLPSocket链表插入是头插法,使用时从头部开始获取。
假设一共有30个SLPSocket连接,第十五号SLPSocket连接被修改,产生了断链,1号-14号连接就无法被搜寻到。很不幸的是,SLP连接的监听描述符是8,一旦断链,就无法再被寻找到,新连接也就无法接入。需要人为进行伪造,恢复功能,在设计利用时需要特别注意这一点。
布局任意写
CVE-2019-5544的任意写手法与彭博士介绍的手法稍有不同,该CVE的触发路径上有大量堆块的申请操作,其中包含一些大堆块的申请,要想再次保证目标堆块在unsorted bin中,需新创建大量连接进行布局。
彭博士的手法更倾向实战,构造一个永久性的任意写,能多次使用。但理论上不考虑其他因素的话,只复现,拿shell,进行一次任意写即可完成。故笔者转变思路,使用堆溢出修改RecvBuf的start、curpos、end指向目标内存
然后通过堆溢出修改该RecvBuf的SLPSocket,使其转为"STREAM_READ"状态,也就是等待用户的数据输入。此时用户往连接中发送payload即可在目标内存里修改。
最后通过SLP连接,构造一个带有shellcode的堆,将其释放即可完成利用。
五、总结
本文解析了CVE-2021-21974和CVE-2019-5544,并尝试在低版本ESXi上进行漏洞利用,利用本身并不复杂。在之后的版本中,VMware将ESXi中的SLP设置为默认关闭,使得SLP不再是一个研究优先度高的攻击面。