В эко­сис­теме 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 https://wordpress.org/latest.zip
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. Эк­спор­тировать пла­гины из SVN-репози­тория 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 есть хорошее рас­ширение Excel Viewer.

Список плагинов по ключевому слову Backup
Спи­сок пла­гинов по клю­чево­му сло­ву Backup

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии