Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2026-00966
- Vendor: 誠和國際股份有限公司, 悟空科技股份有限公司, 株式会社喜喜
- Title: 株式会社喜喜 日本站特定付款方式可被注入外部網域,導致第三方付款連結產生 Delayed Redirect 與 Phishing Risk
- Introduction: 修改日本站特定付款方式之特定參數,可在第三方付款網址過期後導向外部網站,造成 Delayed Redirect 與 Phishing Risk。
處理狀態
目前狀態
-
新提交
-
已審核
-
已通報
-
未回報修補狀況
-
未複測
-
公開
處理歷程
- 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 平台自動更新)
詳細資料
參考資料
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://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。
但測試時發現:
- 將參數
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": ""
}
}
-
而瀏覽器開啟
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被注入了該外部網域。 -
當
PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>)過期後,任何取得該連結的使用者再次開啟連結,頁面會先短暫停留在PayPay網站,接著導向先前注入的外部網域。
上述漏洞的完整流程如下:
攻擊者修改 webhook 參數
→ 後端接受外部 URL
→ 後端產生 PayPay 付款連結
→ 外部 URL 被寫入 redirectUrl
→ 付款連結過期
→ 使用者被重新導向外部網站
因此:
-
攻擊者可依此流程建立一個 PayPay 官方網域付款連結,待其過期後傳送給其他使用者、網站客服或網站後台管理人員;而點擊者若持續停留於頁面中,就會被導向攻擊者注入之外部網域,進而產生社交工程與釣魚風險。
-
串聯台灣站與日本站付款流程綁定不足漏洞,可將此流程建立的
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)
- 需要擁有一個可登入
helper.diyidphoto.com後台的 PB user 帳號,建議該帳號"role": ["admin"]會比較方便測試。可參考未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC建立。 - 不需實際付款或完成第三方金流交易等後續流程。
3. 重現步驟(Steps to Reproduce)與概念驗證(Proof of Concept)
以下重現步驟皆使用 Burp Suite 作為工具。
以下 PoC 僅整理我在實際測試中看到的重點證據。因為 HITCON 提交附件最多只能上傳 10 張圖,所以我只附足以證明主問題成立的必要代表性截圖。其餘我都有保留,若需要可再補充。
步驟 1:建立一筆日本站訂單並攔截 PayPay 付款流程的請求
- 在 Burp Suite 的瀏覽器連上日本站
https://jp.diyidphoto.com/上傳測試圖片,並一路進行到選擇付款方式的頁面;お支払い方法選擇paypay並填妥相關資料。
但在點擊次へ前,先到 Burp Suite 的 Proxy 頁籤中的 Intercept,點擊 Intercept off 按鈕使其變成 Intercept on,以便攔截請求。
- 點擊
次へ後,可見攔截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 改成外部網域
- 攔截請求後,將參數
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 請求
- 按下
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 請求
- 在 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 過期,並觀察其過期後行為
- 等待
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。
- 再次開啟該
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-可查看特定訂單的支付細節。
-
瀏覽器另一個分頁開啟
https://helper.diyidphoto.com/login,並使用已建立的帳號及密碼登入。可參考未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC建立。 -
在 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 可發現:
- 參數
RedirectURL的值為PayPay cashier URL。 - 參數
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/ 訂單,並攔截付款流程請求
- 在 Burp Suite 的瀏覽器開啟另兩個分頁,分別連上
https://diyidphoto.com/zh-tw/與https://qkidphoto.goqoo.com,上傳測試圖片並一路進行到選擇付款方式的頁面;
付款方式選擇街口支付、Line Pay 等掃碼式支付方式(以避免實際付款)並填妥相關資料,在點擊確定前,先到 Burp Suite 的 Intercept,點擊 Intercept off 按鈕使其變成 Intercept on,以便攔截請求。
- 點擊確定後,主要須觀察攔截到的請求如下(其它請求都能按下
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 改成外部網域
- 攔截到上述請求後,將參數
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": ""
}
- 按下
Forward按鈕以送出修改後的請求,然後點擊Intercept on使其變成Intercept off,不再攔截請求。此時瀏覽器會自然開始跳轉付款頁面。
重覆步驟 6,利用後台的支付細節(payment detail)端點,查詢交易紀錄(trade)
- 在 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 可發現:
- 參數
RedirectURL的值不為PayPay cashier URL,且Tappaysdk_PayURL不會過期跳轉,這是不同於日本站https://jp.diyidphoto.com其步驟 6 的結果。 - 參數
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 也同樣可發現:
- 參數
RedirectURL的值不為PayPay cashier URL,且Tappaysdk_PayURL不會過期跳轉,不同於日本站https://jp.diyidphoto.com其步驟 6 的結果。 - 參數
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>),這表示外部網域可被寫入交易紀錄作為資料保存。
經上述之測試,可發現:
- 三個付款流程端點的參數
webhook全都可以注入PayPay cashier URL(https://www.paypay.ne.jp/app/cashier?code=<Redacted_Code>)。 - 參數
RequestJSON會保存被污染後的webhook,這增加此漏洞的可利用性與嚴重性。
這裡值得注意的是,由於上述訂單皆沒有實際付款,公司內部應確認在付款完成/失敗的情況下,是否會跳轉被注入的外部網域。
4. 預期行為(Expected Behavior)
付款流程中的參數 webhook:
-
應由後端根據訂單站別與付款方式固定產生,並須經後端核對其指向可信任網域。
-
若被注入非預期之外部網域,後端應在付款流程進行前直接拒絕,或忽略外部傳入的值直接指向後端可信任的網域。
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)
如同台灣站與日本站付款流程綁定不足,問題可能出在付款流程端點與其相關資訊綁定不完整,或後端未在付款流程進行前再次嚴格檢查相關參數。
可能原因包括:
-
後端未嚴格限制
webhook必須屬於可信任之網域,或過度信任前端傳入的值。 -
webhook沒有嚴格綁定站別和付款方式,導致多個付款流程端點(/api/pay-ja、/api/pay與/api/idphoto/pay)皆可注入外部網域。 -
同一訂單新增交易紀錄前,沒有完整檢查
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)
-
本報告之測試僅限確認漏洞存在,未進行批次化、大量化或自動化操作,也未進行破壞性操作。
-
由於此系統與技術牽涉到三家公司-台灣公司為誠和國際股份有限公司(
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 的問題。