CVE-2023-38545 Curl 堆溢出漏洞分析

admin 2024年2月23日07:35:08评论26 views字数 13477阅读44分55秒阅读模式

文章发表于:

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 curlautoreconf./configure --with-openssl --prefix=$HOME/code/c/curl-8.3.0/build --enable-debugmake -j 16make install


技术分析&调试

补丁

漏洞在fb4415d8aee6c1045be932a34fe6107c2f5ed147修复,修复代码如下CVE-2023-38545 Curl 堆溢出漏洞分析

从修复代码中可以看出两个区别

  • 当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=0x5595      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 = 0x1gef➤  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:274gef➤

在这状态下,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 helloCVE-2023-38545 Curl 堆溢出漏洞分析server helloCVE-2023-38545 Curl 堆溢出漏洞分析

正常情况下,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 堆溢出漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月23日07:35:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2023-38545 Curl 堆溢出漏洞分析https://cn-sec.com/archives/2499550.html

发表评论

匿名网友 填写信息