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). 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 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#

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

References#