Bali, Indonesia
Insights

Spotting XML-RPC Multicall Attacks in Web Server Access Logs

October 18, 2026
0:00
A server starts thrashing with elevated CPU and database load, yet the traditional access logs for wp-login.php show only an occasional request. Security plugins configured to block IP addresses after five failed login attempts remain quiet. If you look only at the login URI, everything looks peaceful. Looking at xmlrpc.php, however, reveals a steady drip of HTTP POST requests that look harmless on the surface but are consuming PHP-FPM workers and database connections behind the scenes. This pattern usually points to an XML-RPC brute-force attack leveraging the system.multicall method. When troubleshooting high load on a WordPress hosting environment, this is one of the easiest attack vectors to misdiagnose because it intentionally works around traditional per-request rate limiting.
WordPress includes xmlrpc.php to allow external applications—like mobile publishing apps, remote editors, and desktop clients—to interact with the site over HTTP using XML data formats. Inside the XML-RPC specification is a utility method called system.multicall. Its intended purpose is efficiency: instead of establishing separate HTTP connections to perform ten different administrative actions, an authorized client can bundle ten commands into a single XML envelope and send them in one POST request. The complication arises because XML-RPC methods like wp.getUsersBlogs or metaWeblog.getUsersBlogs validate authentication on every call using a plaintext username and password inside the payload. When an attacker wraps multiple authentication calls inside a system.multicall wrapper, WordPress processes each nested method call sequentially during a single execution cycle of xmlrpc.php. If an attacker packs five hundred credential combinations into a single POST body, the web server sees exactly one HTTP request. Standard firewalls and rate limiters monitoring request frequency per IP address see a low-volume client making a request every few seconds. Meanwhile, the PHP backend runs five hundred database queries against the wp_users and wp_usermeta tables, hashing passwords and validating users within a single PHP process. If several bots do this at the same time, PHP-FPM pools saturate quickly.
Most web hosting environments run standard Combined Log Format or a similar standard Nginx log layout. When a typical brute-force tool targets /wp-login.php, every single attempt creates one line in the access log, almost always returning an HTTP 200 (if rendering the login page with an error) or an HTTP 302 redirect. A quick inspection using wc -l or grep reveals thousands of lines from a single IP address. With system.multicall, the logs tell a deceptive story. The request line will show an HTTP POST to /xmlrpc.php returning an HTTP 200 status code. Because XML-RPC returns an HTTP 200 status even when individual sub-methods return authentication failures (which are packaged inside XML <fault> responses), the HTTP status code alone does not reveal an attack. As discussed in our look at why HTTP 200 in access logs does not mean an exploit succeeded, status codes describe transport success, not business logic outcomes. To identify a multicall attack, you have to look at the metrics that sit beside the status code: the byte size of the response, the frequency of POST requests relative to legitimate site activity, and, if your logs record it, the request processing time.
A legitimate XML-RPC call (for example, a pingback or a remote post publish) usually produces a relatively compact response, often under a couple of kilobytes, or it appears irregularly. A system.multicall brute-force attack generates distinct log signatures. Because the server returns an XML array containing hundreds of failure responses within that single HTTP response, the byte count sent back to the client is consistently much larger than a standard pingback. You can inspect these patterns using basic command-line tools against your web server's access log file:
BASH
# Filter POST requests to xmlrpc.php and print IP, response code, and bytes sent
awk '6 ~ /POST/ &&7 ~ /xmlrpc\.php/ {print 1,9, $10}' /var/log/nginx/access.log | head -n 30
In a standard log, column 1 is the remote IP address, 9 is the HTTP response code, and $10 is the body bytes sent. An example format output during a multicall scan often looks like this:
TEXT
192.0.2.45 200 48210
192.0.2.45 200 48195
192.0.2.45 200 48220
198.51.100.12 200 32150
198.51.100.12 200 32144
Notice the byte counts in the third column: they are consistently in the tens of kilobytes (such as 30,000 to 50,000 bytes) for every single POST. When an endpoint that usually returns brief confirmations suddenly starts sending 40 KB payloads across repeated HTTP 200 responses to the same remote address, the server is returning a massive batch of failed method responses. A few dozen of these requests in an hour might not trigger a generic fail2ban rule configured for web logins, but they represent thousands of password attempts and significant database hashing overhead.
If you want to observe how this appears in your own environment, you can safely test the response behavior on an isolated staging server using curl. You do not need automated brute-force tools to see the mechanism in action; sending a small XML payload containing a single system.multicall with invalid credentials illustrates the log and payload behavior immediately. Here is how to structure a diagnostic call against a local test instance:
BASH
curl -X POST -H 'Content-Type: text/xml' \
  -d '<?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>testuser</string></value><value><string>testpass</string></value></data></array></value></member></struct></value></data></array></value></param></params></methodCall>' \
  http://localhost/xmlrpc.php
The server will process the nested call to wp.getUsersBlogs, fail authentication, and wrap that error in a standard XML-RPC response structure containing fault code 403 (or similar, depending on the WordPress version). In your server's access log, you will see a POST /xmlrpc.php HTTP/1.1 entry with a 200 status code. If you duplicate that <value><struct>...</struct></value> block fifty times inside the <data> array, the execution time will visibly increase, the returned XML will repeat fifty fault blocks, and the logged bytes sent will climb proportionally.
When administrators first encounter these multicall logs, the immediate reaction is often to shut down the file entirely. In many hosting setups, outright disabling xmlrpc.php at the web server level is indeed the cleanest fix, especially if the site relies purely on the modern WordPress REST API. However, there are environments where legacy systems or specific third-party services require access to xmlrpc.php. For example, older publishing integrations or certain features of management plugins route their monitoring checks through this file. If you block the file completely using an Nginx location = /xmlrpc.php { deny all; } directive or an Apache .htaccess rule, verify whether the site owner uses external mobile publishing or automated syndication that relies on legacy endpoints. If xmlrpc.php must remain accessible for specific remote systems, the safer path is whitelisting trusted source IPs or configuring the web server to block requests specifically when the request method is POST and the payload contains system.multicall. For sites that do not use any mobile apps or external XML-RPC publishing tools, terminating all access to xmlrpc.php directly at the web server layer relieves PHP and MySQL from parsing these payloads entirely. Recognizing that low request volumes can hide thousands of authentication attempts helps support teams locate the real cause of sudden load spikes when standard login logs appear completely clean.