From b64c8cc4e42e97e4441a6088d0592944ac380040 Mon Sep 17 00:00:00 2001 From: imnyang Date: Wed, 28 May 2025 23:28:31 +0900 Subject: [PATCH] =?UTF-8?q?[Add]=20PKCE=20=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/controller/PKCECheck.ts | 81 ++++++++++++++++---- playground/PKCEDowngrade/.env.example | 2 + 2 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 playground/PKCEDowngrade/.env.example diff --git a/packages/backend/src/controller/PKCECheck.ts b/packages/backend/src/controller/PKCECheck.ts index 1d3525d..8fc5671 100644 --- a/packages/backend/src/controller/PKCECheck.ts +++ b/packages/backend/src/controller/PKCECheck.ts @@ -1,6 +1,5 @@ import type { SDK } from "caido:plugin"; -import type { Request } from "caido:utils"; -import { fetch, Request as FetchRequest } from "caido:http"; +import { Body, RequestSpec, type Request } from "caido:utils"; export class PKCECheck { async test(sdk: SDK, req: Request): Promise { @@ -32,7 +31,7 @@ export class PKCECheck { : "[WARN] OAuth2 Flow PKCE Parameters Missing", description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`, request: req, - reporter: "", + reporter: "PKCE Checker", }); return false; } @@ -45,7 +44,7 @@ export class PKCECheck { : "[WARN] OAuth2 Flow PKCE Method is 'plain'", description: `PKCE method is set to 'plain' for ${url}. This may indicate a downgrade vulnerability.`, request: req, - reporter: "", + reporter: "PKCE Checker", }); return false; } @@ -54,23 +53,71 @@ export class PKCECheck { 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}`; + const scheme = req.getUrl().startsWith("https") ? "https" : "http"; + const downgradedUrl = `${scheme}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`; + + sdk.console.log(`${req.getHost()} Original URL: ` + url); + sdk.console.log(`${req.getHost()} Downgraded URL: ` + downgradedUrl); try { - const [resOriginal, resDowngraded] = await Promise.all([ - fetch(new FetchRequest(url, { method: "GET" })), - fetch(new FetchRequest(downgradedUrl, { method: "GET" })), - ]); + // Use Caido Replay SDK to replay the original request + const spec = new RequestSpec(downgradedUrl); + spec.setBody(req.getBody() as Body); + for (const [key, value] of Object.entries(req.getHeaders())) { + if (Array.isArray(value)) { + spec.setHeader(key, value.join(', ')); // or another suitable delimiter + } else { + spec.setHeader(key, value); + } + } + spec.setHost(req.getHost()); + spec.setMethod(req.getMethod()); + spec.setPath(req.getPath()); + spec.setQuery(downgradedQuery); + spec.setTls(req.getTls()); + spec.setPort(req.getPort()); - const [bodyOriginal, bodyDowngraded] = await Promise.all([ - resOriginal.text(), - resDowngraded.text(), - ]); + let sendDowngradedRequest = await sdk.requests.send(spec); + + if (sendDowngradedRequest.response) { + let domain = spec.getHost(); + let port = spec.getPort(); + let path = spec.getPath(); + let query = spec.getQuery(); + let id = sendDowngradedRequest.response.getId(); + let code = sendDowngradedRequest.response.getCode(); + sdk.console.log(`REQ ${id}: ${domain}:${port}${path}${query} received a status code of ${code}`); + } + + if (sendDowngradedRequest.response?.getCode() === 302) { + await sdk.findings.create({ + title: isOpenID + ? "[CRITICAL] OpenID Flow PKCE Downgrade Vulnerability" + : "[CRITICAL] OAuth2 Flow PKCE Downgrade Vulnerability", + description: `The request to ${url} is vulnerable to a PKCE downgrade attack. This may indicate a configuration error.`, + request: req, + reporter: "PKCE Checker", + }); + } + +/* + sdk.console.log(`${req.getHost()} Original Status: ` + resOriginal.status); + sdk.console.log(`${req.getHost()} Downgraded Status: ` + resDowngraded.status); + + sdk.console.log(`${req.getHost()} Original Headers: ` + JSON.stringify(resOriginal.headers)); + sdk.console.log(`${req.getHost()} Downgraded Headers: ` + JSON.stringify(resDowngraded.headers)); + + // Caido Dev Docs 기준으로, 리다이렉트된 URL은 Response 객체의 url 속성에 저장되어 있음 + const locationOriginal = resOriginal.url ?? ""; + const locationDowngraded = resDowngraded.url ?? ""; + + sdk.console.log(`${req.getHost()} Original Location: ` + locationOriginal); + sdk.console.log(`${req.getHost()} Downgraded Location: ` + locationDowngraded); const statusEqual = resOriginal.status === resDowngraded.status; - const codeInBoth = bodyOriginal.includes("code=") && bodyDowngraded.includes("code="); + const codeInRedirects = locationOriginal.includes("code=") && locationDowngraded.includes("code="); - if (statusEqual && codeInBoth) { + if (statusEqual && codeInRedirects) { const title = isOpenID ? "[CRITICAL] OpenID Flow PKCE Downgraded to Plaintext" : "[CRITICAL] OAuth2 Flow PKCE Downgraded to Plaintext"; @@ -80,13 +127,13 @@ export class PKCECheck { await sdk.findings.create({ title, - description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nReference: ${reference}`, + description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nRedirect contained code=.\n\nReference: ${reference}`, request: req, reporter: "", }); return true; - } + }*/ } catch (err) { sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`); } diff --git a/playground/PKCEDowngrade/.env.example b/playground/PKCEDowngrade/.env.example new file mode 100644 index 0000000..13f5f37 --- /dev/null +++ b/playground/PKCEDowngrade/.env.example @@ -0,0 +1,2 @@ +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET=