Skip to content

Instantly share code, notes, and snippets.

@shimarin
Created July 19, 2026 13:19
Show Gist options
  • Select an option

  • Save shimarin/89c3835b1e0de75bd4f8d73eacdd23a2 to your computer and use it in GitHub Desktop.

Select an option

Save shimarin/89c3835b1e0de75bd4f8d73eacdd23a2 to your computer and use it in GitHub Desktop.
サーバー内のWordPressを一括検出し、緊急脆弱性の対象バージョンを調べる
#!/usr/bin/env python3
"""指定ディレクトリ配下の wp-includes/version.php を探索し、WordPressバージョンを表示する。
Python 3.6以上で動作する(RHEL8クラスのシステムPythonを想定)。
"""
import argparse
import glob
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
VERSION_RE = re.compile(r"""\$wp_version\s*=\s*['"]([^'"]+)['"]""")
# 2026-07-17 公表。REST APIバッチルート混同(CVE-2026-63030)とWP_Query SQLインジェクション
# (CVE-2026-60137)を組み合わせると未認証RCE(通称 wp2shell)に至る。
CVE_CHECKS = (
(
"CVE-2026-63030",
"対象【脆弱】6.9.5または7.0.2以降へのアップデートが必要です",
(
((6, 9, 0), (6, 9, 4)),
((7, 0, 0), (7, 0, 1)),
),
),
(
"CVE-2026-60137",
"対象【脆弱】6.8.6、6.9.5または7.0.2以降へのアップデートが必要です",
(
((6, 8, 0), (6, 8, 5)),
((6, 9, 0), (6, 9, 4)),
((7, 0, 0), (7, 0, 1)),
),
),
)
PRERELEASE_RE = re.compile(r"-(alpha|beta|rc)-?(\d+)?", re.IGNORECASE)
PRERELEASE_STAGE_RANK = {"alpha": 0, "beta": 1, "rc": 2}
# 7.1系は 7.1 Beta 2 で両CVEとも修正。それより前のプレリリース(alpha, beta1)は対象。
WP_7_1_FIX_RANK = (PRERELEASE_STAGE_RANK["beta"], 2)
def parse_version(version: str) -> Optional[Tuple[int, ...]]:
m = re.match(r"^(\d+(?:\.\d+)*)", version)
if not m:
return None
parts = tuple(int(p) for p in m.group(1).split("."))
return (parts + (0, 0, 0))[:3]
def parse_prerelease(version: str) -> Optional[Tuple[int, int]]:
m = PRERELEASE_RE.search(version)
if not m:
return None
stage = m.group(1).lower()
number = int(m.group(2)) if m.group(2) else 1
return (PRERELEASE_STAGE_RANK[stage], number)
def check_cves(version: str) -> List[Tuple[str, str]]:
parsed = parse_version(version)
if parsed is None:
return [
(cve_id, "判定不能(バージョン文字列を解析できません)")
for cve_id, _, _ in CVE_CHECKS
]
prerelease = parse_prerelease(version)
is_vulnerable_7_1_prerelease = (
parsed == (7, 1, 0) and prerelease is not None and prerelease < WP_7_1_FIX_RANK
)
results = []
for cve_id, vulnerable_message, ranges in CVE_CHECKS:
vulnerable = any(lo <= parsed <= hi for lo, hi in ranges) or is_vulnerable_7_1_prerelease
results.append((cve_id, vulnerable_message if vulnerable else "対象外"))
return results
def extract_version(version_php: Path) -> Optional[str]:
try:
content = version_php.read_text(encoding="utf-8", errors="replace")
except OSError as e:
print(f" 読み込み失敗: {e}", file=sys.stderr)
return None
m = VERSION_RE.search(content)
return m.group(1) if m else None
def resolve_directories(patterns: List[str]) -> List[Path]:
directories: Dict[Path, None] = {}
for pattern in patterns:
matches = sorted(glob.glob(pattern)) or [pattern]
for match in matches:
path = Path(match)
if path.is_dir():
directories.setdefault(path.resolve())
else:
print(f"警告: {match} はディレクトリではないためスキップします", file=sys.stderr)
return list(directories)
def find_version_php_files(directory: Path) -> List[Path]:
# pathlib.Path.rglob() はデフォルトでシンボリックリンクを辿らない
# (recurse_symlinks は Python 3.13以降限定のため、os.walk(followlinks=True)で代替)。
results = []
for dirpath, _dirnames, filenames in os.walk(str(directory), followlinks=True):
if os.path.basename(dirpath) == "wp-includes" and "version.php" in filenames:
results.append(Path(dirpath) / "version.php")
return results
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"directories",
nargs="+",
metavar="directory",
help="探索対象のディレクトリ(複数指定可、'/home/*/public_html' のようなglobパターンも可)",
)
args = parser.parse_args()
directories = resolve_directories(args.directories)
if not directories:
print("エラー: 探索可能なディレクトリがありません", file=sys.stderr)
return 1
found = sorted(
version_php
for directory in directories
for version_php in find_version_php_files(directory)
)
if not found:
print("wp-includes/version.php は見つかりませんでした。")
return 0
for version_php in found:
version = extract_version(version_php)
if version:
cve_statuses = " / ".join(
f"{cve_id}: {status}" for cve_id, status in check_cves(version)
)
print(f"{version_php}: WordPress {version} / {cve_statuses}")
else:
print(f"{version_php}: バージョン文字列が見つかりませんでした")
return 0
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment