Curl CVE-2023-38545 分析

渗透技巧 7个月前 admin
208 0 0

Curl CVE-2023-38545 分析

  • 影响面分析

  • 成因分析

    • a、初始化

    • b、CONNECT_SOCKS_INIT阶段

    • c、CONNECT_REQ_INIT阶段

    • d、溢出点

  • 可利用性分析

    • a、过滤0x20以下字符

    • b、0x80以上字符需要视情况


影响面分析

结论:触发场景有要求,利用难度大。

影响版本范围

curl/libcurl:7.69.0 到 8.3.0 版本

Ubuntu:23.04、22.04LTS、20.04LTS (只说系统自带的curl,不代表系统中其他包依赖,https://ubuntu.com/security/CVE-2023-38545)

RedHat:9、Jboss Core (https://access.redhat.com/security/cve/CVE-2023-38545)

触发条件

1、启用socks5代理解析域名,如:curl -x socks5h://、如:curl_setopt($ch,CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME); 即:在socks5服务端解析。
2、可控制要访问的URL

触发后影响

堆溢出,可能会导致一些内置了libcurl的服务crash、DOS。
目前来看,控EIP难度挺大。

利用限制

a、场景限制(见触发条件)
b、启用IDN下,Payload只能为ASCII
c、未启用IDN下,Payload需要大于0x20

0x01 补丁

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

Curl CVE-2023-38545 分析

主要,干了一件事情,把长度大于255的hostname,直接返回过长,而不是设置标志位。

这里的逻辑,应该是:域名最长不能超过255,所以,当传入的域名大于255的时候,要变成本地hostname来解析。(实际上,也很少有这么长的…

尽管CURL维护者一直遮遮掩掩,在讨论区也煞有其事地说让等到10.11发布新版本。但似乎9月30,Redhat就有补丁了:https://gitlab.com/redhat/centos-stream/rpms/curl/-/commit/0783247f07250043dceb74e426f16f9d46147163

0x02 PoC

网上主要有俩份,一种是跟着X上:John Hammond的分析:
设置代理访问一个302调转页面来触发(因为补丁里的test也是这么写的):

另一个PoC来自:
https://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a811d3
很简单,设置socks5h去curl一个超长hostname即可:
curl -vvv -x socks5h://localhost:9050 $(python3 -c "print(('A'*10000), end='')")

对着他,改了一个PHP的,用于测试远程传入URL的场景:

<?php
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $_POST["url"]);
    curl_setopt($ch,CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
    curl_setopt($ch, CURLOPT_PROXY, "127.0.0.1:1080");
    $o = curl_exec($ch);
    curl_close($ch);
?>

重点,其实就跟前面说的一样,设置socks5代理,并且设置CURLPROXY_SOCKS5_HOSTNAME,让走代理解析。php -S 0.0.0.0:8080起来,看他崩不崩。

Curl CVE-2023-38545 分析

0x03 成因分析

分析完后,感觉和作者,以及官方文档提到的不太一样,感觉并不是因为多线程操作、使用同一个变量导致走入非预期的分支(可能是我没读明白),而且我本地环境使用正常socks5服务器必现(有可能我socks5服务有问题?)。

最终,我理解的成因是:因为走socks有多个阶段,但代码中不同阶段对hostname解析的标识位(socks5_resolve_local)设置不同,最终导致某一个变量值设置成了非预期,进一步造成溢出。

分析如下:

a、初始化

设置代理发起请求,会走到lib/socks.c:Curl_SOCKS5中,在这里,会根据配置的代理类型,来设置局部变量socks5_resolve_local

bool socks5_resolve_local =
(conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;

这个时候,socks5_resolve_local被设置为FALSE。这段代码在整个函数的最开头。

b、CONNECT_SOCKS_INIT阶段

在上面设置完后,会通过switch case走到socks5的初始化阶段CONNECT_SOCKS_INIT,其中有如下一段代码:

/* RFC1928 chapter 5 specifies max 255 chars for domain name in packet */
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;
}

当,传入的hostname大于255字节时,因为域名最大长度255,所以,判定该传入的hostname应该是本地主机名,故将局部变量socks5_resolve_local设置为TRUE,来改为走本地解析,让继续走后面的socks5流程,这就出现问题了。

c、CONNECT_REQ_INIT阶段

经过上面两个阶段后,会再通过switch case来到CONNECT_REQ_INIT,需要注意的是,整个socks5过程并不是流式进行的,而是根据socks5服务器返回的数据,来进到不同的switch case中,也就意味着,走到CONNECT_REQ_INIT,的时候socks5_resolve_local存的是a步骤初始化的FALSE

d、溢出点

之后,就将hostname复制到socksreq中:

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

socksreq的定义如下:

#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];

/* CONNECT_SOCKS_SEND */
ssize_t outstanding; /* send this many bytes more */
unsigned char *outp; /* send from this pointer */
};

也就是说,socksreq总共预留了255字节,用来存储非本地解析的主机名。所以,hostname足够长的时候,就溢出了。

而,这个socksreq,其实是在整个curl会话初始阶段,就calloc的:

CURLcode Curl_open(struct Curl_easy **curl)
{
CURLcode result;
struct Curl_easy *data;

/* Very simple start-up: alloc the struct, init it with zeroes and return */
data = calloc(1, sizeof(struct Curl_easy));
if(!data) {
/* this is a very serious error */
DEBUGF(fprintf(stderr, "Error: calloc of Curl_easy failedn"));
return CURLE_OUT_OF_MEMORY;
}

所以,大小超过255后,就,堆溢出了。

Curl CVE-2023-38545 分析

0x04 可利用性分析

先不说能不能控EIP吧(主要是太久没弄过了…),看看哪些可以作为URL输入。所以,构造了php的那个场景来测试。
情况是:0x20以下字符会被过滤,0x80以上字符会视情况决定。
结论是:我觉得挺难的,投入产出比不高,但也不担保有神人,碰到了神仙场景…

a、过滤0x20以下字符

lib/escape.c:Curl_urldecode中,会对传入URL做检测:

...
if(((ctrl == REJECT_CTRL) && (in < 0x20)) ||
((ctrl == REJECT_ZERO) && (in == 0))) {
free(ns);
return CURLE_URL_MALFORMAT;
}
...

也就是说,不能出现小于0x20的字符。

b、0x80以上字符需要视情况

在curl编译的时候,可以通过configure来让curl支持国际化域名。简单说就是,比如是否支持访问中文域名这种。
如果启用了IDN
则传入的大于0x80的,会做idn对应的转换,如:

☁  curl 你好
curl: (6) Could not resolve host: xn--6qq79v

也就是说,如果开启了IDN,则更难布局、利用,因为会被转换。
看了系统自带、默认的curl,都是支持IDN的。(自己按照上面git上的PoC中的环境,编译的Curl,是没有IDN的。

☁ ./curl -vvv -x socks5h://localhost:1080 $(python3 -c "print(('xAA'*10000), end='')")
* IDN support not present, can't parse Unicode domains

另外,apt-get install php-curl安装php-curl中,似乎也是不支持IDN的:

cURL support => enabled
cURL Information => 7.74.0
Age => 7
Features
AsynchDNS => Yes
CharConv => No
Debug => No
GSS-Negotiate => No
IDN => No

启用idn需要在编译时,带上参数:

  --with-winidn=PATH      enable Windows native IDN
--without-winidn disable Windows native IDN
--with-libidn2=PATH Enable libidn2 usage
--without-libidn2 Disable libidn2 usage

附录

1、官方文档:https://curl.se/docs/CVE-2023-38545.html
2、作者的分析:https://daniel.haxx.se/blog/2023/10/11/how-i-made-a-heap-overflow-in-curl/
3、HackOne上的提交:https://hackerone.com/reports/2187833


原文始发于微信公众号(Purpleroc的札记):Curl CVE-2023-38545 分析

版权声明:admin 发表于 2023年10月12日 下午9:22。
转载请注明:Curl CVE-2023-38545 分析 | CTF导航

相关文章

暂无评论

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