Drupal核心REST模块反序列化漏洞CVE-2019-6340深度剖析与复现

📅 2026/6/24 4:24:21
Drupal核心REST模块反序列化漏洞CVE-2019-6340深度剖析与复现
1. 项目概述一次对Drupal核心REST模块的深度剖析最近在整理历史高危漏洞的复现笔记翻到了2019年初那个让不少Drupal站点管理员心惊肉跳的CVE-2019-6340。这个漏洞的特别之处在于它并非出现在某个第三方插件里而是直接存在于Drupal 8.x核心的REST模块中一个反序列化不当导致的远程代码执行RCE漏洞。当时官方给出的CVSS 3.x评分高达9.8临界意味着攻击者无需任何身份验证就能通过特制的HTTP请求在服务器上执行任意代码。对于任何一个还在使用受影响版本主要是Drupal 8.6.x的网站来说这无异于门户大开。我之所以决定重新梳理并复现这个漏洞有几个原因。一是它非常经典完美展示了在现代化、模块化的CMS框架中一个看似“安全”的API端点如何因为底层组件的使用不当而演变成致命的入口。二是它的利用链涉及PHP反序列化、REST资源处理、以及Drupal特有的实体渲染机制理解这个过程对深入Web安全尤其是框架级漏洞的挖掘思路很有帮助。三是时至今日虽然官方早已发布补丁但互联网上仍有大量未及时更新的老旧站点或测试环境可能暴露于此风险之下作为安全研究者或运维人员掌握其原理和检测方法依然具有现实意义。这篇文章我会从一个实践者的角度带你一步步搭建靶场、分析漏洞成因、构造利用载荷并最终实现代码执行。整个过程我会尽量还原当时的漏洞环境并穿插我在复现过程中踩过的坑和总结的技巧。无论你是想入门漏洞复现的安全新手还是想深化对Drupal框架安全机制理解的中级开发者相信都能从中获得一些直接的参考。2. 漏洞原理深度解析从REST端点到底层反序列化要理解CVE-2019-6340我们不能只停留在“发个包就能执行命令”的层面必须深入Drupal 8的核心架构。这个漏洞的根源在于Drupal 8的REST模块对接收到的用户输入进行了不安全的反序列化操作。2.1 Drupal 8 REST模块与内容实体Drupal 8极大地强化了其Web服务能力RESTful Web Services模块被集成到核心中允许通过标准的HTTP方法GET, POST, PATCH, DELETE等对Drupal中的“内容实体”Content Entities进行操作。内容实体是Drupal的核心数据抽象比如节点Node、用户User、评论Comment等都可以视为一种内容实体。当通过REST API创建一个或更新一个实体时客户端会发送一个HTTP请求请求体通常是JSON或XML格式描述了实体的字段数据。Drupal的REST模块接收到请求后需要将这些序列化的数据“反序列化”回Drupal内部能够处理的PHP对象。这个过程本应是严格、受控的。2.2 漏洞触发点\Drupal\Core\Entity\EntityStorageBase::createFromStorageRecord问题的关键出现在特定条件下对实体字段数据的反序列化上。在Drupal 8.6.x中当通过REST API以application/json或application/xml格式发送PATCH请求用于更新资源时如果请求的目标实体类型支持特定的存储方式并且请求体中包含了精心构造的字段数据Drupal会尝试调用PHP的unserialize()函数来处理这些数据。具体来说漏洞存在于\Drupal\Core\Entity\EntityStorageBase类的createFromStorageRecord方法及其相关调用链中。当处理来自REST请求的字段值时如果该字段被定义为“可存储的序列化对象”并且其值以特定的序列化字符串格式如C:开头的自定义序列化对象开头Drupal的默认反序列化逻辑就会将其直接传递给unserialize()。注意这里有一个非常重要的前提条件即目标实体必须启用了REST资源并且配置为支持PATCH方法和application/json/application/xml格式。默认情况下Drupal 8核心的某些实体如node的REST端点可能是禁用的需要手动配置或通过其他模块开启。2.3 PHP反序列化与Gadget链的利用直接调用unserialize()用户可控的数据是极度危险的。攻击者可以提供一个序列化字符串该字符串在反序列化时会实例化一个PHP对象。如果这个对象的类在代码库中定义了__wakeup()、__destruct()或__toString()等魔术方法攻击者就可能利用这些方法作为跳板执行一系列预定义的操作最终达成任意代码执行。这一系列被利用的类和方法被称为“Gadget链”。在CVE-2019-6340的利用中攻击者正是利用了Drupal核心或某些常见贡献模块中存在的特定类链。一个经典的利用链会最终指向能够执行系统命令的PHP函数如passthru()、system()或exec()或者通过file_put_contents()写入Webshell。2.4 漏洞影响范围与条件总结简单来说要成功利用此漏洞需要满足以下几个条件Drupal版本Drupal 8.x具体是8.6.x系列8.6.0至8.6.10在未打补丁前受影响。其他8.x版本也可能在特定配置下受影响但8.6.x是主要靶标。模块启用核心的RESTful Web Services模块必须启用。权限与端点至少存在一个REST资源端点允许匿名或低权限用户执行PATCH操作。这通常需要站点管理员对REST权限进行了宽松的配置。数据格式请求必须使用application/json或application/xml格式。Gadget存在代码库中存在可利用的PHP类链Gadget Chain。Drupal核心本身在特定版本中就包含了可利用的链这降低了利用门槛。3. 靶场环境搭建与配置要点纸上得来终觉浅绝知此事要躬行。要真正理解这个漏洞亲手搭建一个复现环境是最好的方式。这里我选择使用Vulhub这个开源的漏洞靶场集成项目它已经为我们准备好了标准化的环境省去了很多繁琐的配置工作。3.1 使用Vulhub快速部署漏洞环境Vulhub的使用非常方便前提是你的系统已经安装了Docker和Docker Compose。首先从GitHub克隆Vulhub项目git clone https://github.com/vulhub/vulhub.git cd vulhub然后进入Drupal CVE-2019-6340漏洞的目录cd drupal/CVE-2019-6340最后使用Docker Compose一键启动环境docker-compose up -d执行上述命令后Docker会自动拉取镜像并启动容器。通常等待一两分钟服务就会在本地8080端口启动。你可以通过浏览器访问http://your-server-ip:8080/来访问Drupal安装界面。提示第一次启动时Drupal会进入安装向导。Vulhub的镜像通常已经做了一些预处理但你可能仍然需要完成基本的站点信息配置如站点名称、管理员账号等。为了复现漏洞建议使用标准安装选项并记住你设置的管理员密码。3.2 关键配置启用REST模块与设置权限环境启动后漏洞利用还需要一些配置。默认情况下Drupal 8的REST API可能没有为匿名用户开放写权限。我们需要进入后台进行配置。登录管理后台使用你安装时设置的管理员账号登录http://your-server-ip:8080/user/login。启用REST UI模块可选但推荐Drupal核心自带REST模块但管理界面可能不够直观。你可以先到“管理” - “扩展” (/admin/modules) 中搜索并启用“REST UI”模块。这是一个贡献模块但Vulhub环境可能已包含它提供了图形化界面来配置REST资源。配置内容类型的REST资源进入“管理” - “配置” - “Web服务” - “REST” (/admin/config/services/rest)。在资源列表中找到“内容”对应entity:node资源。点击“编辑”。在“方法”中确保“PATCH”是被勾选的。这允许通过REST API更新节点。在“格式”中确保“json”是被勾选的。这是漏洞利用所需的格式。最关键的步骤在“身份验证”部分。为了简化复现模拟配置不当的场景我们可以暂时为这个资源设置一个宽松的权限。在“身份验证提供程序”中取消所有选项仅保留“cookie”。但这还不够我们还需要设置权限。设置REST权限进入“管理” - “人员” - “权限” (/admin/people/permissions)。在权限列表中找到“RESTful Web Services”区块。我们需要为“匿名用户”授予通过REST更新内容的权限。找到类似于“通过REST PATCH内容资源”的权限描述具体名称可能是“Access PATCH on Content resource”。勾选“匿名用户”对应的复选框。点击“保存权限”。完成以上步骤后理论上匿名用户就可以向/node/nid发送PATCH请求来更新节点了。这为漏洞利用创造了必要条件。实操心得在实际的漏洞复现或渗透测试中遇到这种RCE漏洞我们往往没有后台权限去修改配置。因此更常见的利用场景是1目标站点管理员已经为了某些功能如移动端接入配置了宽松的REST权限2攻击者通过其他漏洞如信息泄露、权限提升先获取了配置权限。我们的复现环境是理想化的旨在理解漏洞原理。在真实测试中发现开放的REST端点本身就是信息收集的重要一环。4. 漏洞利用链构造与PoC实战环境配置妥当接下来就是最核心的部分构造能够触发代码执行的Payload。我们不会使用现成的、危害性的攻击载荷而是通过一个无害的PoC概念验证来演示漏洞确实可以被触发例如执行一个简单的phpinfo()或弹出一个计算器在Linux下。4.1 利用链分析与Payload生成Drupal 8.6.x中内置了一个可利用的Gadget链。安全研究人员已经公开了相关的细节。其核心是利用了GuzzleHttp\Psr7\FnStream类Guzzle HTTP库的一部分和Drupal内部的一些类。简单来说攻击者构造一个特殊的序列化对象当它被反序列化时会触发FnStream的__destruct()方法进而调用一个由攻击者控制的回调函数最终达到执行任意代码的目的。我们可以使用一个简单的PHP脚本在攻击机或一个独立环境中运行来生成Payload。以下是一个生成调用phpinfo()的Payload的示例脚本?php // gadget.php - 用于生成CVE-2019-6340的序列化Payload namespace GuzzleHttp\Psr7; class FnStream { public $_fn_close; public function __construct() { // 将 _fn_close 设置为一个能执行代码的数组回调 // 例如调用 phpinfo() $this-_fn_close [phpinfo]; } } $obj new FnStream(); $serialized serialize($obj); // 为了符合Drupal字段的格式我们需要将其包装一下。 // 漏洞触发点期望一个以特定方式序列化的对象。 // 一种常见的PoC格式是使用自定义序列化C:开头的格式但这里我们简化演示。 // 实际利用中可能需要更复杂的包装和编码。 echo base64_encode($serialized) . \n; // 或者直接输出用于嵌入JSON echo urlencode($serialized) . \n; ?注意上述代码是一个极度简化的原理演示。实际的、完整的利用Payload要复杂得多它需要精确匹配Drupal内部处理字段时的预期数据结构并串联起多个类构成完整的Gadget链。在公开的漏洞利用工具如Metasploit模块、Python EXP脚本中Payload是精心构造的。出于安全与合规考虑本文不提供可直接执行任意命令的完整攻击载荷。安全研究应在完全隔离的实验室环境中进行。4.2 构造恶意HTTP PATCH请求假设我们已经通过信息收集知道了目标Drupal站点上存在一个ID为1的节点文章并且其REST端点/node/1对匿名用户开放了PATCH权限。我们的目标是将恶意Payload通过某个字段例如body字段的value属性传递进去。请求的构造如下请求方法:PATCH请求URL:http://target-server:8080/node/1?_formatjson请求头:Content-Type: application/jsonAccept: application/json请求体 (JSON):{ _links: { type: { href: http://target-server:8080/rest/type/node/article } }, type: { target_id: article }, body: { value: YOUR_MALICIOUS_SERIALIZED_PAYLOAD } }关键点在于body.value的值。我们需要将之前生成的、经过适当编码如Base64的序列化字符串放在这里。在某些利用方式中可能需要对Payload进行额外的包装例如将其设置为一个数组其第一个元素是序列化字符串并带有特定的元数据标记以欺骗Drupal的反序列化逻辑。4.3 使用工具发送请求并验证结果我们可以使用cURL命令行工具或Burp Suite这类代理工具来发送请求。使用cURL的示例curl -X PATCH \ http://192.168.1.100:8080/node/1?_formatjson \ -H Content-Type: application/json \ -H Accept: application/json \ -d { _links: { type: { href: http://192.168.1.100:8080/rest/type/node/article } }, type: { target_id: article }, body: { value: YToxOntzOjIxOiIgR3V6emxlSHR0cFxccHNyc1xcRm5TdHJlYW1fZm5fY2xvc2UiO2E6MTp7aTowO3M6NzoicGhwaW5mbyI7fX0 } }如果漏洞存在且利用成功服务器在处理这个PATCH请求时就会触发反序列化执行Payload中的代码例如phpinfo()。那么响应中可能包含phpinfo()函数的输出或者如果执行的是系统命令则可能无回显。如何验证直接回显如果Payload是phpinfo()或输出特定字符串查看HTTP响应体。间接验证如果执行的是无回显命令如写入文件、反弹Shell可以尝试访问写入的文件或检查是否建立了网络连接。错误信息即使利用不成功服务器也可能返回与反序列化相关的错误信息如“Class ‘GuzzleHttp\Psr7\FnStream’ not found”这同样可以侧面证实漏洞点已被触及。踩坑记录在我最初的复现中直接使用网上找到的旧Payload失败了。原因是Vulhub使用的Drupal镜像版本或PHP环境可能与生成Payload的原始环境有细微差异。例如PHP版本不同可能导致序列化字符串的格式特别是涉及对象引用和私有/受保护属性时不兼容。解决办法是最好在与靶场完全相同的PHP版本环境下生成Payload或者使用针对特定环境调整过的利用脚本。5. 漏洞修复方案与安全加固建议复现漏洞是为了更好地防御。Drupal官方在漏洞披露后迅速发布了安全更新。了解如何修复和加固对于运维人员和开发者至关重要。5.1 官方补丁分析Drupal针对CVE-2019-6340的修复方案核心是在反序列化用户输入之前进行严格的类型检查和过滤杜绝不可信数据触发对象实例化。补丁主要修改了\Drupal\Core\Entity\EntityStorageBase等相关类的代码。修复逻辑可以概括为白名单验证在反序列化前检查即将被反序列化的数据是否来自可信的内部存储结构而不是直接来自用户输入的REST请求。移除危险路径修改了字段数据从请求到存储的反序列化流程确保用户提供的序列化字符串不会被直接传递给unserialize()函数。对于通过REST API接收的实体字段数据框架会将其作为纯文本或结构化数组处理而不再尝试将其解析为序列化对象。引入严格的序列化格式检查即使在某些内部流程中需要反序列化也会先验证序列化字符串的格式拒绝那些包含对象实例化O:、C:等危险类型的字符串。修复操作 对于受影响的Drupal 8.6.x站点最直接、最安全的做法是立即升级到最新版本。当时官方的安全公告要求升级到Drupal 8.6.10 或更高版本针对8.6.x系列或者升级到Drupal 8.7.x系列如果准备进行大版本升级升级命令示例在Drupal根目录下# 使用Composer升级到指定安全版本 composer require drupal/core-recommended:8.6.10 --update-with-dependencies --no-update composer update升级后务必运行update.php(/update.php) 以完成数据库架构的更新。5.2 临时缓解措施如果因为某些原因无法立即升级可以考虑以下临时缓解措施但这绝不能替代永久性升级禁用REST模块如果站点不需要RESTful Web Services可以直接在“扩展”页面禁用核心的RESTful Web Services模块。这是最彻底的方案。严格收紧REST权限立即审查所有REST资源配置。进入/admin/config/services/rest检查每一个资源。确保没有任何资源对“匿名用户”或“认证用户”开放PATCH、POST、DELETE等写操作方法。对于只读API仅保留GET方法。使用Web应用防火墙WAF在网站前端部署WAF可以配置规则来拦截包含可疑序列化字符串如O:、C:、s:后跟异常长度等模式的HTTP请求体特别是对/node/*等路径的PATCH请求。5.3 针对开发者的安全编码启示这个漏洞也给所有使用类似框架的开发者上了一课永远不要反序列化不可信数据这是铁律。unserialize()用户输入是高风险操作。如果必须传递复杂数据结构请使用JSON等安全格式并在应用层进行解析和验证。谨慎使用__wakeup()和__destruct()在定义这些魔术方法时要意识到它们可能在对象生命周期的不确定时刻被调用尤其是在反序列化场景下。避免在其中执行关键性或危险的操作。最小权限原则对于API端点尤其是写操作POST/PATCH/PUT/DELETE必须实施严格的权限控制。默认情况下应该拒绝所有匿名访问并基于角色进行细粒度授权。依赖项安全漏洞利用了Guzzle库中的类。这说明第三方依赖也是攻击面的一部分。需要定期使用composer audit或类似工具检查项目依赖的已知漏洞并及时更新。6. 复现过程中的常见问题与排查技巧即使按照步骤操作复现过程也可能遇到各种问题。这里我总结了一些常见的情况和解决方法。6.1 环境启动与访问问题问题现象可能原因排查与解决docker-compose up失败提示端口冲突本地8080端口已被其他程序占用修改docker-compose.yml文件将ports下的8080:80改为其他端口如8081:80然后重启。访问http://ip:8080显示“无法连接”或超时1. Docker服务未运行2. 防火墙阻止了端口3. 容器启动失败1. 运行systemctl status docker或docker ps检查Docker。2. 检查防火墙规则开放对应端口。3. 运行docker-compose logs查看容器日志定位启动错误。Drupal安装页面提示数据库连接错误Vulhub的MySQL容器尚未完全初始化等待一两分钟再刷新页面。可以运行docker-compose logs mysql查看数据库容器日志。6.2 漏洞利用请求失败分析问题现象可能原因排查与解决发送PATCH请求后返回403 ForbiddenREST权限未正确配置匿名用户无权PATCH节点。1. 确认已按照3.2章节为匿名用户授予了“通过REST PATCH内容资源”的权限。2. 检查REST资源配置中entity:node资源是否启用了PATCH方法和json格式。3. 尝试使用已认证的用户如管理员的Session Cookie来发送请求。返回404 Not Found目标节点ID不存在或REST路由未正确注册。1. 确保站点上存在ID为1的节点。可以先访问http://ip:8080/node/1看看。2. 尝试创建一个新节点并使用其真实的ID。3. 检查REST模块是否确实已启用。返回415 Unsupported Media Type请求头Content-Type不是application/json。确保cURL或Burp Suite的请求头中正确设置了Content-Type: application/json。返回500 Internal Server Error或反序列化错误1. Payload格式错误或编码问题。2. 目标环境缺少必要的Gadget类。1. 检查Payload的生成环境PHP版本是否与靶场一致。尝试使用公开的、针对该Vulhub环境测试过的EXP脚本。2. 查看Docker容器的错误日志docker-compose logs drupal请求成功返回200但未执行代码Payload未能成功触发完整的Gadget链。1. 这可能是因为Payload只触发了部分链但未能执行到最终的命令。尝试使用更简单、有回显的PoC如触发一个Notice或Warning来验证漏洞点是否可达。2. 检查Drupal版本是否完全匹配。8.6.0到8.6.10之间的小版本也可能有细微差异。6.3 高级调试技巧如果上述方法都无法解决问题可以进行更深入的调试进入容器内部使用docker-compose exec drupal bash进入Drupal容器。你可以直接查看web目录下的文件确认版本号甚至修改代码加入调试语句。开启Drupal错误日志在Drupal的sites/default/settings.php中确保有以下配置以显示所有错误$config[system.logging][error_level] verbose; $config[system.performance][css][preprocess] FALSE; $config[system.performance][js][preprocess] FALSE;修改后需要重启Web服务如Apache或清除Drupal缓存。使用Xdebug如果环境支持可以配置Xdebug然后使用Burp Suite或Postman发送请求在IDE中设置断点跟踪反序列化的完整流程。这对于理解漏洞机理和调试复杂Payload至关重要。复现CVE-2019-6340的过程就像一次对Drupal内核和PHP反序列化漏洞的解剖。从配置宽松的REST端点到不安全的反序列化调用再到精心串联的Gadget链每一个环节的失守都提醒我们安全是一个整体任何一处的疏忽都可能被放大成整个系统的崩塌。对于防御者而言及时更新、最小权限、对用户输入保持绝对警惕这些原则永远不过时。