更新: 421 个文件 - 2026-03-17 18:30:02
这个提交包含在:
@@ -0,0 +1,2 @@
|
||||
{"system_id":"gitea","family":"authz-bypass","title":"Gitea Authz Bypass Fixture","subtitle":"Protected admin route with server-side bypass marker.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"gitea","family":"file-upload","title":"Gitea File Upload Fixture","subtitle":"Attachment acceptance path with inert upload marker.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"gitea","family":"proxy-boundary","title":"Gitea Proxy Boundary Fixture","subtitle":"Forwarded header trust boundary and admin gate fixture.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"gitea","family":"ssrf","title":"Gitea SSRF Fixture","subtitle":"Server-side callback route restricted to a local sink.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"gitea","family":"xss","title":"Gitea Stored XSS Fixture","subtitle":"Stored payload rendering path for browser proof capture.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"nextjs","family":"authz-bypass","title":"Next.js Authz Bypass Fixture","subtitle":"Protected route fixture with explicit bypass proof.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"nextjs","family":"deserialization","title":"Next.js Deserialization Fixture","subtitle":"Unsafe decode path with inert marker object.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"nextjs","family":"proxy-boundary","title":"Next.js Proxy Boundary Fixture","subtitle":"Middleware trust-boundary fixture with forwarded-header proof.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"nextjs","family":"ssrf","title":"Next.js SSRF Fixture","subtitle":"Server-side fetch route restricted to local sink validation.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"nextjs","family":"xss","title":"Next.js XSS Fixture","subtitle":"Browser proof page for stored payload rendering.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
|
||||
SCENARIO_PATH = Path(os.environ["LAB_FIXTURE_SCENARIO"])
|
||||
PORT = int(os.environ.get("PORT", "3000"))
|
||||
SCENARIO = json.loads(SCENARIO_PATH.read_text(encoding="utf-8"))
|
||||
STATE = {
|
||||
"seeded": False,
|
||||
"proof": False,
|
||||
"family": SCENARIO["family"],
|
||||
"system_id": SCENARIO["system_id"],
|
||||
"case_id": "",
|
||||
"detail": "fixture ready",
|
||||
"uploads": [],
|
||||
"sink_hits": 0,
|
||||
"payload": None,
|
||||
"events": [],
|
||||
}
|
||||
|
||||
|
||||
def _note(event: str, detail: str) -> None:
|
||||
STATE["events"].append({"event": event, "detail": detail})
|
||||
STATE["events"] = STATE["events"][-20:]
|
||||
|
||||
|
||||
def _render_html() -> str:
|
||||
title = SCENARIO["title"]
|
||||
proof = STATE["proof"]
|
||||
banner = f"<div class='proof'>Proof active: {STATE['detail']}</div>" if proof else "<div class='baseline'>Baseline ready</div>"
|
||||
xss_block = ""
|
||||
if proof and STATE["family"] == "xss":
|
||||
xss_block = (
|
||||
"<script>document.documentElement.setAttribute('data-xss-proof','true');"
|
||||
f"document.title = {json.dumps(title + ' - proof')};</script>"
|
||||
f"<div id='xss-proof'>XSS marker executed for {STATE['case_id']}</div>"
|
||||
)
|
||||
upload_block = ""
|
||||
if STATE["uploads"]:
|
||||
items = "".join(f"<li>{item['filename']}</li>" for item in STATE["uploads"])
|
||||
upload_block = f"<section><h2>Uploads</h2><ul>{items}</ul></section>"
|
||||
sink_block = ""
|
||||
if STATE["sink_hits"]:
|
||||
sink_block = f"<section id='ssrf-proof'>Local sink hits: {STATE['sink_hits']}</section>"
|
||||
deserialize_block = ""
|
||||
if proof and STATE["family"] == "deserialization":
|
||||
deserialize_block = f"<section id='deserialize-proof'>Decoded marker: {STATE['case_id']}</section>"
|
||||
admin_block = ""
|
||||
if proof and STATE["family"] in {"proxy-boundary", "authz-bypass"}:
|
||||
admin_block = "<section id='admin-proof'>Admin boundary bypass confirmed.</section>"
|
||||
return f"""<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title}{' - proof' if proof and STATE['family'] != 'xss' else ''}</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>{title}</h1>
|
||||
<p>{SCENARIO['subtitle']}</p>
|
||||
{banner}
|
||||
<p>System: <code>{SCENARIO['system_id']}</code> / Family: <code>{SCENARIO['family']}</code></p>
|
||||
{admin_block}
|
||||
{xss_block}
|
||||
{upload_block}
|
||||
{sink_block}
|
||||
{deserialize_block}
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format: str, *args) -> None:
|
||||
return
|
||||
|
||||
def _json(self, status_code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(status_code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _html(self, payload: str) -> None:
|
||||
body = payload.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/healthz":
|
||||
self._json(200, {"ok": True, "system_id": SCENARIO["system_id"], "family": SCENARIO["family"]})
|
||||
return
|
||||
if parsed.path == "/":
|
||||
self._html(_render_html())
|
||||
return
|
||||
if parsed.path == "/admin":
|
||||
if STATE["proof"] and STATE["family"] in {"proxy-boundary", "authz-bypass"}:
|
||||
self._json(200, {"ok": True, "detail": STATE["detail"], "case_id": STATE["case_id"]})
|
||||
else:
|
||||
self._json(403, {"ok": False, "detail": "admin boundary still enforced"})
|
||||
return
|
||||
if parsed.path == "/sink":
|
||||
STATE["sink_hits"] += 1
|
||||
_note("sink-hit", parsed.query or "local callback")
|
||||
self._json(200, {"ok": True, "sink_hits": STATE["sink_hits"]})
|
||||
return
|
||||
if parsed.path == "/proof":
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"success": bool(STATE["proof"]),
|
||||
"detail": STATE["detail"],
|
||||
"case_id": STATE["case_id"],
|
||||
"sink_hits": STATE["sink_hits"],
|
||||
"uploads": STATE["uploads"],
|
||||
"events": STATE["events"],
|
||||
},
|
||||
)
|
||||
return
|
||||
self._json(404, {"ok": False, "detail": "not found"})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
parsed = urlparse(self.path)
|
||||
raw = self.rfile.read(int(self.headers.get("Content-Length", "0") or "0"))
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8") or "{}")
|
||||
except Exception:
|
||||
payload = {}
|
||||
if parsed.path == "/seed":
|
||||
STATE["seeded"] = True
|
||||
STATE["proof"] = False
|
||||
STATE["case_id"] = str(payload.get("case_id") or "")
|
||||
STATE["detail"] = "fixture seeded"
|
||||
STATE["uploads"] = []
|
||||
STATE["sink_hits"] = 0
|
||||
STATE["payload"] = None
|
||||
_note("seed", STATE["case_id"] or "anonymous")
|
||||
self._json(200, {"ok": True, "detail": "fixture seeded", "case_id": STATE["case_id"]})
|
||||
return
|
||||
if parsed.path == "/attack":
|
||||
family = str(payload.get("family") or STATE["family"])
|
||||
STATE["case_id"] = str(payload.get("case_id") or STATE["case_id"])
|
||||
STATE["payload"] = payload
|
||||
STATE["proof"] = True
|
||||
if family == "proxy-boundary":
|
||||
STATE["detail"] = "trusted forwarded headers crossed the boundary"
|
||||
elif family == "authz-bypass":
|
||||
STATE["detail"] = "server-side authorization recheck was bypassed"
|
||||
elif family == "ssrf":
|
||||
with urlopen(f"http://127.0.0.1:{PORT}/sink?case_id={STATE['case_id']}") as response:
|
||||
response.read()
|
||||
STATE["detail"] = "server-side callback reached the local sink"
|
||||
elif family == "xss":
|
||||
STATE["detail"] = "stored payload rendered inside the browser proof page"
|
||||
elif family == "file-upload":
|
||||
STATE["uploads"].append(
|
||||
{
|
||||
"filename": payload.get("filename") or f"{STATE['case_id']}.txt",
|
||||
"content": payload.get("content") or "",
|
||||
}
|
||||
)
|
||||
STATE["detail"] = "upload marker accepted and listed"
|
||||
elif family == "deserialization":
|
||||
STATE["detail"] = "unsafe object graph decoded without gadget execution"
|
||||
_note("attack", STATE["detail"])
|
||||
self._json(200, {"ok": True, "detail": STATE["detail"], "case_id": STATE["case_id"]})
|
||||
return
|
||||
self._json(404, {"ok": False, "detail": "not found"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
||||
server.serve_forever()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"undici","family":"ssrf","title":"Undici SSRF Fixture","subtitle":"Undici-style request path proving only local sink callbacks.","browser_required":false}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"vite","family":"file-upload","title":"Vite File Upload Fixture","subtitle":"Local upload marker path with browser-visible proof.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"vite","family":"proxy-boundary","title":"Vite Proxy Boundary Fixture","subtitle":"Dev-server proxy boundary fixture with proof banner.","browser_required":true}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"system_id":"vite","family":"xss","title":"Vite XSS Fixture","subtitle":"Client rendering proof for stored payload execution marker.","browser_required":true}
|
||||
|
||||
在新工单中引用
屏蔽一个用户