Ограничение на просмотр гостями
  • [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Ищем и эксплуатируем уязвимости в плагинах WordPress

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,517
Розыгрыши
0
Реакции
888
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В экосистеме WordPress постоянно находят новые уязвимости: от совсем простых до довольно сложных. У многих из них есть CVE, и многие исследователи тоже хотели бы когда‑нибудь увидеть свое имя в NVD. В этой статье я покажу, как поднять домашнюю лабораторию для поиска багов в плагинах WordPress: развернуть WordPress в Docker, подключить Xdebug для динамической отладки, настроить Semgrep как SAST-инструмент и на основе этого собрать базовый пайплайн поиска уязвимостей.

Поднимаем WordPress​

Для поиска уязвимостей в WordPress совершенно точно понадобится иметь живую тестовую установку WordPress, которую можно со спокойной душой сломать и не жалеть. Проще всего это сделать, используя Docker: один контейнер с базой и один с WordPress, в котором заранее будет настроен Xdebug.

info​

Все примеры в статье сделаны на Linux, но на Windows все тоже должно заработать — для этого понадобится WSL2 или Docker Desktop.
Для начала создаем папку для тестов и скачиваем туда последнюю версию WordPress:

mkdir wordpresslab
cd wordpresslab
wget Для просмотра ссылки Войди или Зарегистрируйся
unzip latest.zip
После распаковки переименовываем wordpress в wp, эта папка нам понадобится позже при создании контейнера.

Дальше создадим в нашей папке файл docker-compose.yaml, чтобы одной командой можно было поднять сразу весь тестовый стенд. В нем описаны база данных и сам контейнер с WordPress. Секреты от базы данных тоже описаны в этом файле, позже они будут нужны для настройки WordPress. Поскольку наш тестовый инстанс не будет смотреть в сеть, да и чувствительных данных там не предвидится, придумывать сложный пароль кажется лишним, так что я везде задал его как wordpress.

version: '3.3'

services:
db:
image: docker.io/bitnami/mariadb:10.3-debian-10
restart: on-failure
environment:
MARIADB_USER: wordpress
MARIADB_PASSWORD: wordpress
MARIADB_ROOT_PASSWORD: wordpress
MARIADB_DATABASE: wordpress

wp:
depends_on:
- db
image: wordpressxdebug
build:
context: .
dockerfile: Dockerfile
volumes:
- ./wp:/var/www/html
ports:
- 8080:80
restart: on-failure
extra_hosts:
- "host.docker.internal:host-gateway"
command: sh -c "chmod -R 777 /var/www/html && apache2-foreground"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
Порт 8080 можно поменять на любой удобный. Здесь же папка ./wp, в которую мы шагом раньше качали чистый WordPress, монтируется внутрь контейнера.

Теперь перейдем к самому образу WordPress. Создаем рядом файл Dockerfile со следующим содержимым:

FROM wordpress:latest

ENV XDEBUG_PORT 9000
ENV XDEBUG_IDEKEY docker

RUN pecl install "xdebug" \
&& docker-php-ext-install pdo pdo_mysql \
&& docker-php-ext-enable xdebug pdo pdo_mysql

RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/xdebug.ini && \
echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/xdebug.ini && \
echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/xdebug.ini && \
echo "xdebug.client_port=${XDEBUG_PORT}" >> /usr/local/etc/php/conf.d/xdebug.ini && \
echo "xdebug.idekey=${XDEBUG_IDEKEY}" >> /usr/local/etc/php/conf.d/xdebug.ini && \
echo "xdebug.log=/tmp/xdebug.log" >> /usr/local/etc/php/conf.d/xdebug.ini
К официальному образу WordPress тут добавляется Xdebug, что позволит нам на живую препарировать и сам WordPress, и его плагины.

Кроме добавления Xdebug в контейнер, нужно настроить еще ответную часть со стороны IDE. Я использую Visual Studio Code, куда прикручен сам отладчик Xdebug. Для настройки в папке с WordPress создаем папку .vscode и в ней файл launch.json:

{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000,
"pathMappings": {
"/var/www/html": "${workspaceRoot}/wp",
}
}
]
}
pathMappings тут указывает, что путь /var/www/html внутри контейнера соответствует папке wp на хосте. Если будешь менять какие‑либо папки в стенде — не забудь дописать их в этот файл.

Теперь, когда все готово, можно собрать образ и запустить контейнеры. Благодаря Docker это делается всего одной командой:

docker compose up
После запуска открываем в браузере адрес нашего тестового стенда и настраиваем самый обычный WordPress. Параметры базы нужно переписать из docker-compose.yaml, а адрес хоста с базой задать как db (имя контейнера базы). Никаких .com, .local или подобного — только само имя!

info​

Чтобы не возникло проблем с Burp Suite, для подключения к WordPress лучше использовать именно IP из локальной сети, а не localhost.
После окончания установки сам фронтенд сайта будет доступен на /, а админка — на /wp-admin. Рекомендую проконтролировать установку, чтобы дальше не было сюрпризов: проверь, что сайт открывается и работает, в админку можно нормально залогиниться, а исходники лежат в ./wp и видны в VS Code.

Теперь нужно убедиться, что сам Xdebug работает и его можно использовать.

Открываем проект в VS Code из корня с нашим тестовым проектом, где лежат wp, docker-compose.yaml и .vscode:

code .
На вкладке Run and Debug выбираем конфигурацию Listen for XDebug и запускаем ее кнопкой Start Debugging. Теперь отладчик будет принимать все входящие подключения от Xdebug, который находится в контейнере WordPress.

Для просмотра ссылки Войди или Зарегистрируйся
Чтобы не лезть сразу в ядро WordPress, давай потренируемся на кошках тестовом скрипте, на примере которого рассмотрим основные возможности Xdebug. Вот листинг test.php:

<?php

$requestId = bin2hex(random_bytes(4));
$action = $_GET['action'] ?? 'view';
$userId = (int)($_GET['user'] ?? 0);

$noteTitle = trim($_POST['title'] ?? '');
$noteBody = trim($_POST['body'] ?? '');

$note = [
'id' => $requestId,
'user_id' => $userId,
'title' => $noteTitle,
'body' => $noteBody,
];

log_request($action, $note);

if ($action === 'save') {
$filename = build_filename($userId, $noteTitle);
save_note($filename, $note);
echo "OK";
} else {
echo "Nothing to do";
}

function log_request(string $action, array $note): void {
$log = [
'time' => date('c'),
'action' => $action,
'note_title' => $note['title'],
'note_len' => strlen($note['body']),
];

file_put_contents(DIR . '/debug.log', json_encode($log) . PHP_EOL, FILE_APPEND);
}

function build_filename(int $userId, string $title): string {
$safeTitle = preg_replace('~[^a-z0-9_-]+~i', '-', $title);
return DIR . '/data/' . $userId . '-' . $safeTitle . '.txt';
}

function save_note(string $filename, array $note): void {
if (!is_dir(dirname($filename))) {
mkdir(dirname($filename), 0777, true);
}

$payload = $note['title'] . "\n\n" . $note['body'];
file_put_contents($filename, $payload);
}
Функциональность максимально простая: создаем пользовательские заметки и сохраняем их в файл. На основе этого скрипта мы рассмотрим следующее:

  1. Как проверять суперглобальные переменные.
  2. Как отслеживать изменения переменных по ходу работы скрипта.
  3. Как изучать стек вызовов для нахождения точки входа.
Ставим брейкпоинт на строке с $action и делаем запрос на наш скрипт:

curl 'http://<ip>:8080/test.php?action=save&user=42' --data "title=note&body=hello"
После запроса VS Code автоматически подсветит, на какой строке остановился отладчик. На вкладке Variables можно детально изучить, что и в какой переменной сейчас находится, а еще эту информацию можно получить, просто встав курсором на нужную переменную: попробуй навести курсор на $action или $requestId и посмотреть, какие данные у них сейчас внутри.

Теперь перейдем в Call Tract. Убери брейкпоинт с $action и поставь внутри любой функции — например, на строку с созданием файла в file_put_contents. Опять выполни запрос и жди, когда сработает брейкпоинт. После этого обрати внимание на панель Call Stack, в которой можно видеть весь стек вызовов:

  1. Вызов test.php ({main}).
  2. Далее save_note().
  3. И уже внутри остановка на file_put_contents().
В настоящих плагинах WordPress это будет выглядеть примерно так же — ты сможешь изучать цепочку вызовов от точки входа, даже если это будет AJAX-обработчик или REST endpoint до конкретной функции, на которой будет стоять точка останова.


Плагины​

Сами плагины можно скачивать из Для просмотра ссылки Войди или Зарегистрируйся WordPress. Для начала багхантинга можно найти несколько не очень популярных плагинов и скачать их вручную, но если ты планируешь массово их изучать и искать баги, то лучше, конечно, скачать как можно больше плагинов за раз и исследовать их все.

Сделать это можно разными способами:

  1. Экспортировать плагины из Для просмотра ссылки Войди или Зарегистрируйся WordPress.
  2. Написать парсер для магазина и скачивать плагины по ключевым словам. Я же обычно скачиваю их напрямую из магазина, потому что там сразу можно найти описание и количество активных установок плагинов.
  3. Использовать сторонние инструменты или зеркала.
Я предпочитаю второй способ, поскольку это дает возможность получить данные о количестве активных установок, описание плагина и другую полезную информацию, которая, по моему мнению, важна для отбора кандидатов на исследование.

Плагины с небольшим количеством установок обычно получают меньше внимания со стороны авторов и ресерчеров, а значит, в таких плагинах обычно меньше конкуренции. Еще один плюс этих плагинов — простота кода, ведь обычно на старте авторы не заморачиваются и не используют сложные фреймворки, что делает исследование кода более простой задачей.


Скрипт для массового скачивания​

Как я уже говорил выше, я предпочитаю скачивать плагины прямо с официального сайта. К сожалению, недавно на сайте ввели ограничение по количеству запросов с одного IP-адреса, так что этот метод требует существенно больше времени, чем другие, но плюсы все еще перевешивают затраты времени.

Для скачивания я написал Для просмотра ссылки Войди или Зарегистрируйся, который по ключевым словам скачивает все плагины и создает CSV-файл, который можно использовать для отбора кандидатов на исследование. Задача скрипта простая:

  1. По ключевому слову пройти все страницы поиска в каталоге.
  2. Для каждого найденного плагина собрать название, короткое имя (slug), краткое описание, количество активных установок, ссылку на ZIP-файл и полное описание.
  3. Скачать все ZIP-файлы.
  4. Сохранить всё в CSV.
Внутри все реализовано на requests + BS и учитывает недавно введенные лимиты. Сам скрипт получился весьма объемный, поэтому в статье его разбора не будет, но код довольно простой, и при необходимости ты легко в нем разберешься.

Пример запуска скрипта:

python3 downloader.py
-q backup
-o backup_plugins.csv
--download
--download-dir ./download/
--workers 5
Тут ключ -q задает ключевую фразу для поиска плагинов, backup_plugins.csv — имя файла с результатами, ключ --download включает скачивание файлов, --download-dir указывает папку, в которую будут скачаны плагины, а --workers задает число потоков. Из‑за лимитов скачивание займет некоторое время. После завершения процесса появится CSV-файл, в котором будет вся информация о найденных плагинах.

www​

Для удобства работы с CSV-файлами в Visual Studio Code есть хорошее расширение Для просмотра ссылки Войди или Зарегистрируйся.
Для просмотра ссылки Войди или Зарегистрируйся

Отбор кандидатов​

Следующий шаг — сортировка и отбор плагинов. На этом этапе у нас уже есть готовый к тестированию WordPress, скачанные плагины и CSV с их описаниями.

Дальше можно отсортировать таблицу и выбрать плагины с наименьшим количеством установок, отфильтровать по ключевым словам и отбирать плагины по функциональности, исключить слишком тяжелые плагины, которые тянут за собой фреймворки и огромное количество абстракций, или просто исследовать все плагины подряд.

Несколько плагинов, конечно, можно исследовать руками, но, когда их сотни, а количество кода превышает миллионы строк, придется как‑то автоматизировать и облегчить проверки. Поэтому переходим к следующему этапу — Semgrep.


Semgrep​

Чтобы хоть как‑то упростить себе жизнь и не копаться в тоннах кода на PHP, нужно внедрить статический анализатор. Самое простое решение — это Для просмотра ссылки Войди или Зарегистрируйся: бесплатный SAST с открытым исходным кодом и нормальной документацией.

Для просмотра ссылки Войди или Зарегистрируйся
Устанавливается Semgrep из pip:

python3 -m pip install semgrep
Проверяем, что все установилось:

semgrep --version
Если команда отрабатывает без ошибок и показывает версию — можно смело двигаться дальше.

Полный синтаксис правил Semgrep достаточно объемный, но для старта нам хватит одного простого правила. Оно будет находить и подсвечивать любые вызовы file_put_contents в PHP-коде.

info​

У Semgrep есть удобная Для просмотра ссылки Войди или Зарегистрируйся для тестирования правил.
Создаем в текущей папке правило file_put_contents.yaml:

rules:
- id: php-file-put-contents
languages:
- php
severity: ERROR
message: Usage of file_put_contents detected
pattern: file_put_contents(...)
И запускаем:

semgrep --config file_put_contents.yaml wp/test.php
Если все хорошо, ты увидишь срабатывание Semgrep на определенной строке. Сейчас важна не столько сама функция, сколько понимание подхода: нужно писать правило под определенный уязвимый паттерн и прогонять его по всем анализируемым плагинам. Это помогает пачками находить однотипные баги во всех плагинах сразу, как, например, это делают многие исследователи из Patchstack с их Для просмотра ссылки Войди или Зарегистрируйся.


Запуск Semgrep на плагине WordPress​

Теперь попробуем применить этот подход к более реальному сценарию. Представим, что все плагины мы уже скачали и распаковали. Выбрали самые удобные для анализа, сделали список и загрузили один из них в WordPress для тестирования. Теперь нужно прогнать Semgrep по этому плагину:

cd wp/wp-content/plugins/my-plugin/

semgrep \
--config "<путь к нашему правилу>" \
--output="semgrep.sarif" \
--sarif \
--dataflow-traces \
"./"
Что тут происходит: --config указывает путь к правилу, наподобие того, что я писал выше, --sarif заставляет записывать результаты в формате SARIF (о нем ниже), --output перенаправляет вывод в файл, --dataflow-traces добавляет в отчет информацию о трассировке данных, если это описано в правиле, а ./ – это директория плагина, чтобы Semgrep знал, что сканировать.

После окончания сканирования у нас останется файл semgrep.sarif со всеми находками.

Зачем сканировать таким способом​

Зачем страдать с Semgrep, если все уязвимости невозможно охватить статическим анализом? Лично мне больше всего нравится возможность на потоке находить однотипные баги сразу во всех плагинах. Еще удобно таким образом искать места, где вызываются определенные функции — будь то встроенные PHP-функции или же функции ядра WordPress, и анализировать логику сразу вокруг этих вызовов. То есть не нужно вычитывать код на предмет опасных вызовов — это сделает за нас Semgrep.
Да, в этой статье не будет каких‑то волшебных правил, которые можно запустить и получить сотню 0-day-уязвимостей (а если бы такие правила были, я бы уже запустил и отрепортил эти уязвимости сам, будь уверен). Вместо этого мы поговорим про функции, вокруг которых часто попадаются баги.

Формат SARIF​

Для более удобной навигации по потенциально уязвимым местам лучше всего подходит файл SARIF. В связке с расширением для VS Code он максимально облегчает анализ, ради которого теперь отвлекаться нужно минимально.

SARIF (static analysis results interchange format) представляет собой основанный на JSON формат для обмена результатами статического анализа. По сути это обычный JSON, в котором описаны инструменты анализа, перечислены срабатывания, указаны файлы и позиции в коде, а еще при возможности добавлены трассировки потоков данных в файлах.

Идея формата простая и лежит на поверхности: разные анализаторы могут писать результаты в одном и том же формате, который дальше можно использовать и обрабатывать общими инструментами.

info​

Разбор самого формата SARIF выходит за рамки этой статьи, но, если тебе интересно разобраться глубже, можешь почитать Для просмотра ссылки Войди или Зарегистрируйся в блоге PVS-Studio или хотя бы посмотреть Для просмотра ссылки Войди или Зарегистрируйся на GitHub.
Для повседневной работы с SARIF существует на самом деле немного программ и плагинов, но в VS Code есть достойные. Мне больше всего нравится Для просмотра ссылки Войди или Зарегистрируйся от Microsoft. После его установки VS Code начнет автоматически узнавать .sarif-файлы и открывать по ним отдельную панель с результатами анализа.

Чтобы лучше разобраться с SARIF, сделаем второе правило Semgrep, которое будет подсвечивать наиболее опасные функции ядра PHP. Создаем и сохраняем:

rules:
- id: php-core-dangerous-functions
languages:
PHP:
    severity: ERROR
    message: "PHP Danger"
    pattern-either:
      # Code execution
      - pattern: eval(...)
      - pattern: assert(...)
      - pattern: create_function(...)

      # System commands
      - pattern: system(...)
      - pattern: exec(...)
      - pattern: shell_exec(...)
      - pattern: passthru(...)
      - pattern: popen(...)
      - pattern: proc_open(...)

      # Dynamic include
      - pattern: include(...)
      - pattern: include_once(...)
      - pattern: require(...)
      - pattern: require_once(...)

      # Deserialization
      - pattern: unserialize(...)

      # File write
      - pattern: file_put_contents(...)
      - pattern: move_uploaded_file(...)
      - pattern: unlink(...)
      - pattern: rename(...)
      - pattern: mkdir(...)
      - pattern: rmdir(...)
      - pattern: copy(...)

      # File read
      - pattern: file_get_contents(...)
      - pattern: readfile(...)
      - pattern: highlight_file(...)
      - pattern: show_source(...)
Теперь берем любой плагин (я взял [URL='https://wordpress.org/plugins/preferred-languages/']Preferred Languages[/URL], но это не критично), устанавливаем его в WordPress и запускаем Semgrep прямо в папке с плагином:

cd wp/wp-content/plugins/preferred-languages/

semgrep \
  --config "<путь к нашему второму правилу>" \
  --output="semgrep.sarif" \
  --sarif \
  --dataflow-traces \
  "./"
Проверяем, что файл semgrep.sarif появился, и открываем его в VS Code.

При открытии файла плагин автоматически понимает, что это SARIF, и открывает новое окно, в котором видны все записи из файла. Нажимай на любую строку, и тебя будет перекидывать на точное место, которое обнаружил Semgrep, а тебе остается только переключать места, анализировать и проверять.

 
[HEADING=1]Основные паттерны[/HEADING]
Чтобы завершить методологию поиска, необходимо упомянуть о типичных местах, где прячутся уязвимости. Кроме стандартных функций PHP, следует знать [URL='https://wordpress.org/documentation/']документацию[/URL] на ядро WordPress и четко уяснить механизмы работы шаблонов, встроенных фильтраций, плагинов и вспомогательных функций. Поскольку WordPress действительно большой и его API весьма обширный, рекомендую написать свой простой плагин, который будет работать с базой, AJAX-хуками и ядром, в частности с wp_options и подобным.

[HEADING=3]info[/HEADING]
Статья предполагает, что читатель уже знает основные уязвимые функции PHP и может легко ознакомиться с остальными. Если это не так, то вначале рекомендую изучить базовые уязвимости PHP.
Для полноты повествования начнем с базы — классических уязвимостей PHP. Перечислять все возможные варианты я не буду, да и в этом нет никакого смысла, потому что их бесконечное количество. Главное, что я пытаюсь показать, — что вообще ты можешь встретить, изучая плагины.

 
[HEADING=2]LFI[/HEADING]
Самое простое место, где можно допустить ошибку, — работа с файлами. Если плагин не фильтрует входные данные от пользователя при работе с файлами, то это в большинстве случаев приводит к уязвимостям, например к LFI:

<?php

if (!defined('ABSPATH')) {
    exit;
}

add_action('init', function () {
    if (empty($_GET['ifd_include'])) {
        return;
    }

    // Нет фильтрации, прямое использование пользовательского ввода
    $file = $_GET['ifd_include'];
    $base_dir = plugin_dir_path([B]FILE[/B]);
    $full     = $base_dir . $file;

    if (file_exists($full)) {
        include $full;
    } else {
        echo "File not found";
    }
});
Это пример самого вероятного поведения разработчика, и такое часто можно встретить в панели администратора, когда нужно подгрузить шаблон, или же в плагинах, где используются разные стили, при рендеринге пользовательского контента. Конечно, большинство разработчиков не пишут настолько очевидно уязвимый код, как описан выше, и обычно все выглядит, как в примере ниже:

include "template-calc-" . $full . ".php";
Это сильно ограничивает эксплуатацию, но это все еще баг, который можно репортить со спокойной душой.

 
[HEADING=2]Arbitrary file upload[/HEADING]
Загрузка произвольных файлов тоже встречается довольно часто, особенно от авторизованного пользователя. Уязвимость обычно выглядит как‑то так:

add_shortcode('ifd_upload_form', function () {
    ob_start();
    ?>
    <form method="post" enctype="multipart/form-data">
        <p><input type="file" name="ifd_file"></p>
        <p><button type="submit" name="ifd_upload" value="1">Upload</button></p>
    </form>
    <?php
    return ob_get_clean();
});

add_action('init', function () {
    if (empty($_POST['ifd_upload']) || empty($_FILES['ifd_file'])) {
        return;
    }

    $upload_dir = wp_upload_dir();
    $target_dir = trailingslashit($upload_dir['basedir']);

    // Берем оригинальное имя файла как есть
    $target_name = basename($_FILES['ifd_file']['name']);
    $target_path = $target_dir . $target_name;

    // Нет проверки расширения/MIME/размера/содержимого
    if (move_uploaded_file($_FILES['ifd_file']['tmp_name'], $target_path)) {
        echo "File uploaded to: " . esc_html($target_path);
    } else {
        echo "Upload failed";
    }
});
Чтобы найти уязвимость в таком месте, нужно смотреть на наличие фильтрации до вызова move_uploaded_file. Если ее нет — тут, скорее всего, есть уязвимость. Дополнительно отмечу, что даже если эта уязвимость будет доступна только от имени администратора, то ее тоже примут и разработчик будет обязан ее пофиксить.

 
[HEADING=2]Переименование файла[/HEADING]
Еще одна интересная уязвимость — когда плагин меняет имя файла без проверки входных данных.

add_action('init', function () {
    if (empty($_GET['ifd_rename_from']) || empty($_GET['ifd_rename_to'])) {
        return;
    }

    // Абсолютные пути собираются из пользовательского ввода
    $from = ABSPATH . $_GET['ifd_rename_from'];
    $to   = ABSPATH . $_GET['ifd_rename_to'] . ".jpg";

    if (@rename($from, $to)) {
        echo "Renamed $from to $to";
    } else {
        echo "Rename failed";
    }
});
Казалось бы, в чем уязвимость? Обычно меняются названия фото или документов, но если разработчик забыл проверить исходный файл, то один из популярных векторов — это переименование файла wp-config.php, что позволяет заново пройти процесс настройки инстанса WordPress и в итоге дает полный захват сайта.

 
[HEADING=2]RCE[/HEADING]
Следующая ступень после работы с файлами — выполнение кода или команд. Чаще всего подобные уязвимости встречаются в плагинах для бэкапов или работы с файловой системой. Любые возможности, которые автор реализовал через system, exec или eval, потенциально уязвимы. Простой пример:

add_action('admin_init', function () {
    if (empty($_GET['ifd_run_cmd'])) {
        return;
    }

    $file = $_GET['ifd_run_cmd'];

    echo "<pre>";
    // Полноценная RCE
    system("tar czvf backup.tar.xz" . $file);
    echo "</pre>";

    exit;
});
Как видно, тут system используется напрямую и никаких фильтров нет. Это позволяет дописать любую команду в запрос, и она будет выполнена в системе от имени пользователя, от которого запущен веб‑сервер (обычно www-data).

 
[HEADING=2]Unsafe deserialization[/HEADING]
Отдельный и самый сложный, на мой взгляд, класс проблем — небезопасная десериализация. В WordPress часто сохраняют сложные пользовательские структуры через serialize()/unserialize() — это могут быть настройки, кеш и подобное. Если в unserialize попадет строка, которую может контролировать пользователь, то это может привести к многим уязвимостям, включая RCE. К счастью админов и несчастью хакеров, в последних версиях WordPress в ядре нет цепочек, ведущих к критическим багам. Впрочем, наличие валидной цепочки необязательно, важно показать саму возможность вызвать unserialize с контролируемыми данными, и баг примут.

Простой пример:

add_action('admin_post_ifd_save_settings', function () {
    if (empty($_POST['ifd_settings'])) {
        return;
    }

    $raw = wp_unslash($_POST['ifd_settings']);
    $settings = @unserialize($raw);

    if (!is_array($settings)) {
        echo "Invalid settings";
        return;
    }

    update_option('ifd_insecure_settings', $settings);
    echo "Settings saved";
});
Как видно, пользовательский ввод подается напрямую в unserialize. Но перед этим делается wp_unslash, на что стоит обратить внимание, ведь в стандартной конфигурации WordPress сам экранирует все кавычки. Учитывай это, особенно если нашел уязвимую цепочку, а она не работает.

 
[HEADING=2]SQL-инъекции[/HEADING]
Давай еще рассмотрим уязвимости, специфичные для ядра WordPress. Почему SQL-инъекции относятся к специфичным уязвимостям ядра? Потому что у WordPress есть свой ORM, который требует использования prepare для каждого запроса. Гарантированно избежать инъекции можно, только используя prepare, но многие все еще забывают это делать. Найти такое довольно просто, потому что большая часть запросов в базу обычно использует пользовательские данные.

Пример:

<?php

if (!defined('ABSPATH')) {
    exit;
}

add_action('init', function () {
    if (empty($_GET['ifd_post_id'])) {
        return;
    }

    global $wpdb;

    $post_id = $_GET['ifd_post_id'];

    $table = $wpdb->posts;
    $sql   = "SELECT * FROM {$table} WHERE ID = $post_id";

    $rows = $wpdb->get_results($sql);

    foreach ($rows as $row) {
        echo esc_html($row->post_title) . "<br>";
    }
});
Как видно, никакой фильтрации в запросе нет, а должна она выглядеть примерно так:

$sql = $wpdb->prepare(
    "SELECT * FROM {$table} WHERE ID = %d",
    $post_id
);
 
[HEADING=2]Update Option[/HEADING]
Еще одна интересная уязвимость, которая при успешной эксплуатации позволяет менять почти любые настройки ядра в базе, включая системные, — это Update Option. Ее магия заключается в функции update_option, которая находится в ядре WordPress и обновляет настройки, в том числе настройки плагинов, в базе данных. Вот пример ее небезопасного использования:

add_action('admin_post_ifd_save_option', function () {
    if (empty($_POST['ifd_option_name']) || !isset($_POST['ifd_option_value'])) {
        return;
    }

    $name  = $_POST['ifd_option_name'];
    $value = $_POST['ifd_option_value'];

    update_option($name, $value);

    wp_redirect(admin_url('options-general.php?page=ifd-settings&updated=1'));
    exit;
});
Этот код изменяет значение произвольного параметра в глобальных настройках для всех плагинов — можно, например, изменить ключ для приема платежей в Stripe. К каким последствиям это может привести, рассказывать излишне. В норме в таких функциях должна быть проверка nonce и current_user_can(...), о которых мы сейчас еще подробно поговорим.

 
[HEADING=2]AJAX и REST без проверки прав[/HEADING]
Когда разработчик реализует бэкенд для AJAX, он часто думает, что только админ сможет отправить этот запрос, а обычный пользователь не знает всех нужных параметров. Этот подход к разработке называется Security Through Obscurity (безопасность через неясность), и за него обычно ломают руки выглядит пример уязвимого кода вот так:

// Регистрируем AJAX-хендлер
add_action('wp_ajax_ifd_delete_post', 'ifd_delete_post_callback');

function ifd_delete_post_callback() {
    if (empty($_POST['post_id'])) {
        wp_send_json_error('Missing post_id');
    }

    $post_id = (int) $_POST['post_id'];

    wp_delete_post($post_id, true);

    wp_send_json_success('Deleted');
}
Чтобы удалить любой пост, просто берем post_id и вызываем wp_delete_post. Удобно, что никаких проверок тут нет и сделать это мы сможем безо всякой авторизации. Безопасный код должен выглядеть так:

add_action('wp_ajax_ifd_delete_post', 'ifd_delete_post_callback');

function ifd_delete_post_callback() {
    if ( ! isset($_POST['nonce']) || ! check_ajax_referer('ifd_delete_post', 'nonce', false) ) {
        wp_send_json_error(['message' => 'Bad nonce'], 400);
    }

    if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
        wp_send_json_error(['message' => 'Invalid method'], 405);
    }

    if ( empty($_POST['post_id']) ) {
        wp_send_json_error(['message' => 'Missing post_id'], 400);
    }

    $post_id = (int) $_POST['post_id'];

    if ( ! current_user_can('delete_post', $post_id) ) {
        wp_send_json_error(['message' => 'Insufficient permissions'], 403);
    }

    $deleted = wp_delete_post($post_id, true);

    if ( ! $deleted ) {
        wp_send_json_error(['message' => 'Delete failed'], 500);
    }

    wp_send_json_success(['message' => 'Deleted', 'post_id' => $post_id]);
}
REST-эндпоинты без проверки прав относятся сюда же:

add_action('rest_api_init', function () {
    register_rest_route('ifd/v1', '/reset/', [
        'methods'  => 'POST',
        'callback' => 'ifd_reset_settings',


        'permission_callback' => '__return_true', <--- Тут проблема
    ]);
});

function ifd_reset_settings(\WP_REST_Request $request) {
    delete_option('ifd_insecure_settings');
    return new \WP_REST_Response(['status' => 'ok']);
}
В примере функция проверки прав просто возвращает true, что дает любому пользователю право выполнять действия в этой функции.

 
[HEADING=2]PHP-код без проверки среды[/HEADING]
Отдельный, хотя и редкий тип уязвимости — отсутствие проверки среды выполнения скрипта. В хорошем сценарии весь код должен вызываться только из среды WordPress, но есть скрипты, которые можно вызывать и напрямую, по прямому URL:

<?php
$log = $_GET['log'] ?? '';

$base_dir = [B]DIR[/B] . '/logs/';
$path     = $base_dir . $log;

if ($log === '') {
    echo "Missing log parameter";
    exit;
}

if (file_exists($path)) {
    unlink($path);
    echo "Deleted: " . htmlspecialchars($path, ENT_QUOTES, 'UTF-8');
} else {
    echo "Not found";
}
Скрипт выше предназначен для удаления файлов. В нем нет никаких проверок, и, если атакующий найдет этот файл и откроет его напрямую, он сможет наделать много нехорошего:

https://example.com/wp-content/plugins/test/tools/delete-log.php?log=error.log
Исправленная версия включает в себя проверку, не запущен ли сценарий напрямую:

<?php
if ( ! defined('ABSPATH') ) {
    exit; // Не даем выполнять файл напрямую
}

if ( ! current_user_can('manage_options') ) {
    wp_die('Insufficient permissions');
}

check_admin_referer('ifd_delete_log');

$log = isset($_GET['log']) ? sanitize_file_name($_GET['log']) : '';

$base_dir = plugin_dir_path([B]FILE[/B]) . 'logs/';
$path     = $base_dir . $log;
[HEADING=3]Методология[/HEADING]
Если подвести итог сказанному выше, план поиска уязвимостей выглядит так:
[LIST=1]
[*]Скачиваем как можно больше плагинов.
[*]Выбираем из них самые интересные.
[*]Готовим правила для Semgrep — тут придется поработать мозгами и проявить смекалку.
[*]Сканируем.
[*]Фильтруем результаты — удаляем пустые файлы и мусорные находки.
[*]Детально изучаем руками конкретные уязвимые места.
[/LIST]
Если ты выбрал разбирать конкретный плагин полностью, Semgrep придется отложить в сторонку и ковыряться руками и отладчиком. В таком сценарии лучше обращать внимание на то, какие действия доступны пользователям и администратору, какие доступны эндпоинты, используется ли сторонний код и так далее. Вот мой чек‑лист особо интересных мест:
[LIST]
[*]Файловые операции: file_put_contents, rename, fopen, unlink, rename и другие.
[*]Работа с базой данных: $wpdb->query, $wpdb->get_results, $wpdb->prepare и так далее.
[*]Функции ядра: update_options, update_user_meta.
[*]Десериализация.
[*]AJAX и REST, проверка авторизации и прав внутри вызовов.
[*]Сетевые запросы, если имеются: wp_remote_get и подобные.
[*]Вывод данных в HTML и JavaScript, что потенциально может давать XSS без фильтрации.
[/LIST]
 
[HEADING=1]Оформление отчета[/HEADING]
Если ты нашел уязвимость — писать стоит не авторам, а специализированным компаниям. На WordPress специализируются две основные:

[LIST]
[*][URL='https://www.wordfence.com/']WordFence[/URL] — я работаю с ними, они хоть и довольно медленные, но надежные;
[*][URL='https://patchstack.com/']Patchstack[/URL] — с ними я не работал, но, судя по их CVE, работают они не очень.
[/LIST]
Выбор только за тобой. Главное — не забудь при оформлении бага расписать все детально и приложить скриншоты. Нужно обрисовать полный путь к уязвимости и показать простой PoC — достаточно всего лишь запрос из Burp Suite.

Рассмотрение уязвимостей может занимать разное время вплоть до трех месяцев (хотя мой рекорд был всего два). Помни, что чем менее популярен плагин (меньшее число установок), тем дольше будет время ожидания.

 
[HEADING=1]Заключение[/HEADING]
Поиск багов легче, чем кажется. Я считаю, что каждый должен внести свой вклад в безопасность и найти парочку багов. Мир вокруг станет лучше, а тебе дадут ачивку в виде CVE. Для успеха важно построить процесс: регулярно искать баги, аккуратно их оформлять и параллельно наращивать насмотренность — тогда результаты начинают появляться более стабильно, а наметанный глаз поможет справляться быстрее. У меня есть уже шесть CVE и еще столько же на рассмотрении. Удачи!
 
Activity
So far there's no one here
Сверху Снизу