Curl SOCKS5安全漏洞(CVE-2023-38545)分析

1

漏洞简介


漏洞编号: CVE-2023-38545

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

响范围: libcurl 7.69.0 – 8.3.0

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

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



2

漏洞复现


环境搭建

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.gztar -vxf curl-7.74.0cd curl-7.74.0.tar.gz./configure --with-openssl --enable-debugmake

这里不想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='')")
Curl SOCKS5安全漏洞(CVE-2023-38545)分析



3

漏洞原理


补丁分析

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

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

Curl SOCKS5安全漏洞(CVE-2023-38545)分析
  • 漏洞版本中,大于255并不会返回错误,而是将socks5_resolve_local 设置为true,根据名字我们可以看出该变量代表是否进行本地处理(在这里意味着本地解析域名)

  • 修复版本中,如果hostname_len大于255则会直接返回错误代码 CURLPX_LONG_HOSTNAME


调用栈

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

Curl SOCKS5安全漏洞(CVE-2023-38545)分析



代码分析

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


 设置代理状态 

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

Curl SOCKS5安全漏洞(CVE-2023-38545)分析

由于我们的运行参数  ./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.0libsocks.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。

Curl SOCKS5安全漏洞(CVE-2023-38545)分析

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

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

Curl SOCKS5安全漏洞(CVE-2023-38545)分析

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

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


 第二次执行Curl_SOCKS5 

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

curl-7.74.0libsocks.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,造成堆溢出,程序崩溃。

Curl SOCKS5安全漏洞(CVE-2023-38545)分析


 修复后 

Curl SOCKS5安全漏洞(CVE-2023-38545)分析

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

Curl SOCKS5安全漏洞(CVE-2023-38545)分析



4

总结


curl的状态机挺复杂的,以复现这个漏洞为目的的话去研究状态会耗时较长。这里仅关注漏洞触发的相关逻辑。

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



5

参考


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/


本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!


点这里Curl SOCKS5安全漏洞(CVE-2023-38545)分析关注我们,一键三连~

原文始发于微信公众号(华为安全应急响应中心):Curl SOCKS5安全漏洞(CVE-2023-38545)分析

版权声明:admin 发表于 2023年12月28日 下午6:11。
转载请注明:Curl SOCKS5安全漏洞(CVE-2023-38545)分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...