redirect uri misconfig

This commit is contained in:
caterpii 2025-05-31 11:49:57 +09:00
commit 8e33934951
21 changed files with 1019 additions and 6 deletions

View file

@ -7,4 +7,4 @@
"typecheck": "tsc --noEmit",
"build": "vite build"
}
}
}

View file

@ -0,0 +1,144 @@
import type { SDK } from "caido:plugin";
import { Body, RequestSpec, type Request } from "caido:utils";
export class PKCECheck {
async test(sdk: SDK, req: Request): Promise<boolean> {
const method = req.getMethod();
if (method !== "GET") {
sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping.");
return false;
}
const query = req.getQuery();
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 = searchParams.get("scope")?.includes("openid") || url.includes("id_token");
const methodVal = searchParams.get("code_challenge_method");
const challengeVal = searchParams.get("code_challenge");
if (!methodVal || !challengeVal) {
sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping.");
await sdk.findings.create({
title: isOpenID
? "[WARN] OpenID Flow PKCE Parameters Missing"
: "[WARN] OAuth2 Flow PKCE Parameters Missing",
description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`,
request: req,
reporter: "PKCE Checker",
});
return false;
}
if (methodVal === "plain") {
sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping.");
await sdk.findings.create({
title: isOpenID
? "[WARN] OpenID Flow PKCE Method is 'plain'"
: "[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: "PKCE Checker",
});
return false;
}
// Remove PKCE parameters to simulate a downgraded request
searchParams.delete("code_challenge");
searchParams.delete("code_challenge_method");
const downgradedQuery = searchParams.toString();
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 {
// 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());
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 codeInRedirects = locationOriginal.includes("code=") && locationDowngraded.includes("code=");
if (statusEqual && codeInRedirects) {
const title = isOpenID
? "[CRITICAL] OpenID Flow PKCE Downgraded to Plaintext"
: "[CRITICAL] OAuth2 Flow PKCE Downgraded to Plaintext";
const reference = isOpenID
? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7"
: "https://datatracker.ietf.org/doc/html/rfc7636";
await sdk.findings.create({
title,
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)}`);
}
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
return false;
}
}

View file

@ -0,0 +1,48 @@
import type { Request } from "caido:utils";
export class AuthZCodeGrantController {
constructor() {}
isAuthZReq(req: Request) {
// const path = req.getPath();
const query = req.getQuery();
// const raw = req.getRaw().toString();
// 쿼리에 client_id and response_type=code 포함 확인
if (query.includes("client_id=") && query.includes("response_type=code")) {
return true;
}
return false;
}
isSendCodeToClient(req: Request) {
const path = req.getPath();
const query = req.getQuery();
// code & state 쌍 또는 path에 &code= 또는 쿼리로 code= 유사 일치 확인
if (
(query.includes("code=") && query.includes("state=")) ||
path.includes("&code=") ||
/code=%/i.test(query)
) {
return true;
}
return false;
}
testReq(req: Request) {
if (this.isAuthZReq(req)) {
return "isAuthZReq";
}
if (this.isSendCodeToClient(req)) {
return "isSendCodeToClient";
}
return false;
}
// isAccessTokenReq(req: Response) {
// }
}

View file

@ -0,0 +1,40 @@
import type { Request } from "caido:utils";
export class ImplicitGrantController {
isImplicitGrantReq(req: Request) {
const query = req.getQuery();
// const raw = req.getRaw().toString();
// 쿼리에 client_id and response_type=token 포함 확인
if (query.includes("client_id=") && query.includes("response_type=token")) {
return true;
}
return false;
}
isSendTokenToClient(req: Request) {
const path = req.getPath();
const query = req.getQuery();
if (
(query.includes("access_token=") && query.includes("state=")) ||
path.includes("&access_token=") ||
/access_token=%/i.test(query)
) {
return true;
}
return false;
}
testReq(req: Request) {
if (this.isImplicitGrantReq(req)) {
return "isImplicitGrantReq";
}
if (this.isSendTokenToClient(req)) {
return "isSendTokenToClient";
}
return false;
}
}

View file

@ -0,0 +1,33 @@
import type { Request } from "caido:utils";
import { TokenLeakCheck } from "./tokenLeakCheck";
export class NonceCheckController{
/**
* OIDC(OpenID Connect)
*/
public static isOidcFlow(req: Request): boolean {
if(TokenLeakCheck.extractIdToken(req)) {
return true;
}
return false;
}
public static isNonceCheckRequest(req: Request): boolean {
const id_token = decodeIdToken(req);
// 1. nonce 파라미터가 포함된 요청인지 확인
if (id_token.includes("nonce=")) {
return true;
}
return false;
}
}
function decodeIdToken(req: Request): string {
// Implement actual decoding logic here. For now, return an empty string or mock value.
return "";
}

View file

@ -0,0 +1,42 @@
import type { Request } from "caido:utils";
import jwt from "jsonwebtoken";
export class TokenLeakCheck {
public static extractIdToken(req: Request): string | null {
// 1. Authorization 헤더 확인\\
const header = req.getHeaders() as Record<string, string | string[] | undefined>;
const authHeader = header["authorization"] || header["Authorization"];
if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
return authHeader.slice(7).trim();
}
// 2. 쿼리 파라미터
const query = req.getQuery();
if (query && typeof query === "object" && "id_token" in query) {
return (query as Record<string, any>).id_token;
}
// 3. POST 바디 안에 id_token이 있을 경우
const rawBody = req.getRaw();
const body = rawBody ? rawBody.toString() : "";
const match = body.match(/id_token=([^&\s]+)/);
if (match && typeof match[1] === "string") {
return decodeURIComponent(match[1]);
}
return null;
}
public static decodeIdToken(req: Request): Record<string, any> | null {
const token = this.extractIdToken(req);
if (!token) return null;
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded !== "object") return null;
return {
header: decoded.header,
payload: decoded.payload,
};
}
}

View file

@ -5,4 +5,4 @@
"lib": ["ESNext", "DOM"]
},
"include": ["./src/**/*.ts"]
}
}