一、前言

关于SLP几个漏洞的成因与利用,网上已经有彭博士非常精彩且详细的分享(vSphere 攻防技法分享),这里不会做过多的赘述,写这个的意义只是记录一下针对特定低版本ESXi场景下32位SLP的利用心路。网上的一些公开利用,在低版本情况下皆无法成功。

安全研究人员在对某个目标进行研究时,经常会遇到一个场景,研究人员在一个正在开发的功能中发现漏洞,但因此功能未完善,无法进一步利用的遗憾。

笔者遇到的正是类似于此类场景,不过不是正在开发的功能场景,而是一个漏洞的触发路径上存在另一个漏洞,两个漏洞的相互影响下,导致利用无法完成的悲伤。

二、SLP历史漏洞

以下是SLP的历年漏洞

CVE编号漏洞类型
CVE-2019-5544堆溢出
CVE-2020-3992UAF
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不是正常的构造,要想触发漏洞且进行正常利用,需要满足以下几个条件:

  1. Scope List Length需要大于等于0x100
  2. Scope 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 漏洞利用

此部分很大程度上参考彭博士的利用手法,但不完全相同

思路

  1. 泄露libc
  2. 布局出任意写
  3. 通过任意写覆写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不再是一个研究优先度高的攻击面。