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 process with Python.
Why Scapy?#
While netcat with hex strings works for one-off tests, Scapy offers several advantages:
- Dynamic packet construction - Build packets programmatically
- Protocol awareness - Scapy understands ISAKMP/IKE structure
- Automatic field calculation - Length fields, checksums handled automatically
- Interactive testing - Modify and resend packets easily
- Response parsing - Decode and analyze responses
- Scripting - Automate testing across multiple targets
What You’ll Learn#
- Installing and configuring Scapy
- Building ISAKMP packets with Scapy’s ISAKMP layer
- Sending packets and capturing responses
- Parsing ISAKMP responses
- Creating reusable test scripts
- Comparing different transform sets
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_type=1, # ISAKMP
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
}
# Alternative: Legacy transform set for compatibility
legacy_transform_set = {
'encryption': ENCR_3DES_CBC,
'key_length': 0, # Not applicable for 3DES
'hash': HASH_SHA1,
'dh_group': DH_GROUP_5,
'auth_method': AUTH_PSK,
'lifetime': 86400
}
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 may be rejected")
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': 2, # SHA-1
'dh_group': 5, # 1536-bit
'auth_method': 1,
'lifetime': 86400
},
# Legacy crypto
{
'encryption': 5, # 3DES-CBC
'key_length': 0,
'hash': 2, # SHA-1
'dh_group': 2, # 1024-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
load=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
load=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
# Use Main Mode unless you need the speed of 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, production-ready script that combines all the techniques from this post. The full script is available in the nn_examples repository.
Key Features#
- Argument parsing with argparse for flexible command-line usage
- Multiple transform set testing to find accepted configurations
- Main Mode and Aggressive Mode support
- Formatted output with clear status indicators
- Error handling for common issues
- Root permission checking
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 received packet details, making it perfect for:
- Testing the tester script itself
- Learning ISAKMP packet structure
- Debugging packet construction issues
- Demonstrations without VPN hardware
Note: When testing against localhost (127.0.0.1), you may receive ISAKMP responses even without the listener running. This is due to how the loopback interface works - as explained in the Scapy documentation, packets on loopback are handled internally by the kernel and Scapy’s L3 sockets, not through normal packet assembly/disassembly. For realistic testing, use a real ISAKMP/VPN device or test between different machines.
The script handles all the complexity we’ve discussed: packet construction, attribute encoding, response parsing, and error handling. Use it as a starting point for your own ISAKMP testing tools.
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 |
Practical Applications#
- VPN troubleshooting - Test transform set compatibility
- Security auditing - Identify weak cryptographic parameters
- Automation - Script VPN endpoint discovery
- Protocol learning - Understand ISAKMP/IKE internals
- Development - Test VPN implementations
Security Considerations#
⚠️ Authorization Required: Only test systems you own or have explicit permission to test. Unauthorized VPN probing may violate:
- Computer Fraud and Abuse Act (CFAA)
- Network security policies
- Terms of service agreements
🔒 Cryptographic Security: The examples use modern cryptographic parameters:
- Hash: SHA-256 (avoid SHA-1)
- Encryption: AES-256-CBC or AES-256-GCM
- DH Group: 14+ (2048-bit or higher)
- Consider IKEv2 for new deployments
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
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. It provides the flexibility of custom packet building with the convenience of Python scripting. 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 ISAKMP testing.
Key Takeaways:
- Scapy automates ISAKMP packet construction
- Python scripting enables flexible testing workflows
- Response parsing provides immediate feedback
- Ideal for testing multiple transform sets
- Balances learning with practical utility
For production VPN scanning, consider dedicated tools like ike-scan or nmap --script ike-version. For learning and custom testing, Scapy is unmatched.