-
影响面分析
-
成因分析
-
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
主要,干了一件事情,把长度大于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
起来,看他崩不崩。
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后,就,堆溢出了。
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 分析