# -*- 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 ("