# -*- coding: utf-8 -*- """对抗性验证第二轮:修正上一版的归因错误,并加打真正的危险面。 上一版的错:场景 4 里五种非法 deviceId 用了**同一个配对码**, 第一个(控制字符)成功注册后,后面几种其实是被"码已被占用"拒掉的, 并不是因为 deviceId 非法 —— 归因错了,等于没测到。 这一版: * 每次尝试用**全新的配对码**,保证拒绝原因只可能来自 deviceId 本身 * 加打 **管理页 XSS**:deviceId 和 name 是设备自己报上来的, 如果管理页不转义就直接拼进 HTML,就是存储型 XSS * 加打 **deviceId 撞车**:两台设备用同一个 deviceId 会怎样 * 加打 **改码成自己正在占的码 / 改成别人占的码** """ from __future__ import annotations import json import os import subprocess import sys import tempfile import time import urllib.error import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PORT = 8138 BASE = f"http://127.0.0.1:{PORT}" _UA = {"User-Agent": "LanRemote/1.8.2"} ADMIN_KEY = "test-admin-key-67890" passed = 0 failed = 0 findings: list[str] = [] def check(name: str, ok: bool, detail: str = "") -> bool: global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + ((" —— " + str(detail)) if detail else "")) return ok def note(text: str) -> None: findings.append(text) print(" [!!] " + text) def main() -> int: work = Path(tempfile.mkdtemp(prefix="advrelay2_")) relay = None try: data = work / "d" data.mkdir(parents=True, exist_ok=True) env = dict(os.environ) env.update({ "RELAY_PORT": str(PORT), "RELAY_BIND": "127.0.0.1", "RELAY_DATA": str(data), "RELAY_DEVICE_TTL": "3600", "RELAY_ADMIN_KEY": ADMIN_KEY, "RELAY_POLL": "2", "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1", }) relay = subprocess.Popen( [sys.executable, "-u", str(ROOT / "relay" / "relay.py")], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) up = False for _ in range(60): time.sleep(0.5) try: r = urllib.request.Request(BASE + "/v1/health", headers=_UA) with urllib.request.urlopen(r, timeout=3) as resp: if resp.status == 200: up = True break except Exception: # noqa: BLE001 continue check("真中转起来了", up) if not up: return 1 def reg(dev: str, code: str, name: str = "对抗测试机") -> dict: body = json.dumps({"deviceId": dev, "code": code, "name": name, "version": "1.8.2"}).encode() r = urllib.request.Request( BASE + "/v1/agent/register", data=body, method="POST", headers={**_UA, "Content-Type": "application/json"}) try: with urllib.request.urlopen(r, timeout=10) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: raw = exc.read().decode("utf-8", "replace") try: return json.loads(raw) except Exception: # noqa: BLE001 return {"ok": False, "raw": raw[:160]} def admin_get(path: str) -> tuple: r = urllib.request.Request( BASE + path, headers={**_UA, "X-Admin": ADMIN_KEY}) try: with urllib.request.urlopen(r, timeout=15) as resp: return resp.status, resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as exc: return exc.code, exc.read().decode("utf-8", "replace") def health() -> dict: r = urllib.request.Request(BASE + "/v1/health", headers=_UA) with urllib.request.urlopen(r, timeout=10) as resp: return json.loads(resp.read().decode("utf-8")) # 配对码生成器:保证每次都是全新的,避免"码被占"污染归因 counter = [int(time.time()) % 100000] def fresh_code() -> str: counter[0] = (counter[0] + 1) % 1000000 return "%06d" % counter[0] # ============================================================ print() print("═══ 场景 A:非法 deviceId —— 每次用全新配对码,归因干净 ═══") # ============================================================ cases = [ ("超长 200 字符", "x" * 200, False), ("超长 81 字符", "y" * 81, False), ("正好 80 字符", "z" * 80, True), ("控制字符 NUL", "dev\x00abc", None), ("控制字符 \\r\\n", "dev\r\nInjected", None), ("HTML 标签", "", None), ("引号", "dev\"'`", None), ("路径分隔符", "../../etc/passwd", None), ("Unicode", "设备-😀-ünïcode", None), ("只有空格", " ", False), ("空字符串", "", False), ] for label, bad, expect in cases: r = reg(bad, fresh_code()) got = r.get("ok") is True if expect is None: # 未定义期望:只记录实际行为,不判对错 verdict = "接受" if got else "拒绝" print(f" · {label:18s} -> {verdict}" + ("" if got else f"({r.get('error','')})")) if got and (" 标签(未转义的 deviceId)", raw_img not in page, "发现原始标签 -> 存储型 XSS") check("管理页没有原始