172 行
6.3 KiB
JavaScript
172 行
6.3 KiB
JavaScript
import fs from "node:fs";
|
|
import http from "node:http";
|
|
|
|
const scenario = JSON.parse(fs.readFileSync(process.env.LAB_FIXTURE_SCENARIO, "utf8"));
|
|
const port = Number(process.env.PORT || 3000);
|
|
const state = {
|
|
seeded: false,
|
|
proof: false,
|
|
family: scenario.family,
|
|
system_id: scenario.system_id,
|
|
case_id: "",
|
|
detail: "fixture ready",
|
|
uploads: [],
|
|
sink_hits: 0,
|
|
payload: null,
|
|
events: []
|
|
};
|
|
|
|
function note(event, detail) {
|
|
state.events.push({ event, detail });
|
|
state.events = state.events.slice(-20);
|
|
}
|
|
|
|
function sendJson(res, statusCode, payload) {
|
|
const body = JSON.stringify(payload);
|
|
res.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
|
|
res.end(body);
|
|
}
|
|
|
|
function renderHtml() {
|
|
const proof = state.proof;
|
|
const banner = proof ? `<div class="proof">Proof active: ${state.detail}</div>` : `<div class="baseline">Baseline ready</div>`;
|
|
const xssBlock = proof && state.family === "xss"
|
|
? `<script>document.documentElement.setAttribute("data-xss-proof","true");document.title=${JSON.stringify(`${scenario.title} - proof`)};</script><div id="xss-proof">XSS marker executed for ${state.case_id}</div>`
|
|
: "";
|
|
const uploads = state.uploads.length ? `<section><h2>Uploads</h2><ul>${state.uploads.map((item) => `<li>${item.filename}</li>`).join("")}</ul></section>` : "";
|
|
const sink = state.sink_hits ? `<section id="ssrf-proof">Local sink hits: ${state.sink_hits}</section>` : "";
|
|
const admin = proof && ["proxy-boundary", "authz-bypass"].includes(state.family)
|
|
? `<section id="admin-proof">Admin boundary bypass confirmed.</section>`
|
|
: "";
|
|
const deserialize = proof && state.family === "deserialization"
|
|
? `<section id="deserialize-proof">Decoded marker: ${state.case_id}</section>`
|
|
: "";
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${scenario.title}${proof && state.family !== "xss" ? " - proof" : ""}</title>
|
|
<style>
|
|
body { font-family: sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 32px; }
|
|
main { max-width: 900px; margin: 0 auto; background: #111827; border: 1px solid #334155; border-radius: 16px; padding: 24px; }
|
|
.proof { padding: 14px; border-radius: 12px; background: #14532d; color: #dcfce7; }
|
|
.baseline { padding: 14px; border-radius: 12px; background: #1e3a8a; color: #dbeafe; }
|
|
code { background: rgba(255,255,255,0.08); padding: 2px 6px; border-radius: 6px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>${scenario.title}</h1>
|
|
<p>${scenario.subtitle}</p>
|
|
${banner}
|
|
<p>System: <code>${scenario.system_id}</code> / Family: <code>${scenario.family}</code></p>
|
|
${admin}
|
|
${xssBlock}
|
|
${uploads}
|
|
${sink}
|
|
${deserialize}
|
|
</main>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve) => {
|
|
const chunks = [];
|
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
req.on("end", () => {
|
|
try {
|
|
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
|
|
} catch (_error) {
|
|
resolve({});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function handleAttack(payload) {
|
|
const family = payload.family || state.family;
|
|
state.case_id = payload.case_id || state.case_id;
|
|
state.payload = payload;
|
|
state.proof = true;
|
|
if (family === "proxy-boundary") {
|
|
state.detail = "trusted forwarded headers crossed the boundary";
|
|
} else if (family === "authz-bypass") {
|
|
state.detail = "server-side authorization recheck was bypassed";
|
|
} else if (family === "ssrf") {
|
|
await fetch(`http://127.0.0.1:${port}/sink?case_id=${encodeURIComponent(state.case_id)}`);
|
|
state.detail = "server-side callback reached the local sink";
|
|
} else if (family === "xss") {
|
|
state.detail = "stored payload rendered inside the browser proof page";
|
|
} else if (family === "file-upload") {
|
|
state.uploads.push({ filename: payload.filename || `${state.case_id}.txt`, content: payload.content || "" });
|
|
state.detail = "upload marker accepted and listed";
|
|
} else if (family === "deserialization") {
|
|
state.detail = "unsafe object graph decoded without gadget execution";
|
|
}
|
|
note("attack", state.detail);
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
sendJson(res, 200, { ok: true, system_id: scenario.system_id, family: scenario.family });
|
|
return;
|
|
}
|
|
if (req.method === "GET" && url.pathname === "/") {
|
|
const body = renderHtml();
|
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(body) });
|
|
res.end(body);
|
|
return;
|
|
}
|
|
if (req.method === "GET" && url.pathname === "/admin") {
|
|
if (state.proof && ["proxy-boundary", "authz-bypass"].includes(state.family)) {
|
|
sendJson(res, 200, { ok: true, detail: state.detail, case_id: state.case_id });
|
|
} else {
|
|
sendJson(res, 403, { ok: false, detail: "admin boundary still enforced" });
|
|
}
|
|
return;
|
|
}
|
|
if (req.method === "GET" && url.pathname === "/sink") {
|
|
state.sink_hits += 1;
|
|
note("sink-hit", url.searchParams.toString() || "local callback");
|
|
sendJson(res, 200, { ok: true, sink_hits: state.sink_hits });
|
|
return;
|
|
}
|
|
if (req.method === "GET" && url.pathname === "/proof") {
|
|
sendJson(res, 200, {
|
|
success: Boolean(state.proof),
|
|
detail: state.detail,
|
|
case_id: state.case_id,
|
|
sink_hits: state.sink_hits,
|
|
uploads: state.uploads,
|
|
events: state.events
|
|
});
|
|
return;
|
|
}
|
|
if (req.method === "POST" && url.pathname === "/seed") {
|
|
const payload = await readBody(req);
|
|
state.seeded = true;
|
|
state.proof = false;
|
|
state.case_id = String(payload.case_id || "");
|
|
state.detail = "fixture seeded";
|
|
state.uploads = [];
|
|
state.sink_hits = 0;
|
|
state.payload = null;
|
|
note("seed", state.case_id || "anonymous");
|
|
sendJson(res, 200, { ok: true, detail: "fixture seeded", case_id: state.case_id });
|
|
return;
|
|
}
|
|
if (req.method === "POST" && url.pathname === "/attack") {
|
|
const payload = await readBody(req);
|
|
await handleAttack(payload);
|
|
sendJson(res, 200, { ok: true, detail: state.detail, case_id: state.case_id });
|
|
return;
|
|
}
|
|
sendJson(res, 404, { ok: false, detail: "not found" });
|
|
});
|
|
|
|
server.listen(port, "0.0.0.0");
|
|
|