How I Traced 100,000 Failed Logins to a Single xmlrpc.php Exploit — And Shut It Down in Minutes
· 12 min read
A client messaged me on a Monday morning: "the site feels sluggish." Their WooCommerce store was averaging 1.8-second page loads the week before. Now it was pushing 9 seconds. CPU on the VPS was sitting at 94%.
I SSH'd in and checked the Nginx access log. What I expected to see was a traffic spike or a runaway cron job. What I actually saw was this:
grep xmlrpc.php /var/log/nginx/access.log | wc -l
147,832 requests to xmlrpc.php in the past six hours.
The site wasn't slow because of traffic. It was under active brute force attack — but not through the login page. The attacker was using a technique that most WordPress security plugins completely miss.
What xmlrpc.php Actually Is
XML-RPC is a remote procedure call protocol that's been part of WordPress since version 2.6. It was the original way to publish posts from external apps, manage comments remotely, and handle pingbacks between blogs.
WordPress 4.7 (December 2016) introduced the REST API, which does everything XML-RPC does but better. The REST API is what the WordPress mobile app, Gutenberg, and most modern integrations use today.
Despite being obsolete for nearly a decade, xmlrpc.php is still bundled with every WordPress install and enabled by default. It sits in the root directory, accepts unauthenticated POST requests, and on most sites nobody ever looks at it.
Attackers know this.
How system.multicall Turns One Request Into 500 Login Attempts
A normal brute force attack against wp-login.php is noisy. Each password guess is a separate HTTP request. Security plugins like Wordfence or Limit Login Attempts count those requests and lock out the IP after a few failures.
XML-RPC has a method called system.multicall that lets you bundle multiple method calls into a single HTTP request. It was designed for efficiency — a mobile app could publish a post and upload media in one round trip.
Attackers use it to bundle hundreds of wp.getUsersBlogs authentication attempts into a single POST:
<?xml version="1.0"?>
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>methodName</name>
<value><string>wp.getUsersBlogs</string></value>
</member>
<member>
<name>params</name>
<value>
<array>
<data>
<value><string>admin</string></value>
<value><string>password123</string></value>
</data>
</array>
</value>
</member>
</struct>
</value>
<!-- ...repeated 499 more times with different passwords -->
</data>
</array>
</value>
</param>
</params>
</methodCall>
One HTTP request. 500 password guesses. Your login limiter plugin sees one request and does nothing.
In my client's case, I found 296 unique source IPs making these requests — a botnet cycling through a credential list. Each request contained roughly 500 password attempts, meaning those 147,832 requests represented somewhere in the region of 74 million login attempts in six hours.
Every single one of those attempts spawns a PHP-FPM worker, hits the database for authentication, and consumes CPU cycles. That's why the site was crawling.
The Other Attack: Pingback DDoS Amplification
Brute force isn't the only thing attackers do with xmlrpc.php. The pingback method can be weaponised as a DDoS reflector.
Here's how it works: an attacker sends a pingback request to your WordPress site, claiming that a URL on a target victim's server links to your content. WordPress dutifully makes an HTTP request to the victim's server to verify. When an attacker triggers this on thousands of WordPress sites simultaneously, the victim gets flooded with traffic — and the requests appear to come from legitimate WordPress sites, not from the attacker.
Your site becomes an unwitting participant in an attack on someone else. I've seen this in access logs as clusters of POST requests to xmlrpc.php with pingback.ping in the body, often targeting a single external IP.
How to Check if Your Site Is Being Attacked Right Now
Check your access logs. On Nginx:
grep -c "xmlrpc.php" /var/log/nginx/access.log
On Apache:
grep -c "xmlrpc.php" /var/log/apache2/access.log
If you're seeing more than a handful of requests per day, you likely have a problem. Legitimate XML-RPC usage in 2026 is extremely rare.
For a more detailed picture, look at who's making the requests:
grep "xmlrpc.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
If you see dozens or hundreds of unique IPs all hitting xmlrpc.php, that's a botnet.
To check the request body (if you're logging POST data or can use tcpdump), look for system.multicall or wp.getUsersBlogs — those are the telltale signs of a credential stuffing attack.
The Fix: Block It at the Server Level
I don't recommend using WordPress plugins to disable XML-RPC. By the time PHP processes the request and a plugin filter runs, you've already consumed server resources. The request has already passed through Nginx, been handed to PHP-FPM, loaded WordPress core, and executed hooks. On a server under heavy attack, that overhead is exactly what's killing your performance.
Block it before it ever reaches PHP.
Nginx
Add this to your server block:
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
return 444;
}
The 444 status is Nginx-specific — it closes the connection immediately without sending any response headers. This is more efficient than returning a 403, because the attacker's bot doesn't even get the satisfaction of a response. It also saves bandwidth.
Reload Nginx:
nginx -t && systemctl reload nginx
Apache
Add this to your .htaccess file in the WordPress root directory, or preferably to your virtual host configuration:
Require all denied
If you're on Apache 2.2 (you shouldn't be, but just in case):
Order deny,allow
Deny from all
CloudPanel / LiteSpeed
If you're running CloudPanel with Nginx, add the location block to Sites > your-site > Nginx Directives (the Vhost tab). On OpenLiteSpeed, add a context rule in the virtual host configuration to deny access to /xmlrpc.php.
What if You Actually Need XML-RPC?
In 2026, there are very few legitimate reasons to keep xmlrpc.php open. But there are some:
Jetpack historically relied on XML-RPC for the connection between your self-hosted WordPress site and WordPress.com. Modern versions of Jetpack have largely transitioned to the REST API, but older installs or certain features may still fall back to XML-RPC. If you use Jetpack, test after blocking — if the Jetpack dashboard connection breaks, you'll need to allowlist Automattic's IP ranges.
The WordPress mobile app used to require XML-RPC. As of 2025, the app uses the REST API for all core operations. Unless you're running an ancient version of the app, disabling XML-RPC won't affect it.
Third-party publishing tools like some IFTTT integrations or older desktop editors (Windows Live Writer, for example) may still use XML-RPC. These are increasingly rare.
If you genuinely need to keep XML-RPC available for a specific service, allowlist the IP instead of leaving it open to the world:
location = /xmlrpc.php {
allow 192.0.64.0/18; # Automattic/Jetpack IP range
deny all;
access_log off;
log_not_found off;
}
The WordPress-Level Fallback
If you don't have access to server configuration (shared hosting), you can disable XML-RPC at the WordPress level. Add this to your theme's functions.php or a custom plugin:
add_filter('xmlrpc_enabled', '__return_false');
This disables authentication-based XML-RPC methods but does not disable pingbacks. For that, add:
add_filter('xmlrpc_methods', function ($methods) {
unset($methods['pingback.ping']);
unset($methods['pingback.extensions.getPingbacks']);
return $methods;
});
Remember: this still loads PHP and WordPress before the request is rejected. On a server being hammered with thousands of requests per minute, this won't save you. Server-level blocking is always preferable.
What Happened With My Client
After adding the Nginx location block and reloading, the effect was immediate. CPU dropped from 94% to 12% within two minutes. Page load times returned to under 2 seconds.
I also checked the WordPress user table to make sure no account had been compromised during the attack. None had — the passwords were strong and unique, which is the only reason the brute force attempt didn't succeed despite 74 million guesses.
I added the xmlrpc.php block to my standard Nginx template that I deploy across all 70+ sites I manage. It's now part of my onboarding checklist for every new client.
Prevention Checklist
- Block
xmlrpc.phpat the web server level (Nginx, Apache, or LiteSpeed) — not with a plugin - Use strong, unique passwords on every WordPress admin account — the attack wouldn't have mattered anyway if the password was in a leaked credential list
- Enforce two-factor authentication — even if XML-RPC guesses the right password, 2FA stops the login
- Monitor access logs — a cron job that alerts you when
xmlrpc.phprequests exceed a threshold catches attacks early - Keep WordPress updated — WordPress core has added some XML-RPC hardening over the years, but it's still not enough on its own
A Simple Monitoring Script
I run this as a daily cron job across my managed servers. It checks if xmlrpc.php is getting hit and sends an alert if the count exceeds a threshold:
#!/bin/bash
THRESHOLD=100
COUNT=$(grep -c "xmlrpc.php" /var/log/nginx/access.log)
if [ "$COUNT" -gt "$THRESHOLD" ]; then
echo "xmlrpc.php hit $COUNT times today on $(hostname)" | mail -s "XML-RPC Alert" [email protected]
fi
It's crude but effective. On a properly secured server where xmlrpc.php is blocked, this count should be zero. If it's not, something has changed.
Stop Firefighting. Start Maintaining.
XML-RPC attacks are one of dozens of attack vectors I monitor and block across the 70+ WordPress sites I manage. Most site owners don't know xmlrpc.php exists until their server grinds to a halt.
If you'd rather not find out the hard way, I can help. Whether you need ongoing maintenance, a security audit, or someone to lock down your server configuration — that's what I do.
