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:#fffObjective
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)
- NRF returns
HTTP 401 UnauthorizedorHTTP 403 Forbidden - Rogue NF does not appear in
GET /nf-instances
Expected Results (lab default — Open5GS no TLS)
- NRF returns
HTTP 200 Created(lab finding — no auth enforced) - Rogue NF appears in registry
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
- No token:
401 Unauthorized - Invalid token:
401 UnauthorizedwithWWW-Authenticateheader - Valid token:
200 OKwith subscriber AM data - NRF issues tokens via
/oauth2/tokenendpoint
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:#fffObjective
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
- During SCP outage: new registrations fail (SBI routing broken)
- Existing sessions: may survive if NF-to-NF direct mode configured
- SCP restart: re-registers with NRF within 30 seconds
- Post-recovery: new registrations succeed
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:#fffObjective
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
- All malformed inputs return
HTTP 400 Bad RequestorHTTP 422 Unprocessable Entity - Zero container restarts across all NFs
- No memory disclosure in error response body
- NF logs show error handling (not crashes)
Pass Criteria
All 4 malformed input tests: NF returns error HTTP code AND restart count stays at 0. Any restart = CRITICAL FINDING.