os.path.join(app.config['UPLOAD_FOLDER'], f.filename) allows for arbitrary file upload when f.filename is an absolute path.
unlike filechecker_plus, you can't now overwrite existing files such as /bin/file, so you have to identify a way to obtain RCE by uploading a file that does not previously exist on the filesystem
if you strace an execution of /bin/file, you will notice that it tries to open (like any other executable) the /etc/ld.so.preload file. Have a look with strace file -b <whatever> |& grep ENOENT -> access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
/etc/ld.so.preload is used to specify a list of shared libraries that are preloaded when any executable is run
at this point, you need to craft an .so that prints the flag, upload it to a random location on the fs, upload a /etc/ld.so.preload containing the path to your .so and execute file again so that the flag is returned
since files are deleted after being uploaded, you need to exploit a race condition. You should also ensure that file does a clean exit, otherwise subprocess.check_output will raise an exception
以前其實沒用過 strace,發現還滿好用的,簡單記錄一下用法:
strace file -b <whatever> |& grep ENOENT
strace file /etc/passwd 2>&1 | grep "No such file or directory"
簡單記一些自己有打的題目,沒有打的就不記了。
依照慣例先附上關鍵字:
filechecker 系列
mini
程式碼:
簡單來說就是你上傳一個檔案,server 儲存以後會用
/bin/file
去檢查,並且把輸出丟給render_template_string
。也就是說我們只要能控制輸出就能輕鬆 SSTI。當時我就直接跑去 file 的 Github 找測試,看有沒有可以用的,最後找到這個:https://github.com/file/file/blob/master/tests/escapevel.result
可以看到跑出來的結果包含一個 MIME type,這個 MIME type 存在於原始檔案中,所以修改一下就好了。
看到別人 writeup,發現其實這樣就好了,最簡單:
出來的結果會是:
plus
接著是加強版,程式碼跟剛剛差不多,唯一的差別只有結果不會丟到
render_template_string
,所以無法 SSTI。當初這題看了好一陣子,原本我猜這題會跟 file 怎麼運作有關,想說應該會跟他怎麼判斷類型(magic/libmagic)有關,然後想辦法把 flag 檔案當作輸入外加自己寫的判斷,就可以慢慢去 leak file content 之類的。
結果隊友發現這一段有洞:
Python 的這個行為滿有趣的,那就是
os.path.join
第二個參數如果是/
開頭,他做的事情就不是 join 了:因此可以不需要有
..
就把檔案上傳到任意地方,只要隨便寫個 C program 把/bin/file
蓋掉就好了。promax
這題把剛剛的漏洞修了一半,變成只要檔案存在就不讓你上傳,所以不能蓋掉,只能上傳新東西。
此時我們還是往上一題之前想的方向去找,看怎麼運用現有機制,結果另一個隊友說他有個可能是非預期的解法,就解掉了。
原理是可以上傳檔案到
/etc/ld.so.preload
,裡面內容放/tmp/a.so
之類的,然後再上傳另一個檔案到/tmp/a.so
,此時 binary 在執行前就會先載入裡面的程式碼。這邊記一下 DC 裡面 lavish 的詳細回答:
os.path.join(app.config['UPLOAD_FOLDER'], f.filename)
allows for arbitrary file upload when f.filename is an absolute path./bin/file
, so you have to identify a way to obtain RCE by uploading a file that does not previously exist on the filesystem/bin/file
, you will notice that it tries to open (like any other executable) the/etc/ld.so.preload
file. Have a look withstrace file -b <whatever> |& grep ENOENT
->access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
/etc/ld.so.preload
is used to specify a list of shared libraries that are preloaded when any executable is run/etc/ld.so.preload
containing the path to your .so and execute file again so that the flag is returned以前其實沒用過 strace,發現還滿好用的,簡單記錄一下用法:
strace file -b <whatever> |& grep ENOENT
strace file /etc/passwd 2>&1 | grep "No such file or directory"
可以看到呼叫了哪些 system call & 去動了哪些檔案
因為這個解法似乎跟 file 沒太大關係,導致我們一直認為是非預期,最後發現其實是預期解。
作者 writeup:https://github.com/L1aovo/my-ctf-challenges/tree/main/RCTF2022
PrettierOnline
先說結論,我滿喜歡這題的
主要程式碼在這邊:
簡單來說就是載入設定檔以後跑 prettier,這題你唯一能控制的就是這個設定檔。
然後
./fw.js
裡面是去 patchrequire
:既然是有關 prettier config,第一步當然是先看官方文件:https://prettier.io/docs/en/configuration.html
當時有點看到幾個點:
第二點例如說你的
.prettierrc
長這樣:跑 prettier 的時候就會出現:
Error: Cannot find module 'hello'
但因為 server 上也沒其他檔案可以控制,所以沒發現可以幹嘛,就繼續研究 prettier 到底做了什麼事情,花了一些時間開 debugger 去 trace,發現就算你丟的是 JSON,一樣是先走到
yaml.parse
去解析你的程式碼(沒什麼用的發現就是了)後來東看西看,發現外加想起來有 plugin 這種東西,就寫了這樣的設定檔:
出現錯誤訊息
Error: Cannot find module 'abc'
,代表 prettier 會去 require plugin 沒有錯。那我們要 require 什麼?此時我想到我們可以 require 唯一能控制的檔案:
.prettierrc
,也就是說如果.prettierrc
同時是設定檔又是 JS 就行了。幸好這在 yaml 裡面很容易:
plgusin:
在 JS 裡面是標籤,-
是減號,所以完全沒問題。做到這裡我就覺得這題滿有趣的,把 JS+yaml polyglot 這概念再加上 real world 的 prettier 當作範例。可以執行程式碼以後,就要看怎麼繞 require 的限制,我試過
import()
但沒作用,後來想了一下,既然都可以執行任意 JS,就隨便亂改一波就好了,像這樣:好讀版:
先把
RegExp.test
改掉,就可以 require 任意東西,接著再讓fs.writeFileSync
的時候內容會被換成 flag,最後就能拿到 flag 了。作者 writeup:https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2022/prettieronline
發現 require 根本不用繞,用
module.constructor._load('child_process')
其實就可以了,因為 require 裡面也是再去呼叫這個 _load 的方法:https://github.com/nodejs/node/blob/265ea1e74ef429f7c27f05ac4cc9136adf2e8d9b/lib/internal/modules/cjs/loader.js最後還有一個 Nu1L 的 payload 也很炫:
這個利用了我開頭講的輸出一個字串就會 require,背後也是先用 yaml parse 所以
#
後面是註解,然後路徑的部分用了/*
搭配第二行的*/
結合變成合法 JS,tql!最後附上其他有找到的 writeup: