Testing ISAKMP Part 4: Using Scapy
Table of Contents
In Part 2, I showed how to test ISAKMP with a pre-built hex string and netcat. In Part 3, we dove deep into the byte-by-byte construction of ISAKMP packets. Now let’s use Scapy to automate this with Python.
Why Scapy?
Netcat with hex strings works for one-off tests, but Scapy lets you build packets programmatically, parse responses automatically, and script tests across multiple targets. It understands ISAKMP structure and handles length fields and checksums for you.
Prerequisites
- Python 3.8+ installed
- Basic Python knowledge
- Understanding of ISAKMP concepts (see Part 1)
- Target ISAKMP/IKE peer to test
- Root/sudo access (required for raw socket operations)
Installation
# Install Poetry (if not already installed)
curl -sSL https://install.python-poetry.org | python3 -
# Install Scapy via Poetry
cd /path/to/your/project
poetry init
poetry add scapy
# Or install globally
pip install scapy
# Verify installation
python3 -c "from scapy.all import *; print(conf.version)"
Important: Scapy requires raw socket access, which needs root/sudo privileges. When using Poetry with sudo, you must install dependencies as root:
sudo poetry install
Basic ISAKMP Packet with Scapy
The Simple Approach
#!/usr/bin/env python3
from scapy.all import *
# Target configuration
target_ip = "192.168.1.1"
target_port = 500
# Build basic ISAKMP packet
# Create IP and UDP layers
ip = IP(dst=target_ip)
udp = UDP(sport=500, dport=target_port)
# Create ISAKMP header (Identity Protection, Main Mode)
isakmp = ISAKMP(
init_cookie=RandString(8), # Random 8-byte initiator cookie
resp_cookie=b'\x00' * 8, # Responder cookie (zeros for initial packet)
exch_type=2, # Identity Protection (Main Mode)
)
# Create SA payload with a simple transform
sa = ISAKMP_payload_SA(
prop=ISAKMP_payload_Proposal(
proto=1, # ISAKMP
trans=ISAKMP_payload_Transform(
transform_id=1 # KEY_IKE
)
)
)
# Assemble the complete packet
packet = ip / udp / isakmp / sa
# Send and receive
response = sr1(packet, timeout=5, verbose=1)
if response:
response.show()
else:
print("No response received")
Building a Complete Phase 1 Packet
Transform Set Configuration
# Define transform set parameters
# Using modern cryptographic standards
# Encryption algorithms (RFC 3602, RFC 2409)
ENCR_AES_CBC = 7
ENCR_3DES_CBC = 5
# Hash algorithms (RFC 2409)
HASH_SHA256 = 4
HASH_SHA1 = 2
# Diffie-Hellman groups (RFC 2409, RFC 3526)
DH_GROUP_14 = 14 # 2048-bit MODP
DH_GROUP_5 = 5 # 1536-bit MODP
DH_GROUP_2 = 2 # 1024-bit MODP
# Authentication methods (RFC 2409)
AUTH_PSK = 1 # Pre-Shared Key
AUTH_RSA_SIG = 3 # RSA Signatures
# Life duration type (RFC 2409)
LIFE_TYPE_SECONDS = 1
LIFE_TYPE_KILOBYTES = 2
# Define a modern transform set
transform_set = {
'encryption': ENCR_AES_CBC,
'key_length': 256, # AES-256
'hash': HASH_SHA256,
'dh_group': DH_GROUP_14,
'auth_method': AUTH_PSK,
'lifetime': 86400 # 24 hours in seconds
}
Constructing the Packet
from scapy.all import *
def build_isakmp_packet(target_ip, transform_set):
"""
Build a complete ISAKMP Phase 1 Main Mode packet with specified transform set.
Args:
target_ip: Target IP address
transform_set: Dictionary with encryption, hash, dh_group, auth_method, lifetime
Returns:
Complete Scapy packet ready to send
"""
# IP and UDP layers
ip = IP(dst=target_ip)
udp = UDP(sport=500, dport=500)
# ISAKMP header
isakmp = ISAKMP(
init_cookie=RandString(8),
resp_cookie=b'\x00' * 8,
exch_type=2, # Identity Protection (Main Mode)
)
# Build transform attributes as list of (type, value) tuples
# Type numbers: 1=Encryption, 2=Hash, 3=Auth, 4=Group, 11=LifeType, 12=LifeDuration, 14=KeyLength
transforms = [
(1, transform_set['encryption']), # Encryption algorithm
(2, transform_set['hash']), # Hash algorithm
(4, transform_set['dh_group']), # DH Group
(3, transform_set['auth_method']), # Authentication method
(11, 1), # Life Type: seconds
(12, transform_set['lifetime']) # Life Duration
]
# Add key length if specified
if transform_set.get('key_length', 0) > 0:
transforms.insert(1, (14, transform_set['key_length']))
# Build Transform payload
transform = ISAKMP_payload_Transform(
transform_id=1, # KEY_IKE
transforms=transforms
)
# Build Proposal payload
proposal = ISAKMP_payload_Proposal(
proposal=1,
proto=1, # ISAKMP
trans=transform
)
# Build SA payload
sa = ISAKMP_payload_SA(
doi=1, # IPsec DOI (lowercase field name)
situation=1, # Identity Only
prop=proposal
)
# Assemble complete packet
packet = ip / udp / isakmp / sa
return packet
# Example usage
target = "192.168.1.1"
packet = build_isakmp_packet(target, transform_set)
Note: Scapy’s ISAKMP implementation uses a list of
(type, value)tuples for transform attributes, not individual attribute objects. This is simpler and matches how the protocol actually works.
Sending and Analyzing Responses
Send the Packet
def send_isakmp_packet(packet, timeout=5):
"""
Send ISAKMP packet and capture response.
Args:
packet: Scapy packet to send
timeout: Response timeout in seconds
Returns:
Response packet or None
"""
print(f"[*] Sending ISAKMP packet to {packet[IP].dst}:500")
print(f"[*] Initiator Cookie: {packet[ISAKMP].init_cookie.hex()}")
# Send packet and wait for response
response = sr1(packet, timeout=timeout, verbose=0)
if response:
print(f"[+] Response received from {response[IP].src}")
return response
else:
print("[-] No response received (timeout)")
return None
# Send the packet
response = send_isakmp_packet(packet)
if response and response.haslayer(ISAKMP):
# Extract basic info
print(f"\n[*] Response Details:")
print(f" Responder Cookie: {response[ISAKMP].resp_cookie.hex()}")
print(f" Exchange Type: {response[ISAKMP].exch_type}")
print(f" Next Payload: {response[ISAKMP].next_payload}")
# Check if SA payload is present
if response.haslayer(ISAKMP_payload_SA):
print(f"[+] SA payload received - transform set accepted!")
else:
print(f"[-] No SA payload - transform set rejected")
# Note: Some devices send NOTIFY payloads on rejection
# Check for ISAKMP_payload_Notification for detailed error info
Understanding the Response
def parse_isakmp_response(response):
"""
Parse ISAKMP response and extract transform set details.
Args:
response: Scapy packet containing ISAKMP response
Returns:
Dictionary with parsed response details
"""
if not response or not response.haslayer(ISAKMP):
return None
result = {
'responder_cookie': response[ISAKMP].resp_cookie.hex(),
'exchange_type': response[ISAKMP].exch_type,
'accepted': False,
'transform_set': {}
}
# Check for SA payload (indicates acceptance)
if response.haslayer(ISAKMP_payload_SA):
result['accepted'] = True
# Extract transform attributes if present
if response.haslayer(ISAKMP_payload_Transform):
transform = response[ISAKMP_payload_Transform]
# Parse attributes
if hasattr(transform, 'attributes'):
for attr in transform.attributes:
attr_type = attr.attribute_type
attr_val = attr.attribute_value
# Map attribute types to names
if attr_type in [0x8001, 0x0001]:
result['transform_set']['encryption'] = attr_val
elif attr_type in [0x800e, 0x000e]:
result['transform_set']['key_length'] = attr_val
elif attr_type in [0x8002, 0x0002]:
result['transform_set']['hash'] = attr_val
elif attr_type in [0x8004, 0x0004]:
result['transform_set']['dh_group'] = attr_val
elif attr_type in [0x8003, 0x0003]:
result['transform_set']['auth_method'] = attr_val
return result
# Example usage
if response:
parsed = parse_isakmp_response(response)
if parsed and parsed['accepted']:
print("\n[+] Transform set ACCEPTED by peer")
print(f" Encryption: {parsed['transform_set'].get('encryption', 'N/A')}")
print(f" Hash: {parsed['transform_set'].get('hash', 'N/A')}")
print(f" DH Group: {parsed['transform_set'].get('dh_group', 'N/A')}")
else:
print("\n[-] Transform set REJECTED or no response")
Advanced Usage
Testing Multiple Transform Sets
def test_transform_sets(target_ip, transform_sets, timeout=5):
"""
Test multiple transform sets against a target.
Args:
target_ip: Target IP address
transform_sets: List of transform set dictionaries
timeout: Response timeout in seconds
Returns:
List of results with acceptance status
"""
results = []
for i, ts in enumerate(transform_sets, 1):
print(f"\n[*] Testing transform set {i}/{len(transform_sets)}")
print(f" Encryption: {ts['encryption']}, Hash: {ts['hash']}, DH: {ts['dh_group']}")
# Build and send packet
packet = build_isakmp_packet(target_ip, ts)
response = sr1(packet, timeout=timeout, verbose=0)
# Parse response
parsed = parse_isakmp_response(response)
result = {
'transform_set': ts,
'accepted': parsed['accepted'] if parsed else False,
'response': parsed
}
results.append(result)
if result['accepted']:
print(f" [+] ACCEPTED")
else:
print(f" [-] REJECTED or no response")
# Small delay between tests
time.sleep(1)
return results
# Define multiple transform sets to test
test_sets = [
# Modern strong crypto
{
'encryption': 7, # AES-CBC
'key_length': 256,
'hash': 4, # SHA-256
'dh_group': 14, # 2048-bit
'auth_method': 1,
'lifetime': 86400
},
# Moderate crypto
{
'encryption': 7, # AES-CBC
'key_length': 128,
'hash': 4, # SHA-256
'dh_group': 14, # 2048-bit
'auth_method': 1,
'lifetime': 86400
}
]
# Run tests
results = test_transform_sets("192.168.1.1", test_sets)
# Summary
print("\n" + "="*50)
print("SUMMARY")
print("="*50)
accepted = [r for r in results if r['accepted']]
print(f"Accepted: {len(accepted)}/{len(results)} transform sets")
Aggressive Mode vs Main Mode
def build_aggressive_mode_packet(target_ip, transform_set, identity="[email protected]"):
"""
Build ISAKMP Aggressive Mode packet.
Aggressive Mode sends more information in the first packet (including identity)
but completes the exchange faster (3 packets vs 6 in Main Mode).
Args:
target_ip: Target IP address
transform_set: Transform set dictionary
identity: Identity string for ID payload
Returns:
Complete Scapy packet
"""
# IP and UDP layers
ip = IP(dst=target_ip)
udp = UDP(sport=500, dport=500)
# ISAKMP header for Aggressive Mode
isakmp = ISAKMP(
init_cookie=RandString(8),
resp_cookie=b'\x00' * 8,
next_payload=1, # SA payload
exch_type=4, # Aggressive Mode (vs 2 for Main Mode)
flags=0
)
# Build SA payload (same as Main Mode)
# ... (use build_isakmp_packet logic for SA/Proposal/Transform)
# Add Key Exchange payload (sent in first packet in Aggressive Mode)
ke = ISAKMP_payload_KE(
next_payload=5, # ID payload follows
ke=RandString(128) # DH public value (size depends on DH group)
)
# Add Identification payload (sent in first packet in Aggressive Mode)
id_payload = ISAKMP_payload_ID(
next_payload=0,
IDtype=3, # ID_USER_FQDN
IdentData=identity.encode()
)
# Assemble: ISAKMP / SA / KE / ID
# Note: In Main Mode, KE and ID come in later packets
packet = ip / udp / isakmp / sa / ke / id_payload
return packet
# Compare the two modes:
# Main Mode (6 packets total, more secure)
# Packet 1: HDR, SA
# Packet 2: HDR, SA
# Packet 3: HDR, KE, Nonce
# Packet 4: HDR, KE, Nonce
# Packet 5: HDR*, ID, HASH
# Packet 6: HDR*, ID, HASH
main_mode_packet = build_isakmp_packet("192.168.1.1", transform_set)
print(f"Main Mode packet size: {len(main_mode_packet)} bytes")
print(f"Main Mode exchange type: {main_mode_packet[ISAKMP].exch_type}")
# Aggressive Mode (3 packets total, faster but exposes identity)
# Packet 1: HDR, SA, KE, Nonce, ID
# Packet 2: HDR, SA, KE, Nonce, ID, HASH
# Packet 3: HDR*, HASH
aggressive_packet = build_aggressive_mode_packet("192.168.1.1", transform_set)
print(f"Aggressive Mode packet size: {len(aggressive_packet)} bytes")
print(f"Aggressive Mode exchange type: {aggressive_packet[ISAKMP].exch_type}")
# Security consideration:
# Main Mode encrypts identity, Aggressive Mode sends it in CLEARTEXT
# This is a critical security vulnerability - attackers can harvest identities
# Use Main Mode unless you have a specific requirement for Aggressive Mode
NAT-T Detection
# TODO: Add NAT-T vendor ID
# TODO: Test for NAT-T support
Creating a Reusable Script
I’ve created a complete script that combines all these techniques. The full code is in the nn_examples repository.
The repository includes additional transform sets (including legacy crypto) for real-world compatibility testing.
Quick Start
# Clone the repository
git clone https://github.com/lykinsbd/nn_examples.git
cd nn_examples/isakmp_testing
# Install dependencies with Poetry (both user and root)
poetry install
sudo poetry install
# Test a single target
sudo poetry run python isakmp_tester.py 192.168.1.1
# Test multiple transform sets
sudo poetry run python isakmp_tester.py 192.168.1.1 --test-multiple
# Use Aggressive Mode
sudo poetry run python isakmp_tester.py 192.168.1.1 --aggressive
Why
sudo poetry install? Scapy requires raw socket access (root privileges). Sincesudoruns in a separate environment, dependencies must be installed both as your user and as root.
Example Output
[*] Testing 192.168.1.1
Mode: Main Mode
Encryption: 7
Hash: 4
DH Group: 14
[+] ACCEPTED - Transform set accepted by peer
Responder Cookie: a1b2c3d4e5f67890
Self-Contained Testing
The repository also includes isakmp_listener.py - a test responder for testing without a real VPN device:
# Terminal 1: Start the test listener
sudo poetry run python isakmp_listener.py
# Terminal 2: Test against localhost
sudo poetry run python isakmp_tester.py 127.0.0.1
sudo poetry run python isakmp_tester.py 127.0.0.1 --test-multiple
The listener accepts all proposed transform sets and logs packet details. Good for testing the tester script, learning packet structure, and debugging without VPN hardware.
Note: When testing against localhost (127.0.0.1), you may receive ISAKMP responses even without the listener running. This is the kernel’s UDP socket handling, not actual ISAKMP protocol responses. For realistic testing, use a real ISAKMP/VPN device or test between different machines.
Comparison: Scapy vs Netcat vs ike-scan
| Feature | Scapy | Netcat | ike-scan |
|---|---|---|---|
| Dynamic construction | ✅ | ❌ | ✅ |
| Response parsing | ✅ | ❌ | ✅ |
| Scripting | ✅ | ⚠️ | ⚠️ |
| Learning tool | ✅ | ✅ | ❌ |
| Production ready | ⚠️ | ❌ | ✅ |
| Installation | pip | Built-in | Package manager |
Security Considerations
⚠️ Authorization Required: Only test systems you own or have explicit permission to test. ISAKMP probes are logged by VPN concentrators, firewalls, and IDS/IPS systems. Repeated probes trigger security alerts.
Phase 1 Only: This covers IKE Phase 1 (ISAKMP) only. Establishing a full VPN tunnel requires Phase 2 (IPsec Quick Mode) and proper authentication credentials.
Cryptographic Parameters: Use SHA-256 (not SHA-1), AES-256, and DH Group 14+ (2048-bit minimum).
Troubleshooting
Permission Denied
# Scapy requires root for raw sockets
sudo poetry run python script.py
# Make sure dependencies are installed for root too
sudo poetry install
No Response Received
- Check firewall rules (UDP/500)
- Verify target IP is correct
- Confirm ISAKMP service is running
- Check for NAT between you and target
- VPN concentrators rate-limit ISAKMP attempts - wait 30-60 seconds between tests
Import Errors
# Install missing dependencies
pip install scapy cryptography
Next Steps
- Explore IKEv2 with Scapy
- Build Phase 2 (Quick Mode) packets
- Implement full IKE exchange
- Add support for certificates (RSA signatures)
Conclusion
Scapy bridges the gap between manual packet construction and specialized tools like ike-scan. While netcat teaches you the raw protocol (Part 2) and manual construction reveals the internals (Part 3), Scapy gives you the power to automate and scale your testing.
For production VPN scanning, use dedicated tools like ike-scan or nmap --script ike-version. For learning and custom testing, Scapy is unmatched.