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). Since sudo runs 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

FeatureScapyNetcatike-scan
Dynamic construction
Response parsing
Scripting⚠️⚠️
Learning tool
Production ready⚠️
InstallationpipBuilt-inPackage 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.

References