errorlogs.net /WordPress Brute Force & xmlrpc
xmlrpc.php multicall
XML-RPC Attack Patterns

xmlrpc.php Log Signatures

# Simple xmlrpc probe — checking if endpoint exists
91.108.4.12 - - [10/Apr/2025:01:00:00 +0000] "GET /xmlrpc.php HTTP/1.1" 405 42
# 405 Method Not Allowed = xmlrpc.php exists but won't accept GET. Attacker confirmed endpoint.

# system.multicall attack — one POST = hundreds of password attempts
91.108.4.12 - - [10/Apr/2025:01:00:01 +0000] "POST /xmlrpc.php HTTP/1.1" 200 97
91.108.4.12 - - [10/Apr/2025:01:00:02 +0000] "POST /xmlrpc.php HTTP/1.1" 200 97
# 200 + tiny response (97 bytes) = all attempts in multicall returned faultCode (all failed)
# 200 + larger response = at least one credential pair succeeded

# High-volume xmlrpc from distributed IPs (botnet)
185.220.101.45 - - [10/Apr/2025:01:00:03 +0000] "POST /xmlrpc.php HTTP/1.1" 200 97
185.107.213.1  - - [10/Apr/2025:01:00:04 +0000] "POST /xmlrpc.php HTTP/1.1" 200 97
45.83.64.200   - - [10/Apr/2025:01:00:05 +0000] "POST /xmlrpc.php HTTP/1.1" 200 97
⚠ The Multicall Trick
A single POST to xmlrpc.php using system.multicall can contain 500–1000 wp.getUsersBlogs calls, each testing a different password. To your log, this looks like one request. Your rate limiter sees one hit. The attacker tests 1000 passwords. Always block xmlrpc.php entirely if you don't use it.
wp-login.php Status Codes Decoded
MethodStatusMeaning
GET /wp-login.php200Login form served. Normal.
POST /wp-login.php200Login failed — form re-displayed with error. Brute force in progress.
POST /wp-login.php302Login succeeded — redirecting to wp-admin. Investigate the source IP immediately.
GET /wp-login.php?action=lostpassword200Password reset form. May indicate account enumeration (testing if username exists).
POST /wp-login.php?action=lostpassword302Password reset submitted. Monitor for unusual timing with login attempts.
Detection & Blocking
bash
LOG=/var/log/nginx/access.log

# Count POST xmlrpc.php by IP
grep "POST /xmlrpc.php" $LOG | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# Any xmlrpc.php POST that returned more than 200 bytes (possible success)
awk '$7=="/xmlrpc.php" && $6=="\"POST" && $10+0 > 200' $LOG

# wp-login brute force: IPs with > 10 failed POST attempts (200 response)
awk '$7=="/wp-login.php" && $6=="\"POST" && $9=="200" {print $1}' $LOG | sort | uniq -c | sort -rn

# Did any login succeed? (POST → 302)
grep "POST /wp-login.php" $LOG | awk '$9=="302" {print $1, $4}'

Block xmlrpc.php — Nginx

nginx.conf
# Add inside server {} block
location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
    return 444;
}

# Also rate-limit wp-login.php
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=3r/m;
location = /wp-login.php {
    limit_req zone=wplogin burst=5 nodelay;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Block xmlrpc.php — Apache

.htaccess
# Block xmlrpc.php entirely
<Files xmlrpc.php>
    Order Deny,Allow
    Deny from all
</Files>