TP-03_sbi_security

TP-03 — Service-Based Interface (SBI) Security Tests

Domain: 5G SBI Security (HTTP/2, NRF, OAuth2, SCP)
Standards: 3GPP TS 33.501 §13 · GSMA FS.40 v3.0 §5.4 · OWASP API Security Top 10
Prerequisites: TP-00 complete; 5G SA lab running; TC-REG-01 passed


TC-SBI-01: NRF Registration — Unauthorized NF Injection

Threat Model

graph TD
    subgraph LEGIT["Legitimate NFs"]
        AMF["AMF\n(valid cert + NF-ID)"]
        SMF["SMF\n(valid cert + NF-ID)"]
        AUSF["AUSF\n(valid cert + NF-ID)"]
    end

    subgraph ROGUE["Attacker"]
        ROGUE_AMF["Rogue AMF\n(no cert / fake NF-ID)"]
        ROGUE_UPF["Rogue UPF\n(no cert)"]
    end

    NRF["NRF\n(Service Registry)"]
    SCP["SCP\n(Service Proxy)"]
    UDM["UDM\n(Subscriber Data)"]

    AMF -->|mTLS + PUT /nf-instances| NRF
    SMF -->|mTLS + PUT /nf-instances| NRF
    AUSF -->|mTLS + PUT /nf-instances| NRF

    ROGUE_AMF -->|No TLS, PUT /nf-instances| NRF
    NRF --> CHECK{"Auth Check\nmTLS / OAuth2?"}

    CHECK -->|Secure| ACCEPT["✅ NF Registered\nListed in NRF"]
    CHECK -->|No auth in lab| VULN["❌ CRITICAL:\nRogue NF Registered\nAll subscribers at risk"]
    CHECK -->|Reject| BLOCKED["✅ 401 Unauthorized"]

    ROGUE_AMF -.->|If registered| UDM
    UDM -.->|Returns subscriber AV| ROGUE_AMF
    ROGUE_AMF -.->|MitM all auth| SUBSCRIBERS["All Subscribers\n☠️ Compromised"]

    style ROGUE_AMF fill:#7b2d00,color:#fff
    style ROGUE_UPF fill:#7b2d00,color:#fff
    style VULN fill:#c0392b,color:#fff
    style ACCEPT fill:#27ae60,color:#fff
    style BLOCKED fill:#27ae60,color:#fff
    style SUBSCRIBERS fill:#c0392b,color:#fff

Objective

Verify NRF rejects NF registration attempts without valid credentials. Validate the gap in default Open5GS lab config (no mTLS).

Steps

# 1. Attempt rogue NF registration without any authentication
FAKE_UUID="12345678-1234-1234-1234-123456789abc"

curl -v -X PUT \
  "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances/${FAKE_UUID}" \
  -H "Content-Type: application/json" \
  -d '{
    "nfInstanceId": "'${FAKE_UUID}'",
    "nfType": "AMF",
    "nfStatus": "REGISTERED",
    "plmnList": [{"mcc":"001","mnc":"01"}],
    "amfInfo": {"amfSetId": "1", "amfPointer": "1", "guamiList": []}
  }' 2>&1 | grep -E "< HTTP|401|403|200"

# 2. Check if rogue NF appears in registry
curl -s "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances" | \
  jq --arg uuid "$FAKE_UUID" '.[] | select(.nfInstanceId == $uuid)'
# Should return NOTHING if NRF rejected it

# 3. If lab has no TLS (expected finding), document it
curl -s "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances" | \
  jq '. | length'
# Compare before and after — if count increased, rogue NF was accepted

# 4. Validate all registered NFs
curl -s "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances" | \
  jq '.[] | {nfType, nfInstanceId, nfStatus}'

# 5. Cleanup — deregister rogue NF if it was accepted
curl -X DELETE \
  "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances/${FAKE_UUID}"

Expected Results (secure/production)

Expected Results (lab default — Open5GS no TLS)

Finding

HTTP 200 on unauthenticated registration → CRITICAL FINDING. Document as: "Open5GS default SBI has no mTLS; deploy with openssl-generated certs and set tls: in all NF YAML configs for production."

Pass Criteria (production hardened lab)

Unauthenticated registration rejected with HTTP 401/403.


TC-SBI-02: OAuth2 Token Enforcement on NF Service Calls

Threat Model

sequenceDiagram
    participant ATK as Attacker (no token)
    participant LEGIT as Legitimate NF (AMF)
    participant NRF as NRF (token issuer)
    participant UDM as UDM (service provider)

    Note over ATK,UDM: THREAT: Unauthorized NF-to-NF API access

    Note over ATK: Attack: Direct API call without token
    ATK->>UDM: GET /nudm-sdm/v1/imsi-001010000000001/am-data\n(no Authorization header)
    UDM-->>ATK: ❌ 401 Unauthorized\nWWW-Authenticate: Bearer realm=...

    Note over ATK: Attack: Expired/tampered token
    ATK->>UDM: GET /nudm-sdm/v1/.../am-data\nAuthorization: Bearer 
    UDM->>UDM: Validate JWT signature + expiry
    UDM-->>ATK: ❌ 401 Unauthorized

    Note over LEGIT: Legitimate flow
    LEGIT->>NRF: POST /oauth2/token\n{client_credentials, scope: nudm-sdm}
    NRF-->>LEGIT: access_token (JWT, 300s TTL)
    LEGIT->>UDM: GET /nudm-sdm/v1/.../am-data\nAuthorization: Bearer 
    UDM->>UDM: Validate JWT (sig, expiry, scope, audience)
    UDM-->>LEGIT: ✅ 200 OK + subscriber data

Objective

Verify UDM (and other NFs) enforce OAuth2 token presence and validity before returning subscriber data.

Steps

IMSI="imsi-001010000000001"

# 1. Call UDM without Authorization header
echo "=== Test 1: No token ==="
curl -s -o /dev/null -w "%{http_code}" \
  "http://172.22.0.15:7777/nudm-sdm/v1/${IMSI}/am-data"
# Expected: 401

# 2. Call with a manually crafted but invalid JWT
FAKE_TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJmYWtlIn0.invalidsignature"
echo ""
echo "=== Test 2: Invalid token ==="
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer ${FAKE_TOKEN}" \
  "http://172.22.0.15:7777/nudm-sdm/v1/${IMSI}/am-data"
# Expected: 401

# 3. Get a valid token from NRF (legitimate flow baseline)
echo ""
echo "=== Test 3: Valid token (baseline) ==="
TOKEN=$(curl -s -X POST \
  "http://172.22.0.10:7777/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&nfInstanceId=$(uuidgen)&scope=nudm-sdm&nfType=AMF" \
  | jq -r '.access_token')

curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer ${TOKEN}" \
  "http://172.22.0.15:7777/nudm-sdm/v1/${IMSI}/am-data"
# Expected: 200

# 4. Check NRF token endpoint exists
curl -s -o /dev/null -w "%{http_code}" \
  "http://172.22.0.10:7777/oauth2/token"

Expected Results

Pass Criteria

All unauthorized API calls rejected. Valid token grants access. No subscriber data leak without token.


TC-SBI-03: SCP as Single Point of Failure

Threat Model

graph TD
    subgraph NORMAL["Normal Operation"]
        AMF1["AMF"] -->|Route via SCP| SCP["SCP\n(Service Proxy)"]
        SCP -->|Forward| SMF1["SMF"]
        SCP -->|Forward| UDM1["UDM"]
        SCP -->|Forward| AUSF1["AUSF"]
    end

    subgraph ATTACK["Attack / Failure Scenario"]
        AMF2["AMF"] -->|Route via SCP| SCP2["SCP\n❌ KILLED"]
        SCP2 -->|Timeout| FAIL1["SMF — unreachable"]
        SCP2 -->|Timeout| FAIL2["UDM — unreachable"]
        AMF2 -->|Fallback: direct?| SMF2["SMF (direct if configured)"]
    end

    subgraph IMPACT["Impact Assessment"]
        I1["⚠️ New registrations blocked"]
        I2["⚠️ Active sessions: maintained?"]
        I3["⚠️ Recovery time: measured?"]
    end

    SCP2 -.->|DoS input| I1
    SCP2 -.->|Session state| I2
    SCP2 -.->|Restart| I3

    style SCP2 fill:#c0392b,color:#fff
    style FAIL1 fill:#e67e22,color:#fff
    style FAIL2 fill:#e67e22,color:#fff

Objective

Validate behavior when SCP crashes; measure impact on active sessions and recovery.

Steps

# 1. Establish baseline — 2 UEs registered, traffic flowing
docker compose -f ~/open5gs-lab/5g/docker-compose.yml restart ue
sleep 10
BEFORE=$(curl -s "http://localhost:9090/api/v1/query?query=amf_registered_ue_count" | jq '.data.result[0].value[1]')
echo "UEs before SCP kill: ${BEFORE}"

# 2. Start background ping (active session)
docker exec ueransim-ue ping -i 0.5 -I uesimtun0 8.8.8.8 &
PINGPID=$!

# 3. Kill SCP
echo "Killing SCP..."
docker stop open5gs-scp
KILL_TIME=$(date +%s)

# 4. Attempt new UE registration during outage
docker run --rm --cap-add=NET_ADMIN \
  --network open5gs-lab_ran \
  -v ~/open5gs-lab/5g/config/ue.yaml:/etc/ueransim/ue.yaml \
  --name ueransim-test-scp \
  louisroyer/ueransim-ue:latest &
sleep 15
docker logs ueransim-test-scp 2>&1 | grep -E "registered|failed|error"

# 5. Check if existing session survived
kill $PINGPID 2>/dev/null
docker exec ueransim-ue ping -c 3 -I uesimtun0 8.8.8.8 2>&1 | tail -3

# 6. Restart SCP and measure recovery time
docker start open5gs-scp
RESTART_TIME=$(date +%s)

# Wait for SCP to re-register with NRF
until curl -s "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances" | \
  jq -e '.[] | select(.nfType=="SCP")' > /dev/null 2>&1; do
  sleep 1
done
RECOVERED_TIME=$(date +%s)
echo "SCP recovery time: $((RECOVERED_TIME - RESTART_TIME))s"

# 7. Verify new registrations work after recovery
docker compose -f ~/open5gs-lab/5g/docker-compose.yml restart ue
sleep 10
docker exec ueransim-ue ip addr show uesimtun0

Expected Results

Pass Criteria

Document exact behavior. SCP recovery < 60 seconds. No permanent session loss post-recovery.


TC-SBI-04: HTTP/2 Input Validation — Malformed SBI Request

Threat Model

flowchart TD
    ATK["Attacker\n(internal network access)"]

    ATK -->|Empty multipart/related body| NRF["NRF\nparse_multipart()"]
    ATK -->|Oversized JSON >1MB| AMF["AMF\nJSON parser"]
    ATK -->|NULL bytes in SUPI field| UDM["UDM\nSUPI validator"]
    ATK -->|Malformed PFCP IE| UPF["UPF\nogs_pfcp_handle_create_pdr()"]

    NRF --> CHECK1{"Exception\nhandled?"}
    AMF --> CHECK2{"Exception\nhandled?"}
    UDM --> CHECK3{"Exception\nhandled?"}
    UPF --> CHECK4{"CVE-2025-14953\nPatched in v2.7.6+?"}

    CHECK1 -->|HTTP 400| SAFE1["✅ Safe\nError returned"]
    CHECK1 -->|Null ptr deref crash| CRIT1["❌ CRITICAL\nDoS via SBI"]
    CHECK2 -->|HTTP 400| SAFE2["✅ Safe"]
    CHECK2 -->|Container OOM| CRIT2["❌ HIGH\nMemory exhaustion"]
    CHECK3 -->|HTTP 400| SAFE3["✅ Safe"]
    CHECK3 -->|Data returned| CRIT3["❌ CRITICAL\nData injection"]
    CHECK4 -->|No crash| SAFE4["✅ CVE patched"]
    CHECK4 -->|UPF crashes| CRIT4["❌ CRITICAL\nCVE-2025-14953 unpatched"]

    style CRIT1 fill:#c0392b,color:#fff
    style CRIT2 fill:#c0392b,color:#fff
    style CRIT3 fill:#c0392b,color:#fff
    style CRIT4 fill:#c0392b,color:#fff
    style SAFE1 fill:#27ae60,color:#fff
    style SAFE2 fill:#27ae60,color:#fff
    style SAFE3 fill:#27ae60,color:#fff
    style SAFE4 fill:#27ae60,color:#fff

Objective

Verify all 5GC NFs handle malformed/unexpected HTTP/2 SBI inputs without crashing (regression for RANsacked / CVE-2025-14953 class).

Steps

# 1. Empty multipart/related body to NRF (CVE pattern: parse_multipart null deref)
echo "=== Test 1: Empty multipart body ==="
curl -s -o /dev/null -w "%{http_code}" -X PUT \
  "http://172.22.0.10:7777/nnrf-nfm/v1/nf-instances/$(uuidgen)" \
  -H "Content-Type: multipart/related; boundary=boundary" \
  --data-binary ""
sleep 2
docker inspect --format '{{.State.RestartCount}}' open5gs-nrf
# Must be 0

# 2. Oversized JSON body to AMF (memory exhaustion test)
echo "=== Test 2: Oversized JSON ==="
python3 -c "
import json
payload = {'nfInstanceId': 'test', 'padding': 'A' * 1048576}
print(json.dumps(payload))
" > /tmp/oversized.json

curl -s -o /dev/null -w "%{http_code}" -X PUT \
  "http://172.22.0.20:7777/namf-comm/v1/ue-contexts/imsi-000000000000001" \
  -H "Content-Type: application/json" \
  --data-binary @/tmp/oversized.json
sleep 2
docker inspect --format '{{.State.RestartCount}}' open5gs-amf
# Must be 0

# 3. NULL bytes in SUPI path parameter
echo "=== Test 3: NULL bytes in SUPI ==="
curl -s -o /dev/null -w "%{http_code}" \
  "http://172.22.0.15:7777/nudm-sdm/v1/imsi-00101%00000000001/am-data"
sleep 2
docker inspect --format '{{.State.RestartCount}}' open5gs-udm
# Must be 0

# 4. Check restart counts for all NFs
echo "=== Restart count check ==="
for NF in nrf amf smf udm ausf pcf; do
  COUNT=$(docker inspect --format '{{.State.RestartCount}}' open5gs-${NF} 2>/dev/null)
  echo "${NF}: restarts = ${COUNT}"
done
# All must be 0

Expected Results

Pass Criteria

All 4 malformed input tests: NF returns error HTTP code AND restart count stays at 0. Any restart = CRITICAL FINDING.