curl Use-After-Free漏洞导致任意写入的技术分析

本文详细分析了curl中一个Use-After-Free漏洞,该漏洞在特定条件下可导致任意写入,影响多个curl版本。文章包含漏洞复现步骤、代码分析、影响评估及修复讨论,涉及ASAN、堆管理和splay树操作等关键技术细节。

curl Use-After-Free漏洞(导致某些版本任意写入) | HackerOne

摘要

Use-After-Free漏洞导致任意写入/读取

是的,我使用了IA和mermaid编辑器(在线版)生成此图表,显示(分配、释放和释放后使用)的路径: bug_svg.png (F4637660): bug_svg.png

受影响版本

curl 8.13.0 (x86_64-pc-linux-gnu) libcurl/8.13.0 OpenSSL/3.5.0 zlib/1.3.1 brotli/1.1.0 zstd/1.5.5 libpsl/0.21.2(任意写入/读取)

asan_crash_log_curl_8.13.0-Arbitrary-READ.log (F4637640): asan_crash_log_curl_8.13.0-Arbitrary-READ.log asan_crash_log_curl_8.13.0-Arbitrary-WRITE.log (F4637641): asan_crash_log_curl_8.13.0-Arbitrary-WRITE.log

curl 8.14.0 (x86_64-pc-linux-gnu) libcurl/8.14.0 OpenSSL/3.5.0 zlib/1.3.1 brotli/1.1.0 zstd/1.5.5 libpsl/0.21.2(任意写入/读取)

asan_crash_log_curl_8.14.0-Arbitrary-READ.log (F4637642): asan_crash_log_curl_8.14.0-Arbitrary-READ.log asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log (F4637643): asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log

curl 8.15.0 (x86_64-pc-linux-gnu) libcurl/8.15.0 OpenSSL/3.5.0 zlib/1.3.1 brotli/1.1.0 zstd/1.5.5 libpsl/0.21.2(仅任意写入)

asan_crash_log_curl_8.15.0-Arbitrary-WRITE.log (F4637644): asan_crash_log_curl_8.15.0-Arbitrary-WRITE.log

=> 测试环境:

上述所有版本均在Linux kali 6.3.0-kali1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.3.7-1kali1上测试。 Ubuntu 24.04.1(仅测试了curl版本8.14.0) 我使用curl版本8.14.0调查此问题的根本原因,因此当我引用某些函数时,行号可能略有变化。

复现步骤

从github下载目标版本并解压:

1
wget https://github.com/curl/curl/releases/download/curl-8_14_0/curl-8.14.0.zip && unzip curl-8.14.0.zip && cd curl-8.14.0

构建并安装:

1
2
3
CFLAGS="-fsanitize=address,undefined -g -O0 -fno-omit-frame-pointer" ./configure --with-openssl
make all -j$(nproc) && sudo make install
curl --version
1
while true; do curl --url "c071993_domain22.com/image[0-10]tp.com/dir[0-10]/api/path[0-10]/api" -Zs --max-time 0.01; echo $?; done
1
2
3
4
5
6
7
28
28
28
134
=================================================================
==405371==ERROR: AddressSanitizer: heap-use-after-free on address 0x522000306eb0 at pc 0x7fb7ad920768 bp 0x7ffe25f15920 sp 0x7ffe25f15918
READ of size 16 at 0x522000306eb0 thread T0

请参考崩溃文件asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log (F4637643): asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log 如果想在崩溃时停止循环,请使用:

1
2
3
4
5
6
7
8
while true; do 
    crash_=$(ASAN_OPTIONS="abort_on_error=1" curl --url "c071993_domain22.com/image[0-10]tp.com/dir[0-10]/api/path[0-10]/api" -Zs --max-time 0.01 2>&1)
    error_code=$?; echo $error_code
    if [ "$error_code" -eq 134 ]; then
      echo "$crash_"
      break
    fi
done

也可以使用配置文件,编写配置文件(conf.txt):

1
2
3
4
5
6
// ---------------- Begin conf.txt -----------------------------
    --url "c071993_domain22.com/image[0-10]tp.com/dir[0-10]/api/path[0-10]/api"
    -Zs
    --max-time 0.01

// ----------------END conf.txt -----------------------------

然后执行:

1
while true; do ASAN_OPTIONS="abort_on_error=1" curl --config conf.txt;echo $? ;done

检测到崩溃时停止循环:

1
2
3
4
5
6
7
8
while true; do
    crash_=$(ASAN_OPTIONS="abort_on_error=1" curl --config conf.txt 2>&1)
    error_code=$?
    if [ "$error_code" -eq 134 ]; then
      echo "$crash_"
      break 
    fi
done

崩溃分析

对于崩溃分析,我主要关注ASAN输出以及手动代码审查,核心转储文件也很有帮助(无法上传,因为大小为830 MB)。

启用核心转储(基于Debian的系统):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1"
ulimit -c unlimited

--> 执行直到崩溃
while true; do      
        crash_=$(curl --url "c071993_domain22.com/image[0-10]tp.com/dir[0-10]/api/path[0-10]/api" -Zs --max-time 0.01 2>&1)
        error_code=$?; echo $error_code
        if [ "$error_code" -eq 134 ]; then
          echo "$crash_"
          break
        fi
    done

使用gdb运行核心转储文件:
>> gdb curl core.599869

主要传输循环

 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
while(!s->mcode && (s->still_running || s->more_transfers)) {

    /* If stopping prematurely (eg due to a --fail-early condition) then
       signal that any transfers in the multi should abort (via progress
       callback). */

    if(s->wrapitup) {
        if(!s->still_running)
            break;
        if(!s->wrapitup_processed) {
            struct per_transfer *per;
            for(per = transfers; per; per = per->next) {
                if(per->added)
                    per->abort = TRUE;
            }
            s->wrapitup_processed = TRUE;
        }
    }

    s->mcode = curl_multi_poll(s->multi, NULL, 0, 1000, NULL);
    if(!s->mcode)
        s->mcode = curl_multi_perform(s->multi, &s->still_running);
    if(!s->mcode)
        result = check_finished(s);
}

当curl以并行模式调用时,使用curl_multi_perform()函数。

如崩溃输出所示,当调用curl_easy_init()函数时,它使用Curl_open(&data)函数分配Curl_easy结构(data = calloc(1, sizeof(struct Curl_easy));),然后将其分配给传递给Curl_open()的&data。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CURLcode Curl_open(struct Curl_easy **curl)
{
    CURLcode result;
    struct Curl_easy *data;

    /* simple start-up: alloc the struct, init it with zeroes and return */
    data = calloc(1, sizeof(struct Curl_easy)); // 分配

    // 设置数据
    }
    else
        *curl = data;  
    return result;
}

传输完成后,在check_finished(s)中调用post_per_transfer()函数,该函数调用curl_easy_cleanup(),进而调用Curl_close(&data),该函数使用Curl_safefree()、Curl_freeset()等释放数据组件并空其指针,然后释放数据本身,从而释放整个struct Curl_easy数据,包括其state.timenode(struct Curl_tree timenode)。

1
2
3
4
5
6
7
8
9
CURLcode Curl_close(struct Curl_easy **datap) 
{
    struct Curl_easy *data;   

    // 释放Curl_easy数据组件

    free(data); // 数据已释放(struct Curl_easy *data),但未置空 Emmm
    return CURLE_OK;
}

在multi.c:3474,Curl_expire_clear()函数调用Curl_splayremove(multi->timetree, &data->state.timenode,&multi->timetree)`,传递相同的指针(&data->state.timenode)作为第二个参数,该指针已被释放。 在/splay.c:234,Curl_splayremove()函数进行指针操作(假设传递的removenode是树中的有效节点),这写入已释放的堆内存(释放后使用->任意写入)。

 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
int Curl_splayremove(struct Curl_tree *t,
                        struct Curl_tree *removenode, // 第二个参数 => &data->state.timenode
                        struct Curl_tree **newroot)
{

    static const struct curltime KEY_NOTUSED = {
        ~0, -1
    }; /* will *NEVER* appear */
    struct Curl_tree *x;

    if(!t)
        return 1;

    DEBUGASSERT(removenode);

    if(compare(KEY_NOTUSED, removenode->key) == 0) {
        /* Key set to NOTUSED means it is a subnode within a 'same' linked list
           and thus we can unlink it easily. */
        if(removenode->samen == removenode)
            /* A non-subnode should never be set to KEY_NOTUSED */
            return 3;
        removenode->samep->samen = removenode->samen; // splay.c:234 

        removenode->samen->samep = removenode->samep;

        /* Ensures that double-remove gets caught. */
        removenode->samen = removenode;

        *newroot = t; /* return the same root */
        return 0;
    }

说明

由于这些括号范围"[1-10]“纯粹是curl命令行(和配置文件)的功能(称为URL globbing),并且这些类型的URL不受libcurl API支持,因此当curl二进制文件或libcurl API解析括号范围URL(将范围扩展为数组)然后在并行模式(-Z)下与curl_multi_perform、curl_multi_poll等一起使用时,会出现此错误。 当使用–max-time选项设置为相对较低的值(0.01与[1-10];0.1与URL中的[1-100]范围也会触发错误)时,会触发崩溃。 我相信我们可以使用libcurl API重现此问题并到达易受攻击的路径,我尝试使用10-at-a-time.c,编辑URL数组以通过–libcurl选项解析,并添加其他选项(如CURLOPT_NOPROGRESS用于–silent和(easy_handle,CURLOPT_TIMEOUT_MS,10L)用于max-time 0.01),但它没有工作。因为curl命令中–max-time的行为应用于整个URL(带括号范围),因此需要更多工作。 为清晰起见,我尚未使用C代码(使用libcurl <curl/curl.h>)触发相同的崩溃。 此错误仅在启用Address Sanitizers时触发,并且不一定导致应用程序崩溃,未启用ASAN时,它仅损坏堆(curl版本8.15.0)并中止(curl版本8.14.0),这可能导致段错误,当运行更多几次迭代时可能崩溃应用程序,这可能使攻击完美,因为正常的curl操作没有明显指标(但调试相当具有挑战性,因为启用asan时无法使用gdb和strace) 此错误还可以通过其他功能触发,例如–append (-a)、–upload (-T)。 因此,我建议将此错误评为高严重性。 仍在尝试找出可靠的方法来利用此错误。

支持材料/参考

CWE-416: Use After Free: https://cwe.mitre.org/data/definitions/416.html CWE-123: Write-what-where Condition: https://cwe.mitre.org/data/definitions/123.html https://curl.se/libcurl/c/curl_multi_perform.html https://everything.curl.dev/cmdline/urls/globbing.html https://curl.se/libcurl/c/10-at-a-time.html https://www.youtube.com/watch?v=YV3jewkUJ54

影响

此错误的可靠利用允许控制服务器的攻击者在受害者系统上执行任意命令。 执行也可以通过一些注入技术实现。 如果与另一个漏洞链式利用,允许控制传递给curl/libcurl的URL,并劫持服务器响应以进行堆整形,则可能更具影响力。 堆风水(heap feng shui)和堆修饰(heap Grooming)技术可用于覆盖目标地址并在受害者机器上执行命令(仍在调查中)。

附件

6个附件 F4637640: asan_crash_log_curl_8.13.0-Arbitrary-READ.log F4637641: asan_crash_log_curl_8.13.0-Arbitrary-WRITE.log F4637642: asan_crash_log_curl_8.14.0-Arbitrary-READ.log F4637643: asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log F4637644: asan_crash_log_curl_8.15.0-Arbitrary-WRITE.log F4637660: bug_svg.png

活动时间线

letshack9707 向curl提交报告。10天前

dfandrich curl staff 发表评论。10天前 c071993_domain22.com是一个无效的DNS主机名,因为包含下划线。是否有公开服务器显示此内容?

jimfuller2024 curl staff 发表评论。10天前 能够在8.15.0上重现 完成 134 1500920ERROR: AddressSanitizer: heap-use-after-free on address 0x7dd814c566d8 at pc 0x7fb816edc32e bp 0x7ffc3ce20540 sp 0x7ffc3ce20538 WRITE of size 8 at 0x7dd814c566d8 thread T0 #0 0x7fb816edc32d in Curl_splayremove /home/jfuller/src/curl/lib/splay.c:234 #1 0x7fb816e425bb in Curl_expire_clear /home/jfuller/src/curl/lib/multi.c:3598 #2 0x7fb816e1c81c in init_completed /home/jfuller/src/curl/lib/multi.c:131 #3 0x7fb816e1d231 in mstate /home/jfuller/src/curl/lib/multi.c:189 #4 0x7fb816e35e7f in multi_runsingle /home/jfuller/src/curl/lib/multi.c:2645 #5 0x7fb816e382c3 in curl_multi_perform /home/jfuller/src/curl/lib/multi.c:2739 #6 0x000000467f54 in parallel_transfers /home/jfuller/src/curl/src/tool_operate.c:1921 #7 0x00000046a289 in run_all_transfers /home/jfuller/src/curl/src/tool_operate.c:2215 #8 0x00000046aef2 in operate /home/jfuller/src/curl/src/tool_operate.c:2357 #9 0x000000452f69 in main /home/jfuller/src/curl/src/tool_main.c:275 #10 0x7fb815a115f4 in libc_start_call_main (/lib64/libc.so.6+0x35f4) (BuildId: c4b06a608071b2c9852fae62ca3b69cdc22cd022) #11 0x7fb815a116a7 in libc_start_main@@GLIBC_2.34 (/lib64/libc.so.6+0x36a7) (BuildId: c4b06a608071b2c9852fae62ca3b69cdc22cd022) #12 0x000000400e44 in _start (/usr/local/bin/curl+0x400e44) (BuildId: 3154567858150bf133b755149bdffd2f554f7f69) 0x7dd814c566d8 is located 3544 bytes inside of 5392-byte region [0x7dd814c55900,0x7dd814c56e10) freed by thread T0 here: #0 0x7fb817ee5bcb in free.part.0 (/lib64/libasan.so.8+0xe5bcb) (BuildId: 7f1aa7e2e600e8c9d54ce6

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