Категория > Новости > Разборки на куче. Эксплуатируем хип уязвимого SOAP-сервера на Linux - «Новости»

Разборки на куче. Эксплуатируем хип уязвимого SOAP-сервера на Linux - «Новости»


12-03-2022, 00:00. Автор: Клара
Фай­лы задания

SOAP — это про­токол на осно­ве XML, который исполь­зует­ся для уда­лен­ного вызова про­цедур (Remote Procedure Call). Файл ns.wsdl (Web Services Description Language) опи­сыва­ет дос­туп к вызыва­емым про­цеду­рам. Наша задача — получить уда­лен­ное исполне­ние кода в SOAP-сер­висе gsoapNote.


Ав­торы тас­ка намека­ют, что gsoapNote запус­кает­ся на тач­ке с Linux, где заг­ружена биб­лиоте­ка libc-2.27.so. Поэто­му сра­зу зас­тавля­ем отладчик GDB заг­ружать кон­крет­но этот бинарь при стар­те сер­виса. В .gdbinit добавим


user@ubuntu:cat.gdbinit
...
set exec-wrapper env 'LD_PRELOAD=./libc-2.27.so'

Из­вле­чем из ns.wsdl информа­цию c помощью ути­литы SOAPUI.


SOAPUI авто­мати­чес­ки фор­миру­ет XML-шаб­лон зап­роса RPC. Мы сра­зу видим, на каком сетевом интерфей­се и пор­те стар­тует сер­вер — localhost:33263, а так­же имя RPC-метода — handleCommand(). SOAPUI ничего не зна­ет об аргу­мен­тах, поэто­му на их мес­те сто­ит знак воп­роса.


За­пус­каем gsoapNote и через SOAPUI отправ­ляем непол­ноцен­ный шаб­лон. Получа­ем осмыслен­ный ответ от сер­вера, закоди­рован­ный в Base64:


<resultCode>2resultCode>

На­вер­но, двой­ка — это оцен­ка нашего зап­роса, который не пон­равил­ся gsoapNote!


Взгля­нем на бинар­ные митига­ции.


Из пло­хих новос­тей — сте­ковые канарей­ки защища­ют gsoapNote от перепол­нения буфера на сте­ке (2 — Canary found), NX дела­ет некото­рые стра­ницы памяти неис­полня­емы­ми (3 — NX enabled).


Хо­роших новос­тей гораз­до боль­ше: стро­ка RELRO говорит о том, что мы можем перепи­сывать адре­са фун­кций из shared-биб­лиотек (1 — Partial RELRO) и эти адре­са не будут ран­домизи­ровать­ся (4 — No PIE), а это хорошее под­спорье для перех­вата управле­ния. Ну и самая хорошая новость — в бинар­нике есть сим­волы (5 — 817 Symbols), зна­чит, у нас будут хотя бы сиг­натуры фун­кций, а это очень облегчит реверс.


 

Реверс-инжиниринг


 

handleCommand


Не забыва­ем, что gsoapNote соб­ран с сим­волами, поэто­му сра­зу гру­зим его в «Иду» и пры­гаем к фун­кции handleCommand().



info


Очень удоб­ное рас­положе­ние окон в «Иде» я под­смот­рел у ребят с OALabs: окна с дизас­сем­блер­ным и деком­пилиро­ван­ным лис­тинга­ми раз­деля­ют ворк­спейс пополам и син­хро­низи­руют­ся меж­ду собой.



Де­ком­пилиро­ван­ный лис­тинг не то что­бы ужас­ный, но понять, что про­исхо­дит, слож­но. Цикл for, куча вло­жен­ных if, фун­кция executeCommand() при­нима­ет девять аргу­мен­тов...


Да­вай будем рас­смат­ривать лис­тинг как кар­тину импрес­сионис­тов — отой­дем на пару шагов назад и поищем общие пат­терны.


Во‑пер­вых, сра­зу в нес­коль­ких мес­тах видим, что если в if усло­вие не выпол­няет­ся, то локаль­ной перемен­ной v11 прис­ваивает­ся зна­чение 2 и про­пус­кает­ся куча кода. А двой­ка — это как раз тот result code, который при­шел в ответ на шаб­лон SOAPUI. Все схо­дит­ся.


Очень мно­го кода выпол­няет­ся внут­ри цик­ла for. Обра­тим наше вни­мание на него.


Пе­ред цик­лом вызыва­ется фун­кция xmlDocGetRootElement(), воз­вра­щен­ная струк­тура исполь­зует­ся в цик­ле for. Разыме­новы­вает­ся оффсет +24 для ини­циали­зации счет­чика и оффсет +48 для ите­рации.


Вмес­то иссле­дова­ния внут­реннос­тей фун­кции xmlDocGetRootElement() пос­мотрим при­меры исходно­го кода с ее исполь­зовани­ем. В этом нам поможет grep.app. Этот сайт покажет при­меры исходно­го кода качес­твен­ных репози­тори­ев с инте­ресу­ющей нас фун­кци­ей.


По ссыл­ке пры­гаем в репози­торий про­екта (я выб­рал lastpass) и вни­каем в исходный код:


// lastpass xml.c source code
...
#include <libxml/parser.h>
#include <libxml/tree.h>
...
xmlNode *root;
root = xmlDocGetRootElement(doc);
...

Ага... зна­чит, xmlDocGetRootElement() воз­вра­щает ука­затель на струк­туру типа xmlNode, а сама струк­тура опре­деле­на в libxml. Гуг­лим libxml и находим ус­трой­ство струк­туры xmlNode. Осо­бен­но нас инте­ресу­ют оффсе­ты, которые мы встре­тили в деком­пилен­ном лис­тинге.


Пе­рене­сем это зна­ние в «Иду». Соз­даем струк­туру xmlNode по тому же прин­ципу.


Разборки на куче. Эксплуатируем хип уязвимого SOAP-сервера на Linux - «Новости»


info


Не­обя­затель­но вос­ста­нав­ливать струк­туру пол­ностью один в один. Дела­ем это до нуж­ного нам оффсе­та +48 (+0x30).



Те­перь воз­вра­щаем­ся в деком­пилиро­ван­ный лис­тинг и прис­ваиваем локаль­ной перемен­ной v16 тип ука­зате­ля на xmlNode.


И все ста­ло гораз­до луч­ше! То, что про­исхо­дит в цик­ле for, теперь как на ладони! Наш gsoapNote пар­сит XML: дос­тает рутовую ноду, ини­циали­зиру­ет счет­чик его потом­ком xmlNode->children и обхо­дит сосед­ние ноды это­го потом­ка при помощи xmlNode->next. И strcmp() теперь обрел смысл: срав­нива­ется имя ноды curXmlNode->name с захар­дко­жен­ной стро­кой "array".


Об­рати вни­мание на фун­кцию parseArray(), она при­нима­ет текущую XML-ноду и занулен­ный двад­цатибай­товый мас­сив. Зануля­ется он, ско­рее все­го, потому, что в него будет записан резуль­тат пар­синга, не зря же фун­кция parseArray() име­ет такое наз­вание.


Еще обра­тим вни­мание на то, как код воз­вра­та parseArray() вли­яет на поток выпол­нения: если ноль, то вызыва­ется фун­кция с любопыт­ным наз­вани­ем executeCommand(), в про­тив­ном слу­чае обра­баты­ваем сле­дующую ноду.


Оп­ределен­но нам надо попасть в executeCommand(), но для это­го нуж­но рас­парсить мас­сив с нулевым кодом воз­вра­та.


 

parseArray


Фун­кция при­нима­ет два аргу­мен­та: a1 и a2. Мы уже выяс­нили, что пер­вый — ука­затель на xmlNode, а вто­рой — занулен­ный бай­товый мас­сив. Давай пос­мотрим, что про­исхо­дит с этим мас­сивом. Для это­го в деком­пилиро­ван­ном лис­тинге смот­рим кросс‑референ­сы на a2.


Сра­зу под­меча­ем оффсе­ты: +0, +8, +16, +24. Каж­дый сле­дующий оффсет боль­ше пре­дыду­щего на раз­мер QWORD (8 байт). Это не 0x20-бай­товый мас­сив, а мас­сив из четырех QWORD. К 85-й стро­ке вся струк­тура ини­циали­зиро­вана.


По оффсе­там +0 и +16 будут ука­зате­ли на стро­ки, по +8 и +24 будут int.


Пред­положим, что в эту струк­туру из четырех QWORD заносит­ся резуль­тат пар­синга. Назовем ее PARSE_RESULT.


Прис­воим аргу­мен­ту a1 тип xmlNode, а аргу­мен­ту a2 тип PARSE_RESULT и сно­ва взгля­нем на parseArray(). Внут­ри опять видим мно­го одно­образно­го кода пар­синга XML. Пос­коль­ку мы удач­но опре­дели­ли струк­туры вход­ных аргу­мен­тов, чита­ем деком­пилен­ный лис­тинг прак­тичес­ки как исходный код.


Ана­лизи­руя parseArray() даль­ше, мы получа­ем прак­тичес­ки целос­тную кар­тину того, что нуж­но вста­вить вмес­то мно­гоз­начитель­ного зна­ка воп­роса. Не буду утом­лять тебя даль­нейшим раз­бором, а прос­то покажу, как выг­лядит ожи­даемый XML.




info


Зна­чение ноды <number> дол­жно быть рав­но количес­тву нод внут­ри <array> минус один.



Пра­виль­ное опре­деле­ние исполь­зуемых струк­тур поз­волило дос­тать мно­го информа­ции из ста­тичес­кого ана­лиза gsoapNote. Давай про­дол­жать иссле­дова­ние в динами­ке. Отпра­вим XML на рисун­ке выше, пос­тавим брейк‑пой­нт на выходе из parseArray() (по адре­су 0x403BE2) и пос­мотрим, что лежит в PARSE_RESULT пос­ле пар­синга XML из зап­роса выше:



# Посмотрим, что лежит в PARSE_RESULT после выхода из parseArray
pwndbg> x/4gx $PARSE_RESULT
0x7ffffffd5a90: 0x00000000006562a0 0x0000000000001000
0x7ffffffd5aa0: 0x00000000006575b0 0x0000000000010000
pwndbg> x/s ((long*)$PARSE_RESULT)[0]
0x6562a0: "AAAAAA"
pwndbg> x/d ((long*)$PARSE_RESULT + 1)
0x7ffffffd5a98: 4096
pwndbg> x/s ((long*)$PARSE_RESULT)[2]
0x6575b0: "BBBBBB"
pwndbg> x/d ((long*)$PARSE_RESULT + 3)
0x7ffffffd5aa8: 65536
# Содержимое полностью соответствует XML
# А вот код возврата -1 подвел...
pwndbg> i r rax
rax 0xffffffff



С удо­воль­стви­ем наб­люда­ем, как кон­тро­лиру­емые нами дан­ные осе­дают в памяти. Все поля струк­туры PARSE_RESULT ини­циали­зиро­ваны, зна­чит, выпол­нился поч­ти весь код фун­кции parseArray(), но код воз­вра­та -1, а это не дает нам дви­гать­ся даль­ше...


Да­вай раз­бирать­ся. Мож­но, конеч­но, пошаго­во выпол­нить код в отладчи­ке, но это дол­го. Мы сде­лаем это быс­тро, одним выс­тре­лом — под­све­тим выпол­ненный код с помощью DynamoRIO.


DynamoRIO — это фрей­мворк для раз­работ­ки инс­тру­мен­тов динами­чес­кого ана­лиза. Нам понадо­бит­ся встро­енный в него инс­тру­мент drcov, который покажет нам все выпол­ненные инс­трук­ции.



info


Ес­ли вдруг захочешь решать эту проб­лему в отладчи­ке, то советую исполь­зовать кас­томную коман­ду для GDB step before. Вво­дишь в кон­соль sb и бря­каешь­ся перед инс­трук­цией call. Это уско­рит про­цесс отладки.



За­пус­каем gsoapNote под инс­тру­мен­таци­ей drcov сле­дующим обра­зом:


<path to DynamoRIO>/bin64/drrun -tdrcov gsoapNote

Сно­ва отсы­лаем XML через SOAPUI и завер­шаем про­цесс.


Наш drrun сге­нери­ровал файл drcov.gsoapNote.X.Y.proc.log, в котором содер­жатся адре­са выпол­ненных инс­трук­ций. Для прос­мотра это­го фай­ла будем исполь­зовать пла­гин lighthouse для IDA.



info


На момент написа­ния статьи пос­ледняя вер­сия DynamoRIO — 9.0, c drcov вер­сии 3, но lighthouse не может рас­парсить файл треть­ей вер­сии, поэто­му я исполь­зую вер­сию 8.0 с drcov вер­сии 2.



Зе­леным цве­том выделя­ются выпол­ненные инс­трук­ции. На скри­не выше при­веден момент выхода с кодом воз­вра­та -1. И про­изош­ло это потому, что зна­чение внут­ри <bulkString><number> не рав­но дли­не стро­ки <bulkString><content>. В SOAPUI кор­ректи­руем XML, про­лета­ем parseArray() и попада­ем в executeCommand(). Успех.




Перейти обратно к новости