Loading HuntDB...

Use After Free (that leads to arbitrary Write for some versions)

C
curl
Submitted None
Reported by letshack9707

Vulnerability Details

Technical details and impact analysis

Use After Free
## Summary: - Use-After-Free vulnerability that leads to arbitrary write/READ YES, I used IA along with mermaind editor (online one) to generate this graph that show these paths for (allocation, free and use after free) F4637660: bug_svg.png ## Affected version - 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 (Arbitrary write/READ) + F4637640: asan_crash_log_curl_8.13.0-Arbitrary-READ.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 (Arbitrary write/READ) + F4637642: asan_crash_log_curl_8.14.0-Arbitrary-READ.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 (only Arbitrary write ) + F4637644: asan_crash_log_curl_8.15.0-Arbitrary-WRITE.log => tested on : - All version listed above tested on Linux kali 6.3.0-kali1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.3.7-1kali1. - Ubuntu 24.04.1 (I only tested curl version 8.14.0 on it) - I used curl version 8.14.0 to inverstigate the root cause of this, so there might be slight changed in line numbers when I refer to some fucntions. ## Steps To Reproduce: 1. Download the last target version from github and unizp it: 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 2. Build and install: ``` CFLAGS="-fsanitize=address,undefined -g -O0 -fno-omit-frame-pointer" ./configure --with-openssl make all -j$(nproc) && sudo make install curl --version >> 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 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 ``` Please refer to the crash file F4637643: asan_crash_log_curl_8.14.0-Arbitrary-WRITE.log Use this is you want to stop the loop when the crash occurs: ``` 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 ``` We can also use config file , write a config file (conf.txt) : ``` // ---------------- 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 ----------------------------- ``` Then execute : ``` while true; do ASAN_OPTIONS="abort_on_error=1" curl --config conf.txt;echo $? ;done ``` To stop the loop when the crash detected : ``` 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 ``` ## Triaging the crash: For traiging the crash I mainly focused on ASAN output alongs with manual code review , Also the coredump file was helpful (couldn't upload it cuz it's 830 MB in size). - to enable it (debian based os) : ``` export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1" ulimit -c unlimited --> Execute until it crashs 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 Run the coredump file with gdb : Run coredump with gdb >> gdb curl core.599869 ``` - main transfer loop : ``` 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); } ``` - when curl called in parallel mode the function `curl_multi_perform()` is used. - As crash output shows, when `curl_easy_init()` function was called, it uses Curl_open(&data) function to allocate Curl_easy struct `(data = calloc(1, sizeof(struct Curl_easy));)` then assign it to `&data` that was passed to `Curl_open()`. ``` 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)); // ALLOCATION // setup data } else *curl = data; return result; } ``` - Once the transfer completed the `post_per_transfer()` function was called within `check_finished(s)`, which invokes `curl_easy_cleanup()`, which calls `Curl_close(&data)` and that frees the data components and null their pointers using Curl_safefree(), Curl_freeset(),etc, then frees data itself which deallocates the entire struct Curl_easy data including its state.timenode( struct Curl_tree timenode) . ``` CURLcode Curl_close(struct Curl_easy **datap) { struct Curl_easy *data; // frees Curl_easy data components free(data); // data freed ( struct Curl_easy *data ), But not nulled Emmm return CURLE_OK; } ``` ``` - At multi.c:3474, `Curl_expire_clear()` function calls Curl_splayremove(multi->timetree, &data->state.timenode,&multi->timetree)` passing the same pointer (&data->state.timenode) that was freed as second argument. - At /splay.c:234, the `Curl_splayremove()` function does pointers manipulation (assuming passed removenode is a valid node in the tree), which writes into freed heap memory (use-after-free -> arbitrary write). ``` int Curl_splayremove(struct Curl_tree *t, struct Curl_tree *removenode, // second argument => &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; } ``` ## Notes: - Since those bracket ranges "[1-10]" are purely a feature of the curl command line (and config file) - known by URL globbing - , and these types of urls aren't supported by libcurl APIs, this bug occurs when curl binary or libcurl API parses a bracket-range URL(expanding the range into an array) then uses them in parallel mode (-Z) with curl_multi_perform, curl_multi_poll,etc. - The crash is triggered when using the --max-time option set to a relatively low value (0.01 with [1-10]; 0.1 with [1-100] ranges in the URL also triggers the bug) - I believe that we can reprodoce this issue and reach the vulnerable path using libcurl APIs, I tried with 10-at-a-time.c with editing urls array to parsed one by --libcurl options and add other options (like CURLOPT_NOPROGRESS for --silent and (easy_handle,CURLOPT_TIMEOUT_MS,10L) for max-time 0.01 ) but it didn't work. cuz the behavior of --max-time in curl command applied on the entire URL (with bracket ranges), so it needs more work on this . - For clarity I still haven't triggered the the same crash with C code (using libcurl <curl/curl.h>). - This bug only triggerd with Address Sanitizers enabled, and doesn't necessarily lead to app's crash, without ASAN enabled it's only corrupts the heeap with some low memory addresses (curl version 8.15.0) and abort in (curl version 8.14.0) which lead to segfault that might crash the app when it runs for more several iterations, which could make the perfect attack as normal curl operation with no obvious indicators (but quite challaging to debug cause we can't use gdb and strace when asan enabled) - This bug also can be triggered with additional functionalities such as --append (-a), --upload (-T). - Therefor I suggest Severity High For this BUG. - Still trying to figure out a reliable way to exploit this . ## Supporting Material/References: - 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 ## Impact - The reliable exploit of this bug allow Attacker that controlled server to Execute arbitrary commands on victim's system. - The execution could also achieved using some injection techiniques. - it might be more impactful if it's chained with another vulnerabilities that allow both controling the url passed to curl/libcurl, and hijacking server responses for heap massaging. - Heap feng shui and heap Grooming techniques can be used to overwrite the target address and execute commands on victim's machine (still inverstigating)

Report Details

Additional information and metadata

State

Closed

Substate

Informative

Submitted

Weakness

Use After Free