From 0a24c5594d7b4b9f52c6470888fb4bc00cb11782 Mon Sep 17 00:00:00 2001 From: imnyang Date: Mon, 26 May 2025 00:56:03 +0900 Subject: [PATCH] =?UTF-8?q?[Add]=20PKCE=20=EC=B2=B4=ED=81=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?,=20Playground=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++- packages/backend/src/controller/PKCECheck.ts | 29 +++--- playground/.gitignore | 34 ------- playground/PKCEDowngrade/.gitignore | 2 + playground/PKCEDowngrade/README.md | 11 +++ playground/{ => PKCEDowngrade}/bun.lock | 10 +-- playground/PKCEDowngrade/package.json | 12 +++ playground/PKCEDowngrade/src/index.ts | 94 ++++++++++++++++++++ playground/PKCEDowngrade/tsconfig.json | 7 ++ playground/README.md | 15 ---- playground/package.json | 10 --- playground/src/PKCEDowngradeExpress.js | 31 ------- playground/tsconfig.json | 29 ------ 13 files changed, 164 insertions(+), 140 deletions(-) delete mode 100644 playground/.gitignore create mode 100644 playground/PKCEDowngrade/.gitignore create mode 100644 playground/PKCEDowngrade/README.md rename playground/{ => PKCEDowngrade}/bun.lock (75%) create mode 100644 playground/PKCEDowngrade/package.json create mode 100644 playground/PKCEDowngrade/src/index.ts create mode 100644 playground/PKCEDowngrade/tsconfig.json delete mode 100644 playground/README.md delete mode 100644 playground/package.json delete mode 100644 playground/src/PKCEDowngradeExpress.js delete mode 100644 playground/tsconfig.json diff --git a/README.md b/README.md index 4cb6778..c5497cc 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -# caido-plugin-test \ No newline at end of file +# caido-plugin-test + +## To-Do +- [ ] PKCE 다운그래이드 https에서 작동 안하는 이슈 고치기 + +```log +2025-05-25T15:52:40.757475Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connection (29e74afd-9006-445e-88a9-3fc5d4796af9) +2025-05-25T15:52:40.757530Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connected for http://localhost:8787 (29e74afd-9006-445e-88a9-3fc5d4796af9) +2025-05-25T15:52:40.757562Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost/login (29e74afd-9006-445e-88a9-3fc5d4796af9) +2025-05-25T15:52:40.767186Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost:8787/login -> 302 361 (29e74afd-9006-445e-88a9-3fc5d4796af9) +2025-05-25T15:52:40.768696Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b) +2025-05-25T15:52:41.103596Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 -> 302 4927 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b) +2025-05-25T15:52:41.105944Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connection (34585a00-9f9f-4c72-b087-2e9e92418dad) +2025-05-25T15:52:41.105993Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connected for http://localhost:8787 (34585a00-9f9f-4c72-b087-2e9e92418dad) +2025-05-25T15:52:41.106023Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 (34585a00-9f9f-4c72-b087-2e9e92418dad) +2025-05-25T15:52:41.108270Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] Required PKCE parameters missing. Skipping. +2025-05-25T15:52:41.277387Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] No PKCE downgrade detected. +2025-05-25T15:52:41.686109Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost:8787/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 -> 200 1582 (34585a00-9f9f-4c72-b087-2e9e92418dad) +``` \ No newline at end of file diff --git a/packages/backend/src/controller/PKCECheck.ts b/packages/backend/src/controller/PKCECheck.ts index 3e41154..1d3525d 100644 --- a/packages/backend/src/controller/PKCECheck.ts +++ b/packages/backend/src/controller/PKCECheck.ts @@ -11,18 +11,20 @@ export class PKCECheck { } const query = req.getQuery(); - const requiredParams = ["client_id=", "response_type=code", "code_challenge=", "code_challenge_method="]; - if (!requiredParams.every(param => query.includes(param))) { + const searchParams = new URLSearchParams(query); + const requiredKeys = ["client_id", "response_type", "code_challenge", "code_challenge_method"]; + + if (!requiredKeys.every((key) => searchParams.has(key))) { sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping."); return false; } const url = req.getUrl(); - const isOpenID = query.includes("scope=openid") || query.includes("id_token"); - const methodMatch = query.match(/code_challenge_method=([^&]*)/); - const challengeMatch = query.match(/code_challenge=([^&]*)/); + const isOpenID = searchParams.get("scope")?.includes("openid") || url.includes("id_token"); + const methodVal = searchParams.get("code_challenge_method"); + const challengeVal = searchParams.get("code_challenge"); - if (!methodMatch || !challengeMatch) { + if (!methodVal || !challengeVal) { sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping."); await sdk.findings.create({ title: isOpenID @@ -35,7 +37,6 @@ export class PKCECheck { return false; } - const methodVal = decodeURIComponent(methodMatch[1]!); if (methodVal === "plain") { sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping."); await sdk.findings.create({ @@ -46,26 +47,24 @@ export class PKCECheck { request: req, reporter: "", }); - return false; } - const downgradedQuery = query - .replace(/code_challenge_method=[^&]*&?/, "") - .replace(/code_challenge=[^&]*&?/, "") - .replace(/[?&]$/, ""); - + // Remove PKCE parameters to simulate a downgraded request + searchParams.delete("code_challenge"); + searchParams.delete("code_challenge_method"); + const downgradedQuery = searchParams.toString(); const downgradedUrl = `${req.getUrl().split("://")[0]}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`; try { const [resOriginal, resDowngraded] = await Promise.all([ fetch(new FetchRequest(url, { method: "GET" })), - fetch(new FetchRequest(downgradedUrl, { method: "GET" })) + fetch(new FetchRequest(downgradedUrl, { method: "GET" })), ]); const [bodyOriginal, bodyDowngraded] = await Promise.all([ resOriginal.text(), - resDowngraded.text() + resDowngraded.text(), ]); const statusEqual = resOriginal.status === resDowngraded.status; diff --git a/playground/.gitignore b/playground/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/playground/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/playground/PKCEDowngrade/.gitignore b/playground/PKCEDowngrade/.gitignore new file mode 100644 index 0000000..506e4c3 --- /dev/null +++ b/playground/PKCEDowngrade/.gitignore @@ -0,0 +1,2 @@ +# deps +node_modules/ diff --git a/playground/PKCEDowngrade/README.md b/playground/PKCEDowngrade/README.md new file mode 100644 index 0000000..6dd13e7 --- /dev/null +++ b/playground/PKCEDowngrade/README.md @@ -0,0 +1,11 @@ +To install dependencies: +```sh +bun install +``` + +To run: +```sh +bun run dev +``` + +open http://localhost:3000 diff --git a/playground/bun.lock b/playground/PKCEDowngrade/bun.lock similarity index 75% rename from playground/bun.lock rename to playground/PKCEDowngrade/bun.lock index 0a70737..ec75146 100644 --- a/playground/bun.lock +++ b/playground/PKCEDowngrade/bun.lock @@ -2,13 +2,13 @@ "lockfileVersion": 1, "workspaces": { "": { - "name": "playground", + "name": "PKCEDowngrade", + "dependencies": { + "hono": "^4.7.10", + }, "devDependencies": { "@types/bun": "latest", }, - "peerDependencies": { - "typescript": "^5", - }, }, }, "packages": { @@ -18,7 +18,7 @@ "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], } diff --git a/playground/PKCEDowngrade/package.json b/playground/PKCEDowngrade/package.json new file mode 100644 index 0000000..00ae1aa --- /dev/null +++ b/playground/PKCEDowngrade/package.json @@ -0,0 +1,12 @@ +{ + "name": "PKCEDowngrade", + "scripts": { + "dev": "bun run --hot src/index.ts" + }, + "dependencies": { + "hono": "^4.7.10" + }, + "devDependencies": { + "@types/bun": "latest" + } +} \ No newline at end of file diff --git a/playground/PKCEDowngrade/src/index.ts b/playground/PKCEDowngrade/src/index.ts new file mode 100644 index 0000000..4c61f36 --- /dev/null +++ b/playground/PKCEDowngrade/src/index.ts @@ -0,0 +1,94 @@ +import { Hono } from 'hono' +import { randomBytes, createHash } from 'crypto' +import { Buffer } from 'buffer' + +const app = new Hono() + +// In-memory PKCE store (should use Redis or similar in production) +const pkceStore = new Map() + +const generateCodeVerifier = () => { + return randomBytes(32).toString('hex') +} + +const generateCodeChallenge = (verifier: string) => { + const hash = createHash('sha256').update(verifier).digest() + return hash.toString('base64url') +} + +// Step 1: Redirect to GitHub with PKCE +app.get('/login', (c) => { + const codeVerifier = generateCodeVerifier() + const codeChallenge = generateCodeChallenge(codeVerifier) + const state = randomBytes(8).toString('hex') + + pkceStore.set(state, codeVerifier) + + const params = new URLSearchParams({ + client_id: process.env.GITHUB_CLIENT_ID!, + redirect_uri: 'http://localhost:8787/callback', + scope: 'read:user', + state, + response_type: 'code', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }) + + return c.redirect(`https://github.com/login/oauth/authorize?${params}`) +}) + +// Step 2: GitHub redirects back here +app.get('/callback', async (c) => { + const url = new URL(c.req.url) + const code = url.searchParams.get('code') + const state = url.searchParams.get('state') + + if (!code || !state) { + return c.text('Missing code or state', 400) + } + + const codeVerifier = pkceStore.get(state) + if (!codeVerifier) { + return c.text('Invalid or expired state', 400) + } + + // Step 3: Exchange code + verifier for token + const tokenRes = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + code, + redirect_uri: 'http://localhost:8787/callback', + code_verifier: codeVerifier, + }), + }) + + const tokenData = await tokenRes.json() + if (!tokenData.access_token) { + return c.text('Failed to get access token', 500) + } + + // Step 4: Use token to fetch user profile + const userRes = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + 'User-Agent': 'hono-app', + }, + }) + + const user = await userRes.json() + return c.json({ + message: 'GitHub login successful!', + user, + }) +}) + +export default { + port: 8787, + fetch: app.fetch, +} \ No newline at end of file diff --git a/playground/PKCEDowngrade/tsconfig.json b/playground/PKCEDowngrade/tsconfig.json new file mode 100644 index 0000000..c442b33 --- /dev/null +++ b/playground/PKCEDowngrade/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} \ No newline at end of file diff --git a/playground/README.md b/playground/README.md deleted file mode 100644 index 4a3109f..0000000 --- a/playground/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# playground - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run -``` - -This project was created using `bun init` in bun v1.2.14. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/playground/package.json b/playground/package.json deleted file mode 100644 index 0bbbfb8..0000000 --- a/playground/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "playground", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/playground/src/PKCEDowngradeExpress.js b/playground/src/PKCEDowngradeExpress.js deleted file mode 100644 index 61cf737..0000000 --- a/playground/src/PKCEDowngradeExpress.js +++ /dev/null @@ -1,31 +0,0 @@ -const express = require("express"); -const app = express(); - -app.get("/auth", (req, res) => { - const { - client_id, - response_type, - code_challenge, - code_challenge_method, - scope - } = req.query; - - console.log("Incoming request:", req.query); - - if (!client_id || response_type !== "code") { - return res.status(400).send("Missing required parameters"); - } - - // Simulate issuing an authorization code - const code = "dummy-auth-code"; - - // Simulate PKCE check (normally you'd validate here) - // We deliberately allow the downgrade here to simulate the vulnerability - const responseBody = `Authorization successful. code=${code}`; - return res.status(200).send(responseBody); -}); - -const PORT = 5050; -app.listen(PORT, () => { - console.log(`Test PKCE server running on http://localhost:${PORT}`); -}); diff --git a/playground/tsconfig.json b/playground/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/playground/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -}