[Перевод] Фаззинг сокетов: Apache HTTP Server. Часть 3: результаты

Прим. Wunder Fund:  наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.

В первой части этой серии статей я рассказал о том, как организовать фаззинг Apache HTTP Server с привлечением кастомных мутаторов. Во втором материале я раскрыл вопрос создания перехватчиков ASAN, которые позволяют выявлять ошибки при использовании собственных реализаций пулов памяти.

72a314dad937d5ac81c4eaa21934f4a7.png

Эта статья, третья и последняя, посвящена результатам моих исследований. Я расскажу тут об обнаруженных мной уязвимостях Apache.

Разыменование NULL в session_identity_decode

Эту ошибку можно вызвать, поместив в Cookie пару ключ/значение, оба элемента которой равны NULL.

В качестве ключа и значения используется NULLВ качестве ключа и значения используется NULL

В этом примере можно заметить, что первую позицию в Cookie занимает ключ session и значение choko. Во второй позиции ключом является admin-user, а значением — число 2. А вот третья позиция представлена пустыми ключом и значением.

Что здесь за проблема? Если посмотреть на следующий фрагмент кода, там можно заметить два вызова apr_strtok, направленных на извлечение первой и второй строки (ключа и значения):

const char *psep = "=";
char *key = apr_strtok(pair, psep, &plast);
char *val = apr_strtok(NULL, psep, &plast);

А вот что происходит в функции apr_strtok в том случае, если первым её аргументом является NULL:

APR_DECLARE(char *) apr_strtok(char *str, const char *sep, char **last)
{
    char *token;

    if (!str)
        str = *last;

    while (*str && strchr(sep, *str))
        ++str;

Тут можно видеть, что в цикле while делается попытка разыменовать первый аргумент функции (указатель str). Если этот аргумент представлен значением NULL — это приведёт к ошибке разыменования NULL. Кроме того, именно это происходит в инструкции char *val = apr_strtok(NULL, psep, &plast);, когда предыдущий ключ тоже представлен NULL.

Воспользоваться этой ошибкой можно при включённом модуле mod_session. Эта уязвимость может привести к отказу в обслуживании на уровне дочернего потока процесса, повлияв на другие его потоки.

Ошибка неучтённой единицы (воздействующая на стек) в check_nonce

Для того чтобы воспользоваться этой ошибкой — нужно, чтобы был включён модуль mod_auth_digest, и чтобы приложение использовало бы метод аутентификации DIGEST.

Для вызова ошибки нужно назначить полю nonce специфический набор значений:

GET http://127.0.0.1/i?proxy=yes HTTP/1.1
Host: foo.example
Accept: /
Authorization: Digest username="2",
                     realm="private area",
                     nonce="d2hhdGFzdXJwcmlzZXhkeGR4ZHhkeGR4ZHhkeGR4ZHhkeGR4ZA==",
                     uri="http://127.0.0.1:80/i?proxy=yes",
                     qop=auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="53849ce65ba787cd0a07a272ece3bba6",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

Как видите, поле nonce содержит значение в кодировке BASE64. Для декодирования этого значения функция check_nonce выполняет следующий вызов:

apr_base64_decode_binary(nonce_time.arr, resp->nonce)

Здесь nonce_time.arr — это локальный массив размером 8 байтов. Посмотрим на код функции apr_base64_decode_binary:

APR_DECLARE(int) apr_base64_decode_binary(unsigned char *bufplain, const char *bufcoded)
{
	int nbytesdecoded;
	register const unsigned char *bufin;
	register unsigned char *bufout;
	register apr_size_t nprbytes;

	bufin = (const unsigned char *) bufcoded;
	while (pr2six[*(bufin++)] <= 63);
	nprbytes = (bufin - (const unsigned char *) bufcoded) - 1;
	nbytesdecoded = (((int)nprbytes +3) / 4) * 3;

bufout = (unsigned char *) bufplain;
    bufin = (const unsigned char *) bufcoded;

    while (nprbytes > 4) {
	*(bufout++) =
	    (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4);
	*(bufout++) =
	    (unsigned char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2);
	*(bufout++) =
	    (unsigned char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]);
	bufin += 4;
	nprbytes -= 4;
    }

    if (nprbytes > 1) {
	*(bufout++) =
	    (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4);
    }
    if (nprbytes > 2) {
	*(bufout++) =
	    (unsigned char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2);
    }
    if (nprbytes > 3) {
	*(bufout++) =
	    (unsigned char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]);
    }

В обычных обстоятельствах в переменной nprbytes окажется значение 11, а цикл while будет выполнен два раза, записав всего 8 байтов в массив bufplain (6 + 2). Но если дата имеет неправильный формат, при вычислении значения переменной nprbytes может получиться число 12. В результате в этих случаях цикл while будет выполнен три раза, в массив bufplain будет записано 9 байтов. Вследствие этого программа запишет 1 байт за пределами локального массива nonce_time.arr, перезаписав 1 байт стека программы (так и происходит ошибка неучтённой единицы).

Ошибка в cleanup_tables, связанная с использованием памяти после её освобождения 

Тут у нас имеется ошибка, связанная с использованием памяти после её освобождения (Use After Free, UAF) в функции cleanup_tables. Посмотрим на код этой функции:

static apr_status_t cleanup_tables(void *not_used)
{
    ap_log_error(APLOG_MARK, APLOG_INFO, 0, NULL, APLOGNO(01756)
                  "cleaning up shared memory");

    if (client_rmm) {
        apr_rmm_destroy(client_rmm);
        client_rmm = NULL;
    }

    if (client_shm) {
        apr_shm_destroy(client_shm);
        client_shm = NULL;
    }

Она вызывает функцию apr_rmm_destroy для освобождения блока памяти client_rmm. Но тут есть одна проблема: в определённых обстоятельствах этот блок памяти уже может быть освобождён функцией apr_allocator_destroy (её код тут не показан).

В результате программа пытается обратиться к недействительному адресу памяти. Это приводит к появлению уязвимости UAF. Тут важно заметить, что эта уязвимость может быть активирована лишь в режиме ONE_PROCESS.

Ошибка записи данных за пределами допустимого диапазона (воздействующая на кучу) в ap_escape_quotes

В данном случае перед нами ошибка, связанная с записью данных за пределами допустимого диапазона в куче, воздействующая на функцию ap_escape_quotes. Эта функция экранирует кавычки в предоставленной ей строке. Источник данной ошибки — несовпадение длины входной строки и буфера outstring, память под который «выделена» с помощью malloc.

В следующем фрагменте кода показано вычисление длины входной строки:

while (*inchr != '\0'){
	newlen++;
	if (*inchr == '"') {
		newlen++;
	}
	if ((*inchr == '\\') && (inchr[1] != '\0')) {
		inchr++;
		newlen++;
	}
	inchr++;
}
outstring = apr_palloc(p, newlen + 1);

А вот — вычисление размера outstring:

while (*inchr != '\0') {
        if ((*inchr == '\\') && (inchr[1] != '\0')) {
            *outchr++ = *inchr++;
            *outchr++ = *inchr++;
        }
        if (*inchr == '"') {
            *outchr++ = '\\';
        }
        if (*inchr != '\0') {
            *outchr++ = *inchr++;
        }
    }
    *outchr = '\0';
    return outstring;

Как видите, при вычислении размеров этих сущностей используется различная логика. В результате, если функции ap_escape_quotes предоставить особые входные данные, возникает возможность записи данных за пределами массива outchr.

Об этой ошибке, за несколько дней до того, как я её обнаружил, сообщили исследователи из проекта Google OSS-Fuzz.

Состояние гонок, ведущее к UAF

Теперь хочу рассказать кое о чём совершенно отличном от того, о чём уже рассказывал. В данном случае ошибка представлена состоянием гонок, которое ведёт к UAF и воздействует на Apache Core.

В ходе моих фаззинг-исследований я столкнулся с множеством невоспроизводимых UAF-падений программы. Глубже проанализировав ситуацию, я обнаружил нечто вроде состояния гонок между apr_allocator_destroy и allocator_alloc. Всё указывало на то, что эти функции в конкурентных сценариях могут не отличаться потокобезопасностью. Это может привести к повреждениям в некоторых узлах памяти и, иногда, к тому, что программа пытается освободить память, которая уже находится в пуле free. Этот баг чем-то cхож с багом ProFTPd, о котором я сообщал год назад (CVE-2020–9273).

Ниже представлен пример соответствующего стек-трейса ASAN:

==106820==ERROR: AddressSanitizer: heap-use-after-free on address 0x625000091100 at pc 0x7ffff7d2ff4d bp 0x7fffffffd800 sp 0x7fffffffd7f8
READ of size 8 at 0x625000091100 thread T0
    #0 0x7ffff7d2ff4c in apr_allocator_destroy /home/antonio/Downloads/httpd-trunk/srclib/apr/memory/unix/apr_pools.c:197:26
    #1 0x7ffff7d3306c in apr_pool_terminate /home/antonio/Downloads/httpd-trunk/srclib/apr/memory/unix/apr_pools.c:756:5
    #2 0x7ffff77aeba6 in __run_exit_handlers /build/glibc-5mDdLG/glibc-2.30/stdlib/exit.c:108:8
    #3 0x7ffff77aed5f in exit /build/glibc-5mDdLG/glibc-2.30/stdlib/exit.c:139:3
    #4 0x5b1ae8 in clean_child_exit /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:777:5
    #5 0x5b19a5 in child_main /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:2957:5
    #6 0x5afa7b in make_child /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:2981:9
    #7 0x5af005 in startup_children /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:3046:13
    #8 0x5a74c1 in event_run /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:3407:9
    #9 0x6212b1 in ap_run_mpm /home/antonio/Downloads/httpd-trunk/server/mpm_common.c:100:1
    #10 0x5e67e6 in main /home/antonio/Downloads/httpd-trunk/server/main.c:891:14
    #11 0x7ffff778c1e2 in __libc_start_main /build/glibc-5mDdLG/glibc-2.30/csu/../csu/libc-start.c:308:16
    #12 0x44da7d in _start ??:0:0

Эта проблема не нова. О похожих ошибках сообщал в 2018 году Ханно Бок (hanno). Тут можно найти его отчёты.

Небольшие ошибки

Я, занимаясь фаззингом, обнаружил ещё кое-какие небольшие баги. Об одном из них я сейчас расскажу. Это — переполнение целочисленной переменной в функции Session_Identity_Decode. Этот баг не относится к разряду опасных, но я полагаю, что интересно будет показать пример того, как легко его вызвать.

Мы отправляем WebDav-запрос LOCK, нацеленный на MOD_DAV, передавая очень большое значение Timeout (Second-41000000004100000000):

LOCK /dav/c HTTP/1.1
Host: 127.0.0.1
Timeout: Second-41000000004100000000
Content-Type: text/xml; charset="utf-8"
Content-Length: XXX
Authorization: Basic Mjoz

В следующем фрагменте кода можно видеть такую конструкцию:

return now + expires;

Здесь выполняется сложение двух 32-битных целочисленных значений, результат операции оказывается в переменной того же типа. Если складываемые значения достаточно велики — при возврате результата операции произойдёт переполнение.

while ((val = ap_getword_white(r->pool, &timeout))
	if (!strncmp(val, "Infinite", 8)) {
		return DAV_TIMEOUT_INFINITE;
	}

	if (!strncmp(val, "Second-", 7)) {
		val += 7;
		expires = atol(val);
		now = time(NULL);
		return now + expires;
	}
}

Так как этот баг вызывается при выполнении запроса LOCK — для того, чтобы он проявился, должен быть включён модуль MOD_DAV.

Итоги

Хотя безопасность Apache HTTP Server уже очень хорошо изучена, учитывая недавно обнаруженные уязвимости, связанные с обходом путей и раскрытием файлов (CVE-2021–41773 и CVE-2021–42013), ясно, что в этой программе ещё можно обнаружить новые критические уязвимости.

Проводя это исследование, я хотел сделать собственный вклад в улучшение безопасности Apache HTTP Server, и показать, что фаззинг можно применять для поиска уязвимостей в одном из самых популярных опенсорсных проектов современности. Я, в то же время, надеюсь, что у меня получилось поделиться знаниями, приобретёнными в ходе этой работы, с моими читателями.

Что дальше?

Эта статья закрывает цикл «Фаззинг сокетов». В следующем материале я собираюсь рассказать о фаззинге JavaScript-движков. До новых встреч!

О, а приходите к нам работать?

© Habrahabr.ru