xmlrpc.php: дыра, которая открыта на 80% WordPress-сайтов прямо сейчас.
Воскресенье, 2 часа ночи. Сервер с WordPress-магазином начинает тормозить. PHP-FPM упирается в лимит воркеров, CPU под 100%, сайт не отвечает. Смотришь access.log — и видишь это:
185.220.101.42 - - [26/May/2026:02:13:44 +0300] "POST /xmlrpc.php HTTP/1.1" 200 674
185.220.101.43 - - [26/May/2026:02:13:44 +0300] "POST /xmlrpc.php HTTP/1.1" 200 674
185.220.101.44 - - [26/May/2026:02:13:44 +0300] "POST /xmlrpc.php HTTP/1.1" 200 674
185.220.101.45 - - [26/May/2026:02:13:44 +0300] "POST /xmlrpc.php HTTP/1.1" 200 674
Тысячи строк в секунду. Разные IP, одна цель. Это не DDoS на полосу пропускания — это атака на xmlrpc.php, и сервер послушно обрабатывает каждый запрос, тратя CPU на PHP.
Если ты администрируешь WordPress-сайты и ещё не закрыл xmlrpc.php — читай дальше.
ЧТО ТАКОЕ xmlrpc.php И ЗАЧЕМ ОН ВООБЩЕ СУЩЕСТВУЕТ
XML-RPC — это протокол удалённого вызова процедур поверх HTTP, появился ещё в 1998 году. WordPress добавил его для того, чтобы сторонние приложения могли управлять сайтом без браузера: публиковать посты из Windows Live Writer, синхронизировать контент через мобильные приложения, интегрироваться с внешними сервисами.
Запрос через xmlrpc.php выглядит так:
POST /xmlrpc.php HTTP/1.1
Host: example.com
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>password123</value></param>
</params>
</methodCall>
WordPress отвечает — и при верных credentials — возвращает полный доступ к сайту.
Проверить, открыт ли xmlrpc.php на твоём сайте, просто:
curl -s https://example.com/xmlrpc.php
Если в ответе видишь:
XML-RPC server accepts POST requests only.
— поздравляю, xmlrpc.php активен и доступен снаружи. Именно этот ответ боты используют как разведку.
ТРИ СПОСОБА СЛОМАТЬ САЙТ ЧЕРЕЗ xmlrpc.php
1. Брутфорс с амплификацией через system.multicall
Классический брутфорс на wp-login.php легко блокировать — один запрос, одна попытка. С xmlrpc.php всё интереснее: метод system.multicall позволяет упаковать несколько вызовов в один HTTP-запрос.
До WordPress 4.4 один запрос мог содержать тысячи попыток подобрать пароль. После патча в декабре 2015 года WordPress прерывает пакет после первой неудачной аутентификации — но это не значит, что атака исчезла. Боты просто адаптировались: теперь они шлют сотни последовательных запросов с разных IP из ботнета, обходя IP-based rate limiting.
Payload выглядит так:
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param><value><array><data>
<value><struct>
<member>
<name>methodName</name>
<value>wp.getUsersBlogs</value>
</member>
<member>
<name>params</name>
<value><array><data>
<value>admin</value>
<value>password1</value>
</data></array></value>
</member>
</struct></value>
<!-- ещё 499 попыток -->
</data></array></value></param>
</params>
</methodCall>
Итог: каждый запрос — это не одна попытка, а десятки. PHP воркер занят на всё это время. Даже с патчем WordPress — нагрузка на сервер кратно выше, чем от атаки на wp-login.php.
2. DDoS-амплификация через pingback
Это атака на чужие сайты с твоего сервера. Злоумышленник отправляет на твой (уязвимый) WordPress запрос:
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://ЖЕРТВА.com/</string></value></param>
<param><value><string>https://твой-сайт.com/любой-пост/</string></value></param>
</params>
</methodCall>
Твой WordPress послушно делает HTTP-запрос к сайту жертвы. Если таких WordPress-сайтов тысяча — жертва получает тысячу запросов одновременно. Твой сервер становится участником DDoS-атаки, и твой IP попадает в блэклисты.
Именно так была атакована сеть сайтов DoD (Министерства обороны США) — через незакрытый xmlrpc.php на их же WordPress-ресурсах.
3. Сканирование портов внутренней сети (SSRF через pingback)
Pingback принимает произвольный URL, включая внутренние адреса. Атакующий может сканировать внутреннюю инфраструктуру через твой сервер:
<!-- попытка достучаться до внутренних сервисов -->
<value><string>http://192.168.1.1:8080/</string></value>
<value><string>http://10.0.0.1:3306/</string></value>
По разнице во времени ответа и коду ошибки можно понять, какие порты открыты внутри.
КАК ЗАКРЫТЬ: ТРИ УРОВНЯ ЗАЩИТЫ
Уровень 1: nginx — блокировать на уровне веб-сервера
Это самый быстрый и эффективный способ. Запрос не доходит до PHP вообще.
Если xmlrpc.php не нужен совсем (большинство случаев):
# В блоке server{} твоего nginx-конфига
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
Если нужен доступ только с конкретных IP (например, Jetpack или WooCommerce мобильное приложение):
location = /xmlrpc.php {
allow 198.51.100.10; # IP твоего мобильного приложения
allow 198.51.100.11; # IP Jetpack servers
deny all;
access_log off;
}
После изменения — проверить конфиг и перезагрузить:
nginx -t && systemctl reload nginx
Проверить что блокировка работает:
curl -I https://example.com/xmlrpc.php
# Ожидаемый ответ: HTTP/1.1 403 Forbidden
Уровень 2: fail2ban — банить тех, кто всё равно пытается
Даже с блокировкой в nginx — боты продолжат стучать, получать 403 и уходить. Но стучать они будут постоянно, засоряя логи. fail2ban добавит их в чёрный список на уровне nftables.
Создать фильтр:
cat > /etc/fail2ban/filter.d/nginx-xmlrpc.conf << 'EOF'
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST) /xmlrpc\.php HTTP/.*" (200|301|302|403|404) \d+ ".*" ".*"
ignoreregex =
EOF
Убедиться, что fail2ban использует nftables как backend (по умолчанию на современных системах это уже так, но лучше проверить):
grep -r "banaction" /etc/fail2ban/jail.conf /etc/fail2ban/jail.local 2>/dev/null | grep -v "^#"
# Должно быть: banaction = nftables-multiport
# Если там iptables — добавь в /etc/fail2ban/jail.local:
# [DEFAULT]
# banaction = nftables-multiport
Создать jail:
cat > /etc/fail2ban/jail.d/nginx-xmlrpc.conf << 'EOF'
[nginx-xmlrpc]
enabled = true port = http,https filter = nginx-xmlrpc logpath = /var/log/nginx/access.log maxretry = 3 findtime = 60 bantime = 86400 action = nftables-multiport[name=xmlrpc, port=»80,443″, protocol=tcp] EOF
Перезапустить fail2ban:
systemctl restart fail2ban
fail2ban-client status nginx-xmlrpc
Уровень 3: Cloudflare WAF (если сайт за Cloudflare)
Если сайт идёт через Cloudflare, настроить Rate Limiting Rule в дашборде:
Security → WAF → Rate limiting rules → Create rule:
Field: URI Path
Operator: contains
Value: /xmlrpc.php
Action: Block
Duration: 1 hour
Threshold: 5 requests per 30 seconds per IP
На Pro и выше доступно managed WAF-правило WP0018, которое автоматически детектирует system.multicall паттерны.
Важный нюанс: если сайт за Cloudflare, в nginx access.log ты видишь IP Cloudflare, а не реальный IP атакующего. Убедись что в nginx настроен real_ip:
# В nginx.conf, блок http{}
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;
А ЕСЛИ xmlrpc.php НУЖЕН (Jetpack, WooCommerce Mobile)
Jetpack до сих пор использует XML-RPC для связи с WordPress.com. Если отключить полностью — Jetpack сломается. Правильный подход:
- Получить актуальные IP-диапазоны Jetpack и разрешить только их. IP меняются — официальная документация Jetpack явно предупреждает об этом и рекомендует автоматизировать обновление. Актуальный список всегда доступен машиночитаемым образом:
# Посмотреть текущие IP-диапазоны Jetpack
curl -s https://jetpack.com/ips-v4.json | python3 -m json.tool
# Пример конфига на основе актуального списка (jetpack.com/support/how-to-add-jetpack-ips-allowlist)
location = /xmlrpc.php {
# Актуальные Jetpack IP ranges — проверяй на jetpack.com/ips-v4.json
allow 122.248.245.244/32;
allow 54.217.201.243/32;
allow 54.232.116.4/32;
allow 192.0.64.0/18;
allow 192.0.80.0/20;
allow 192.0.96.0/20;
allow 192.0.112.0/20;
allow 195.234.108.0/22;
deny all;
}
Важно: этот список меняется. Не прописывай IP вручную раз и навсегда — лучше автоматизировать обновление через systemd timer или cron.
- Добавить rate limiting даже для разрешённых IP:
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=5r/m;
location = /xmlrpc.php {
limit_req zone=xmlrpc burst=3 nodelay;
allow 192.0.64.0/18;
# ... остальные Jetpack диапазоны
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
ДИАГНОСТИКА: КАК ПОНЯТЬ ЧТО ТЫ УЖЕ ПОД АТАКОЙ
Проверить количество запросов к xmlrpc.php за последний час:
grep "xmlrpc.php" /var/log/nginx/access.log | \
awk '{print $1}' | \
sort | uniq -c | sort -rn | head -20
Посмотреть топ атакующих IP:
grep "POST /xmlrpc.php" /var/log/nginx/access.log | \
awk '{print $1}' | \
sort | uniq -c | sort -rn | head -20
Проверить нагрузку от PHP-FPM:
systemctl status php8.3-fpm
# Или напрямую:
ps aux | grep php-fpm | wc -l
Если видишь десятки PHP-воркеров при минимальном трафике — xmlrpc.php скорее всего уже нагружает сервер.
ТИПИЧНЫЕ ОШИБКИ
Ошибка 1: «Я поставил плагин Disable XML-RPC — теперь безопасно»
Плагин отключает XML-RPC на уровне WordPress, но запрос всё равно доходит до PHP, PHP инициализирует WordPress, подключает плагины, и только потом возвращает пустой ответ. Сервер всё равно тратит ресурсы. Правильный способ — блокировка в nginx до PHP.
Ошибка 2: «У меня нет WordPress, только PHP-приложение»
xmlrpc.php специфичен для WordPress. Если WordPress нет — проблемы нет. Но проверить стоит: иногда WordPress устанавливают в подкаталог и забывают.
Ошибка 3: «Блокирую по IP — атакующих много, не успеваю»
IP-based блокировка не работает против распределённых ботнетов — IP меняются постоянно. Правильный подход: блокировать endpoint целиком (location = /xmlrpc.php { deny all; }), а не пытаться угнаться за IP.
Ошибка 4: «fail2ban забанил, но в логах всё равно появляются запросы»
fail2ban работает реактивно — сначала запрос обрабатывается, потом IP банится. Если атака распределённая (каждый IP делает по 2 запроса), fail2ban не поможет. Решение — комбинация nginx deny all для endpoint + Cloudflare WAF на верхнем уровне.
ПРОВЕРЬ СВОИ САЙТЫ ПРЯМО СЕЙЧАС
# Быстрая проверка — открыт ли xmlrpc.php
curl -s -o /dev/null -w "%{http_code}" https://твой-сайт.com/xmlrpc.php
# 200 или "XML-RPC server accepts POST requests only" = открыт, надо закрыть
# 403 = закрыт nginx
# 404 = файл не найден (WordPress не установлен или путь другой)
# Проверить все WordPress-сайты на сервере
for conf in /etc/nginx/sites-enabled/*; do
domain=$(grep server_name "$conf" | head -1 | awk '{print $2}' | tr -d ';')
code=$(curl -s -o /dev/null -w "%{http_code}" "https://$domain/xmlrpc.php" 2>/dev/null)
echo "$domain: $code"
done
ИТОГ
xmlrpc.php — это легаси из 1998 года, который WordPress тащит за собой до сих пор. В 2026 году подавляющему большинству сайтов он не нужен. Но он включён по умолчанию, доступен снаружи, и боты его знают.
Три минуты в nginx-конфиге закрывают проблему полностью:
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
Если нужен Jetpack — белый список IP вместо полного запрета. Сверху — fail2ban для логирования и бана тех, кто всё равно стучит. И Cloudflare WAF если сайт за проксированием.
Проверь свои сайты прямо сейчас. Скорее всего, хотя бы один xmlrpc.php открыт.
