Code前端首页关于Code前端联系我们

关于Lodash安全漏洞背后的 JavaScript,你需要了解什么

terry 2年前 (2023-09-28) 阅读数 77 #Web安全
Lodash 严重安全漏洞背后 你得知道的 JavaScript 知识

一些信息敏感的同学可能已经了解到,Lodash的图书馆专业 refoam-security,影响了超过 400 万个项目。该漏洞导致lodash发布了“连夜”版本来修复潜在问题,并强烈鼓励开发者更新该版本。

当我们忙着“好看”或者“优化版本”的时候,请保持冷静思考:你真的了解这个漏洞的原因和背后的原理吗?修复这个漏洞吗?

这个简短的文章将从根本上分析这个案例,相信“小白”读者会有所收获。

漏洞产生的原因

其实漏洞很简单。举个例子:lodash中的defaultsDeep方法,

_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } })

输出:

{ 'a': { 'b': 2, 'c': 3 } }

根据上面的例子,这个方法:

返回任意属性的源对象(该方法的第二个参数)的可枚举属性。目标对象(方法的第一个参数)决定不确定

这种行为的隐患:

const payload = '{"constructor": {"prototype": {"toString": true}}}'

_.defaultsDeep({}, JSON.parse(payload))

造成原型污染。原型污染是指:

攻击者以某种方式修改了 JavaScript 对象的原型

与上面的例子相比,Object.prototype.toString会变得不确定。

原型污染详解

了解原型污染需要读者了解JavaScript中原型和原型链的知识。让我们从查看示例开始:

// person 是一个简单的 JavaScript 对象
let person = {name: 'lucas'}

// 输出 lucas
console.log(person.name)

// 修改 person 的原型
person.__proto__.name = 'messi'

// 由于原型链顺序查找的原因,person.name 仍然是 lucas
console.log(person.name)

// 再创建一个空的 person2 对象
let person2 = {}

// 查看 person2.name,输出 messi
console.log(person2.name)

增加风险:

let person = {name: 'lucas'}

console.log(person.name)

person.__proto__.toString = () => {alert('evil')}

console.log(person.name)

let person2 = {}

console.log(person2.toString())

执行此代码将警告恶意文本。同时,Object.prototype.toString这个方法常用于转换和类型判断。用法:

Object.prototype.toString 方法返回表示对象的字符串

每个对象都有一个方法 toString(),当对象表示为文本值时,或者当它被调用时其本身被称为预期字符串。默认情况下,所有 Object 对象都会继承 toString() 方法。如果未在自定义对象上重写此方法,则 toString() 返回 [对象类型],这是对象类型的类型。

如果对象原型上的 toString 被损坏,后果是可以预见的。以此为例,可见lodash这次的空虚是非常严重的。

我们再来说一下原型污染(NodeJS漏洞案例)

根据上面的分析,我们知道原型污染并不是一个新的漏洞,而是“时刻可见”、“无处不在” 。 Nullcon HackIM 竞赛中有一个类似的 hack 主题:

'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);
}
 
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
 
// App
const app = express();
app.use(bodyParser.json())
app.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}`);

此代码的漏洞在于连接。我们可以这样攻击它:

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://0.0.0.0:4000/signup'; 

curl -vv 'http://0.0.0.0:4000/getFlag'

首先请求NodeJS服务中的/signup接口,称为易受攻击的mergeLodash 严重安全漏洞背后 你得知道的 JavaScript 知识

,例如Object.prototype(因为{}.__pro to__ === Object.prototype)将新属性admin添加到请求接口的值admin 再次getFlag,条件语句admin.аadmin == 1true服务被攻击。

攻击案例来自:NodeJS应用原型污染攻击

此类漏洞在jQuery中也很常见$.extend PR:

  • jQuery原型污染漏洞
  • 对于 jQuery:如果您担心安全问题,请更新到最新的 jQuery 3.4.0。如果您仍在使用 jQuery 1.x 和 2.x,您的应用程序和网站可能容易受到攻击。

    避免原型污染

    既然我们了解了漏洞和攻击方法的潜在问题,那么我们该如何预防呢?

    关于lodash版“一夜”的构造:Lodash 严重安全漏洞背后 你得知道的 JavaScript 知识

    我们清楚地看到,当我们交叉合并时,当我们遇到构造函数__程序,exsensitive,是 __pro 。

    那么作为业务开发者,我们应该如何防范攻击呢?简而言之:

    • 禁用Object.prototype,这样原型就无法扩展属性

    我们可以使用Object.Object来实现cool()方法,有的东西会冻结。冻结的东西无法改变;当对象被冻结时,无法向该对象添加新属性,无法删除现有属性,并且无法更改该对象现有属性的枚举、可配置性和注册。 property,并且现有的属性值无法更改。此外,对象的原型在冻结后就无法更改。 freeze() 返回与传递的参数相同的对象。

    查看代码:

    Object.freeze(Object.prototype);
    
    Object.prototype.toString = 'evil'
    
    consoel.log(Object.prototype.toString)
    ƒ toString() { [native code] }
    

    比较:

    Object.prototype.toString = 'evil'
    
    console.log(Object.prototype.toString)
    "evil"
    
    • 生成JSON schema
      在解析用户输入时,使用JSON schema过滤敏感键名。 ?不要使用文字,而是使用 Object.create(null)

    Object.create() 方法创建一个新对象,使用现有对象分配新创建对象的 。 __proto__

    Object.create(null)的返回值不会与Object.prototype关联:

    let foo = Object.create(null)
    console.log(foo.__proto__)
    // undefined
    

    这样,无论对象的扩展名是什么,不干扰原型。 。

    • 实现新的Map数据类型而不是对象类型

    Map存储键/值对和键/货币对的集合。任何值(对象或基元)都可以用作键或值。通过使用Map数据结构,不存在Object原型污染。

    总结一下 Map 和 Object 的区别:

    • Object 键仅支持字符串或符号,而 Map 键可以是任何值,包括函数、对象和键类型
    • Command Map 中的键,但键在对象中则不是。
    • 特殊API差异:例如Map的键值对数量可以直接通过size属性获取,而Object的基本值无法获取;再比如返回一个Map和Object的区别也很明显
    • 在频繁添加和删除密钥对的情况下Map会具有性能优势

    附录:V8,chromium的能力

    JSON .parse 常用方法,但如果运行:

    JSON.parse('{ "a":1, "__proto__": { "b": 2 }}')
    

    ,你会看到返回的结果如图: Lodash 严重安全漏洞背后 你得知道的 JavaScript 知识

    重复写入 ‷‷失败 __原型__ 财产仍然是我们的 熟悉的感觉安全 __proto__ 。这是因为:

    V8 忽略 JSON.parse 中名为 proto 的键

    此讨论与 Doug Crockford、Brendan Eich 相关,但是,Chromium 和 JS 的创建者已多次解决此问题。相关问题和PR:

    • chromium讨论
    • chromium讨论

    相关ES语言设计讨论:ES语言设计讨论:proto-and-json

    在上面的链接中,你可以找到JavaScript的开发者等等对于成年人~

    简而言之,请注意,V8 默认情况下使用 JSON.parse 时会忽略 __proto__。 .当然,原因是为了保护之前的调查。

    总结

    通过分析lodash的弱点和解决办法,了解原型污染的各个方面。包含的知识点包括但不限于:

    • 对象原型
    • 原型、原型链
    • NodeJS相关问题
    • Object.create方法 ♸♸
    • 深度复制
    • 还有其他问题

    这样看来,都是基础知识了。它也是构成正面信息系统各方面的基础。

    版权声明

    本文仅代表作者观点,不代表Code前端网立场。
    本文系作者Code前端网发表,如需转载,请注明页面地址。

    热门