Прим. Wunder Fund: наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.
В первой части этой серии статей я рассказал о том, как организовать фаззинг Apache HTTP Server с привлечением кастомных мутаторов. Во втором материале я раскрыл вопрос создания перехватчиков ASAN, которые позволяют выявлять ошибки при использовании собственных реализаций пулов памяти.
Эта статья, третья и последняя, посвящена результатам моих исследований. Я расскажу тут об обнаруженных мной уязвимостях Apache.
Разыменование NULL в session_identity_decode
Эту ошибку можно вызвать, поместив в Cookie
пару ключ/значение, оба элемента которой равны 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
<?xml version="1.0" encoding="utf-8" ?>
<d:lockinfo xmlns:d="DAV:">
В следующем фрагменте кода можно видеть такую конструкцию:
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-движков. До новых встреч!