Created
July 19, 2026 13:19
-
-
Save shimarin/89c3835b1e0de75bd4f8d73eacdd23a2 to your computer and use it in GitHub Desktop.
サーバー内のWordPressを一括検出し、緊急脆弱性の対象バージョンを調べる
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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