[漏洞分析] CVE-2023-38545 curl“史上最严重的漏洞“分析

18 篇文章 4 订阅
17 篇文章 11 订阅

[漏洞分析] CVE-2023-38545 curl"史上最严重的漏洞"分析

漏洞简介

漏洞编号: CVE-2023-38545

漏洞产品: (curl)[https://github.com/curl/curl]

影响范围: libcurl 7.69.0 - 8.3.0

利用条件: 需要在使用socks5代理的情况下,并且可以指定curl的url参数,约等于需要完全控制curl的参数

利用效果: 程序崩溃,拒绝服务

漏洞复现

环境搭建

参考:https://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a811d3

可以不用这个docker,直接下载对应版本的curl编译,并开启调试符号:

wget https://github.com/curl/curl/releases/download/curl-7_74_0/curl-7.74.0.tar.gz
tar -vxf curl-7.74.0
cd curl-7.74.0.tar.gz
./configure --with-openssl --enable-debug
make

这里我不想make install,可以直接使用编译好的curl进行测试:

  • 编译好的curl在curl-7.74.0/src/.libs/curl

  • libcurl在curl-7.74.0/lib/.libs/libcurl.so

漏洞复现

在没有make install的情况下,需要设置环境变量LD_LIBRARY_PATH让curl使用刚编译的libcurl

export LD_LIBRARY_PATH=/root/cve-2023-38545/curl-7.74.0/lib/.libs/

然后下载这个建议python socks5代理工具:https://github.com/MisterDaneel/pysoxy

直接运行即可:

python3 pysoxy.py

然后执行

./src/.libs/curl -vvv -x socks5h://localhost:9050 $(python3 -c "print(('A'*10000), end='')")

在这里插入图片描述

漏洞原理

补丁分析

在这里插入图片描述

根据不定信息,我们可以发现,漏洞的重点在于hostname_len,当hostname_len过长的时候,会导致memcpy 越界写堆溢出。

除此之外,在补丁的上半部分,判断hostname_len是否大于255:

  • 漏洞版本中,大于255并不会返回错误,而是将socks5_resolve_local 设置为true,根据名字我们可以看出该变量代表是否进行本地处理(在这里意味着本地解析域名)
  • 修复版本中,如果hostname_len大于255则会直接返回错误代码CURLPX_LONG_HOSTNAME

调用栈

调用栈如下,但并不是一次调用就结束的,在easy_transfer函数中,如果没有成功读取到消息,则会一直重复尝试。

在这里插入图片描述

代码分析

由于整个curl过程状态机还是非常复杂的,所以我们重点放在漏洞相关的SOCKS相关函数中

设置代理状态

parse_proxy 函数中设置了代理状态:

在这里插入图片描述

由于我们的运行参数./src/.libs/curl -vvv -x socks5h://localhost:9050 $(python3 -c "print(('A'*10000), end='')") 使用的是socks5h,所以这里设置的状态是CURLPROXY_SOCKS5_HOSTNAME 很重要,后面会遇到。

第一次执行Curl_SOCKS5

接下来直接看漏洞所在函数Curl_SOCKS5:

curl-7.74.0\lib\socks.c : 484

CURLproxycode Curl_SOCKS5(const char *proxy_user,
                          const char *proxy_password,
                          const char *hostname,
                          int remote_port,
                          int sockindex,
                          struct connectdata *conn,
                          bool *done)
{
  ··· ···
  //[1]这里我们的状态是CURLPROXY_SOCKS5_HOSTNAME 而不是CURLPROXY_SOCKS5,所以是false
  bool socks5_resolve_local = 
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE; 
  const size_t hostname_len = strlen(hostname);
  ··· ···

  if(!SOCKS_STATE(sx->state) && !*done)
    sxstate(conn, CONNECT_SOCKS_INIT); //[2]设置初始状态

  switch(sx->state) {
  case CONNECT_SOCKS_INIT: //[2]初始化逻辑
      ··· ···
    //[3]关键逻辑hostname_len 是10000 大于255
    if(!socks5_resolve_local && hostname_len > 255) { 
      infof(conn->data, "SOCKS5: server resolving disabled for hostnames of "
            "length > 255 [actual len=%zu]\n", hostname_len);
      socks5_resolve_local = TRUE; //[3]处理方式不是返回错误,而是将 socks5_resolve_local 设置为true
    }

    ··· ···
    sxstate(conn, CONNECT_SOCKS_READ);//[4]更新状态,按顺序更新状态机
    goto CONNECT_SOCKS_READ_INIT;
  CONNECT_SOCKS_READ_INIT:
  case CONNECT_SOCKS_READ_INIT:
    ··· ···
  case CONNECT_SOCKS_READ:
    ··· ···
    if(result && (CURLE_AGAIN != result)) {
      ···
    }
    ··· ···
    else if(actualread != sx->outstanding) {
      /* remain in reading state */
      sx->outstanding -= actualread;
      sx->outp += actualread;
      return CURLPX_OK; //[5]暂时返回ok
    }
  ··· ···
}

[1] 这里判断proxy的类型,由于我们使用的参数是socksh所以我们的proxy类型为CURLPROXY_SOCKS5_HOSTNAME,所以这里socks5_resolve_local的结果是false。

在这里插入图片描述

[2] 第一次执行这个函数的时候会设置初始状态机CONNECT_SOCKS_INIT,并在下面状态机处理逻辑中进入初始化逻辑

[3] 在初始化中会经历补丁的第一个点,判断hostname_len长度,我们执行的命令中,A*10000就是作为hostname存在。这里由于逻辑有误,导致hostname即便超过255也不会返回错误,而是将socks5_resolve_local改成true继续执行。

在这里插入图片描述

[4] 按照逻辑一步一步更新状态机CONNECT_SOCKS_READ,并根据状态机继续执行

[5] 返回OK,但整个请求并没有完成,当前SXSTATE状态机是CONNECT_SOCKS_READ

第二次执行Curl_SOCKS5

由于我们请求的域名肯定是不存在的,所以curl是没有完成任务的,所以在easy_transfer中还会继续重复刚刚的逻辑,也就是会第二次执行到Curl_SOCKS5函数:

curl-7.74.0\lib\socks.c : 484

CURLproxycode Curl_SOCKS5(const char *proxy_user,
                          const char *proxy_password,
                          const char *hostname,
                          int remote_port,
                          int sockindex,
                          struct connectdata *conn,
                          bool *done)
{
  //[1]缓冲区整个长只有600
  unsigned char *socksreq = &conn->cnnct.socksreq[0];
  //[2]这里我们的状态是CURLPROXY_SOCKS5_HOSTNAME 而不是CURLPROXY_SOCKS5,所以是false
  bool socks5_resolve_local = 
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE; 
  const size_t hostname_len = strlen(hostname);
  ··· ···
  switch(sx->state) {
  case CONNECT_SOCKS_READ: //[3]当前状态机是CONNECT_SOCKS_READ
    ··· ···
    if(result && (CURLE_AGAIN != result)) {
      ··· ···
    }
    ··· ···
    else if(socksreq[1] == 0) {
      /* DONE! No authentication needed. Send request. */
      sxstate(conn, CONNECT_REQ_INIT); //[3]在初始化的时候设置为1,第二次执会走到这里
      goto CONNECT_REQ_INIT;
    }
    ··· ···
 CONNECT_REQ_INIT:
  case CONNECT_REQ_INIT:
    //[4]由于上面给socks5_resolve_local设置为了false,所以跳过这里
    if(socks5_resolve_local) { 
      ··· ···
    }
    goto CONNECT_RESOLVE_REMOTE;// 跳转到CONNECT_RESOLVE_REMOTE
  ··· ···
  CONNECT_RESOLVE_REMOTE:
  case CONNECT_RESOLVE_REMOTE: 
    /* Authentication is complete, now specify destination to the proxy */
    len = 0;
    socksreq[len++] = 5; /* version (SOCKS5) */
    socksreq[len++] = 1; /* connect */
    socksreq[len++] = 0; /* must be zero */

    if(!socks5_resolve_local) {
      socksreq[len++] = 3; /* ATYP: domain name = 3 */
      socksreq[len++] = (char) hostname_len; /* one byte address length */
      memcpy(&socksreq[len], hostname, hostname_len); /* address w/o NULL */ //[5]溢出
      len += hostname_len;
      infof(data, "SOCKS5 connect to %s:%d (remotely resolved)\n",
            hostname, remote_port);
    }
  ··· ···
}    

[1] 溢出缓冲区的来源,socksreq只有600的长度,并且这个缓冲区在使用的时候,单个子缓冲区只有255最大值(因为是用char表示长度)

#define SOCKS_REQUEST_BUFSIZE 600  /* room for large user/pw (255 max each) */
struct connstate {
  enum connect_t state;
  unsigned char socksreq[SOCKS_REQUEST_BUFSIZE];
  ··· ···
};

struct connectdata {
  struct Curl_easy *data;
  struct connstate cnnct;
  ··· ···
}

[2] 跟第一次一样,socks5_resolve_local被初始化为false

[3] 上一次返回后状态机是CONNECT_SOCKS_READ,这一次从CONNECT_SOCKS_READ开始处理,跳过了初始化阶段的hostname长度判断,并将状态机更新为CONNECT_REQ_INIT

[4] 在CONNECT_REQ_INIT状态机处理中,由于socks5_resolve_local是false,则跳过了这一步直接到CONNECT_RESOLVE_REMOTE

[5] 直接向最大长度600的缓冲区拷贝了10000个A,造成堆溢出,程序崩溃。

在这里插入图片描述

修复后

在这里插入图片描述

补丁更新后,在第一次CONNECT_SOCKS_INIT状态机处理的时候,判断hostname长度大于255就会直接返回错误,程序就会异常退出。

在这里插入图片描述

总结

关于为什么要这么设计,可以参考开发人员的这篇博客,里面写了新路历程。curl的状态机挺复杂的,以复现这个漏洞为目的的话去研究状态机实数多余。这里仅关注漏洞触发的相关逻辑。

目前来看利用比较难,因为只是一次性的,不能持续交互,该结构体后面也没有什么函数指针。当前能达到的最佳效果就是拒绝服务,并且利用条件还比较苛刻,需要在使用SOCKS5的场景下可以指定curl的hostname,其实约等于需要攻击者完全控制curl的参数,所以不必太过惊慌。

参考

https://mp.weixin.qq.com/s/V0DoskAs05S1XhEFjgMkpw

https://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a811d3

https://github.com/curl/curl/commit/fb4415d8aee6c1045be932a34fe6107c2f5ed147

https://daniel.haxx.se/blog/2023/10/11/how-i-made-a-heap-overflow-in-curl/

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值