[天翼杯2020]apiTest wp

对javascript不是很熟dbq- -

访问/source得到源码

const express = require("express");
const cors = require("cors");
const app = express();
const uuidv4 = require("uuid/v4");
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");

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"
]
};
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);

res.json(ok({ token: sign({ id: adminId }) }));
});


// Get server info
app.get("/serverInfo", (req, res) => {
let user = users[req.user.id] || { rights: [] };
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];

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}!`)
);

首先从能获取flag的地方倒推回来(一大串代码审计一般都这样做

在最后获取flag的页面源码中:

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

要想返回flag,就需要req.user.id==0

那么再看看怎样才能让req.user.id==0

在/init中:

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);

res.json(ok({ token: sign({ id: adminId }) }));
});

target为config中secret的md5加密值

adminId要经过c.charCodeAt(0) ^ target.charCodeAt(i)),即让md5(secret)md5(config.secret)异或。

如果两个值相等的话异或结果为0,就达到了我们的目的。

因此我们直接将secret设成config.secret即可。接下来我们要得到config.secret

通过分析代码我们可以知道,通过/serverInfo:

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

在没有看接下来的代码的情况下,我们可以根据rights来返回config中的信息。如果传入{"rights"="secret"}的话按理说可以得到config.secret信息。

但得先看如何设置rights。

在/updateUser中:

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] }));
});

要先通过前面的isAdmin判断,然后还有一个rights的checkRights判断。我们先不管isAdmin判断,下一步再看。

先看checkRights函数:

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;
}

secret、port被加入了黑名单,不能出现在字符串或者数组中。因此不能直接使rights为secret了。

但在/serverInfo中,是通过config[i]来获取的,这里用到了一个我不知道的没听说过的的js特性:

这样同样能获取到”name“的值,并且可以绕过checkRights判断。

因此可以传入

POST /updateUser
{"rights":"[[[secret]]]"}

就能够得到secret的值了。

然后在绕过isAdmin判断即可。

按照以上思路,本题分为以下步骤:

绕过isAdmin

在/becomeAdmin中

app.post("/becomeAdmin", (req, res) => {
let {value} = req.body;
let uid = req.user.id;
let user = users[uid];

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!"));
}
});

[value,config.adminValue]进行了排序操作,然后取第二个值并赋给maxValue

然后要进行比较,使value>=maxValue && user.value>=value成立

这里又用到js的一个特性:

在进行默认sort时,先将数字转换为字符串后再排序。

因此,100和8排序的话,默认时js会认为100比8小。

因此只需要传入

POST /becomeAdmin
{"value":"8"}

即可绕过。

返回

{"status":"ok","data":{"isAdmin":true}}

config.secret

以上分析里已经说了,就不重复了。

返回

{"name":["secret"],"value":"1145141919810"}

得到正确token

根据以上分析,让secert值为上一步中的value就可以了。

然后带上返回的token访问/flag就可以了。

Author: Neorah
Link: https://neorah.me/%E5%A4%A9%E7%BF%BC%E6%9D%AF2020/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.