Creating Payload Plugins

Creating Payload Plugins

The following files were used as context for generating this wiki page:

Purpose and Scope

This document provides a technical guide for creating custom payload plugins in WSHawk. Payload plugins extend WSHawk's testing capabilities by providing additional vulnerability test vectors beyond the built-in 22,000+ payload collection. This page covers the PayloadPlugin base class implementation, metadata requirements, and lifecycle management.

For general plugin system architecture and the PluginManager, see Plugin Architecture Overview. For creating custom vulnerability detectors, see Creating Detector Plugins. For custom protocol handlers, see Creating Protocol Plugins.


PayloadPlugin Class Architecture

The PayloadPlugin class serves as the base interface for all custom payload implementations. It inherits from PluginBase and provides a standardized contract for returning vulnerability-specific test vectors.

Class Hierarchy and Core Methods

classDiagram
    class PluginBase {
        <<abstract>>
        +get_metadata() PluginMetadata
        +get_name() str
        +get_version() str
        +get_description() str
    }
    
    class PayloadPlugin {
        <<abstract>>
        +get_payloads(vuln_type: str) List[str]
        +get_payload_count(vuln_type: str) int
    }
    
    class CustomXSSPayloads {
        +get_metadata() PluginMetadata
        +get_payloads(vuln_type: str) List[str]
    }
    
    class PluginMetadata {
        +name: str
        +version: str
        +description: str
        +author: str
        +requires: List[str]
        +min_wshawk_version: str
        +checksum: str
        +to_dict() Dict
        +from_dict(data: Dict) PluginMetadata
    }
    
    PluginBase <|-- PayloadPlugin
    PayloadPlugin <|-- CustomXSSPayloads
    PluginBase --> PluginMetadata

Sources: wshawk/plugin_system.py:35-63

Code Entity Mapping

The following table maps the conceptual components to their code implementations:

| Concept | Code Entity | Location | Purpose | |---------|-------------|----------|---------| | Base Interface | PluginBase | wshawk/plugin_system.py:35-51 | Abstract base for all plugin types | | Payload Interface | PayloadPlugin | wshawk/plugin_system.py:52-63 | Abstract interface for payload providers | | Metadata Container | PluginMetadata | wshawk/plugin_system.py:17-33 | Dataclass holding plugin metadata | | Plugin Registry | PluginManager._payload_plugins | wshawk/plugin_system.py:114 | Dictionary storing loaded payload plugins | | Lazy Loader | PluginManager._load_plugin_lazy() | wshawk/plugin_system.py:158-201 | On-demand plugin loading mechanism | | Payload Retriever | PluginManager.get_payloads() | wshawk/plugin_system.py:335-373 | Cached payload access with LRU cache |

Sources: wshawk/plugin_system.py:35-63, wshawk/plugin_system.py:17-33


PluginMetadata Structure

Every payload plugin must provide a PluginMetadata instance through the get_metadata() method. This metadata is used for validation, version compatibility checking, and plugin registration.

Required and Optional Fields

| Field | Type | Required | Default | Purpose | |-------|------|----------|---------|---------| | name | str | Yes | N/A | Unique plugin identifier (used as registry key) | | version | str | Yes | N/A | Semantic version (e.g., "1.0.0") | | description | str | Yes | N/A | Human-readable plugin description | | author | str | No | "Regaan" | Plugin author name | | requires | List[str] | No | None | Python package dependencies | | min_wshawk_version | str | No | "2.0.0" | Minimum WSHawk version required | | checksum | str | No | "" | SHA-256 checksum for integrity validation |

Metadata Validation Flow

flowchart TD
    Start["PluginManager._validate_plugin()"] --> CheckName{"name and version<br/>populated?"}
    CheckName -->|No| Fail1["Return False<br/>'missing name or version'"]
    CheckName -->|Yes| CheckVersion{"version format<br/>valid semver?"}
    
    CheckVersion -->|No| Fail2["Return False<br/>'invalid version format'"]
    CheckVersion -->|Yes| CheckCompat{"min_wshawk_version<br/>compatible?"}
    
    CheckCompat -->|No| Fail3["Return False<br/>'version incompatible'"]
    CheckCompat -->|Yes| Success["Return True<br/>plugin valid"]
    
    Fail1 --> End
    Fail2 --> End
    Fail3 --> End
    Success --> End["End"]

Validation Logic:

Sources: wshawk/plugin_system.py:203-235, wshawk/plugin_system.py:17-33


Implementing get_payloads()

The get_payloads() method is the core interface that WSHawk calls to retrieve vulnerability test vectors. It must return a list of strings based on the requested vulnerability type.

Method Signature

def get_payloads(self, vuln_type: str) -> List[str]:
    """
    Return payloads for specific vulnerability type
    
    Args:
        vuln_type: Vulnerability category identifier
                   Examples: "xss", "sqli", "nosql", "xxe", "ssrf", 
                            "path_traversal", "ssti", "command_injection"
    
    Returns:
        List of string payloads for testing the specified vulnerability type
        Return empty list if vuln_type is not supported
    """

Sources: wshawk/plugin_system.py:55-58

Vulnerability Type Mapping

WSHawk recognizes the following standard vulnerability type identifiers:

| vuln_type Value | Vulnerability Category | Example Payloads | |-------------------|------------------------|------------------| | "xss" | Cross-Site Scripting | <script>alert(1)</script>, <svg onload=alert(1)> | | "sqli" | SQL Injection | ' OR '1'='1, 1' UNION SELECT NULL-- | | "nosql" | NoSQL Injection | {"$ne": null}, {"$gt": ""} | | "xxe" | XML External Entity | <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> | | "ssrf" | Server-Side Request Forgery | http://169.254.169.254/latest/meta-data/ | | "path_traversal" | Path/Directory Traversal | ../../../etc/passwd, ....//....//etc/passwd | | "ssti" | Server-Side Template Injection | {{7*7}}, <%= 7*7 %> | | "command_injection" | Command Injection | ; ls -la, | whoami |

Plugins can define custom vulnerability types but should document them in the description field of PluginMetadata.

Sources: wshawk/plugin_system.py:455-465

Implementation Pattern

flowchart LR
    Scanner["Scanner Request"] --> GetPayloads["plugin.get_payloads(vuln_type)"]
    GetPayloads --> Check{"vuln_type<br/>supported?"}
    Check -->|Yes| Load["Load/Generate<br/>Payloads"]
    Check -->|No| Empty["Return []"]
    Load --> Return["Return List[str]"]
    Empty --> End
    Return --> End["End"]

The reference implementation demonstrates conditional payload loading:

wshawk/plugin_system.py:455-465

def get_payloads(self, vuln_type: str) -> List[str]:
    if vuln_type == "xss":
        # In production, load from file or database
        return [
            "<svg onload=alert(1)>",
            "<img src=x onerror=alert(1)>",
            "<body onload=alert(1)>",
            "javascript:alert(1)",
            "<iframe src=javascript:alert(1)>"
        ]
    return []

Sources: wshawk/plugin_system.py:443-465


Payload Loading Strategies

Payload plugins can use various strategies for storing and loading payloads, depending on scale and performance requirements.

Strategy Comparison

| Strategy | Implementation | Pros | Cons | Best For | |----------|----------------|------|------|----------| | Inline Lists | Return hardcoded list in method | Simple, no I/O | Limited scale, maintenance burden | Small payload sets (<100) | | File Loading | Read from text/JSON files | Organized, versionable | I/O overhead, error handling | Medium sets (100-10,000) | | Database Loading | Query SQLite/external DB | Scalable, queryable | Complex setup, dependencies | Large sets (>10,000) | | Generator Functions | yield payloads dynamically | Memory efficient | Can't cache easily | Algorithmic generation | | Lazy Loading | Load on first get_payloads() call | Deferred cost | State management | Expensive-to-load payloads |

File-Based Loading Pattern

For medium-sized payload collections, file-based loading is recommended:

class FileBasedPayloads(PayloadPlugin):
    def __init__(self):
        self.payload_dir = os.path.join(os.path.dirname(__file__), "payloads")
        self._cache = {}  # Optional: cache loaded payloads
    
    def get_payloads(self, vuln_type: str) -> List[str]:
        # Check cache first
        if vuln_type in self._cache:
            return self._cache[vuln_type]
        
        # Load from file
        file_path = os.path.join(self.payload_dir, f"{vuln_type}.txt")
        if os.path.exists(file_path):
            with open(file_path, 'r') as f:
                payloads = [line.strip() for line in f if line.strip()]
            self._cache[vuln_type] = payloads
            return payloads
        
        return []

Note: The PluginManager.get_payloads() method applies @lru_cache(maxsize=128) at the manager level wshawk/plugin_system.py:335, so individual plugins don't need to implement their own caching for repeated calls with the same vuln_type.

Sources: wshawk/plugin_system.py:335-373


Plugin Lifecycle and Registration

Understanding the plugin lifecycle is essential for implementing efficient payload loading and avoiding common pitfalls.

Lifecycle Stages

stateDiagram-v2
    [*] --> Discovered: PluginManager._scan_available_plugins()
    Discovered --> Validated: PluginManager._load_plugin_lazy()
    Validated --> Registered: PluginManager._register_plugin_internal()
    Registered --> Active: First get_payloads() call
    Active --> Cached: Result cached in LRU
    Cached --> Active: Cache hit
    
    note right of Discovered
        Plugin file detected
        Checksum calculated
        Added to _available_plugins
    end note
    
    note right of Validated
        Module imported
        Class instantiated
        Metadata validated
    end note
    
    note right of Registered
        Added to _payload_plugins
        Name collision checked
        Ready for use
    end note

Lazy Loading Mechanism

The PluginManager implements lazy loading to minimize startup overhead and memory usage:

  1. Scan Phase wshawk/plugin_system.py:141-156: _scan_available_plugins() discovers all .py files in the plugin directory without importing them
  2. Lazy Load Phase wshawk/plugin_system.py:158-201: _load_plugin_lazy() imports and instantiates the plugin only when get_payloads() is called
  3. Caching Phase wshawk/plugin_system.py:335: Results are cached with @lru_cache to avoid repeated computation

This design means:

  • Plugin __init__() methods are NOT called until the plugin is first used
  • Expensive initialization (file loading, network requests) should be deferred to get_payloads() or use lazy attributes
  • Plugins are loaded once and reused across multiple scans

Sources: wshawk/plugin_system.py:141-201, wshawk/plugin_system.py:335-373


Complete Implementation Example

The following example demonstrates a production-ready payload plugin with all required components:

Example: Advanced SQLi Payloads

# File: plugins/advanced_sqli.py
from wshawk.plugin_system import PayloadPlugin, PluginMetadata
from typing import List
import os

class AdvancedSQLiPayloads(PayloadPlugin):
    """
    Advanced SQL injection payloads covering:
    - MySQL, PostgreSQL, MSSQL, Oracle
    - Time-based blind techniques
    - Boolean-based blind techniques
    - Union-based extraction
    - Error-based extraction
    """
    
    def get_metadata(self) -> PluginMetadata:
        return PluginMetadata(
            name="advanced_sqli",
            version="1.2.0",
            description="Advanced SQL injection payloads for multiple databases",
            author="Your Name",
            requires=[""],  # No external dependencies
            min_wshawk_version="2.0.0"
        )
    
    def get_payloads(self, vuln_type: str) -> List[str]:
        """Return SQL injection payloads"""
        
        if vuln_type != "sqli":
            return []  # Only handle SQLi
        
        # Return comprehensive payload set
        return [
            # MySQL Time-based
            "' AND SLEEP(5)--",
            "' AND BENCHMARK(10000000,MD5('A'))--",
            
            # PostgreSQL Time-based
            "'; SELECT pg_sleep(5)--",
            
            # MSSQL Time-based
            "'; WAITFOR DELAY '00:00:05'--",
            
            # Boolean-based
            "' AND '1'='1",
            "' AND '1'='2",
            "' OR 1=1--",
            "' OR '1'='1'--",
            
            # Union-based
            "' UNION SELECT NULL--",
            "' UNION SELECT NULL,NULL--",
            "' UNION SELECT NULL,NULL,NULL--",
            
            # Error-based
            "' AND 1=CONVERT(int, (SELECT @@version))--",
            "' AND 1=1/0--",
            
            # Stacked queries
            "'; DROP TABLE users--",
            "'; INSERT INTO users VALUES (1,'admin','pass')--"
        ]

Registration Flow

sequenceDiagram
    participant S as Scanner
    participant PM as PluginManager
    participant F as File System
    participant P as PayloadPlugin
    
    Note over PM,F: Initialization Phase
    PM->>F: _scan_available_plugins()
    F-->>PM: advanced_sqli.py detected
    PM->>PM: Calculate checksum
    PM->>PM: Add to _available_plugins
    
    Note over S,PM: First Usage (Lazy Load)
    S->>PM: get_payloads("sqli")
    PM->>PM: Check if loaded
    PM->>F: importlib.import_module()
    F-->>PM: Module loaded
    PM->>P: Instantiate class
    P-->>PM: Instance created
    PM->>P: get_metadata()
    P-->>PM: PluginMetadata
    PM->>PM: _validate_plugin()
    PM->>PM: _register_plugin_internal()
    PM->>PM: Add to _payload_plugins
    PM->>P: get_payloads("sqli")
    P-->>PM: List[str] payloads
    PM->>PM: Cache result (@lru_cache)
    PM-->>S: Payloads
    
    Note over S,PM: Subsequent Calls (Cached)
    S->>PM: get_payloads("sqli")
    PM->>PM: Check cache
    PM-->>S: Cached payloads (no plugin call)

Sources: wshawk/plugin_system.py:158-201, wshawk/plugin_system.py:263-313, wshawk/plugin_system.py:335-373


Best Practices

1. Naming Conventions

| Element | Convention | Example | Rationale | |---------|------------|---------|-----------| | Class Name | PascalCase + "Payloads" suffix | CustomXSSPayloads | Distinguishes payload plugins from detectors | | Plugin Name | snake_case | "custom_xss" | Registry key, must be unique | | File Name | snake_case.py | custom_xss.py | Auto-discovered by _scan_available_plugins() |

Sources: wshawk/plugin_system.py:443, plugins/README.md:15-28

2. Error Handling

Payload plugins should handle errors gracefully:

def get_payloads(self, vuln_type: str) -> List[str]:
    try:
        if vuln_type == "custom":
            # Load from external source
            return self._load_from_api()
    except FileNotFoundError:
        print(f"[WARNING] Payload file not found for {vuln_type}")
        return []
    except Exception as e:
        print(f"[ERROR] Failed to load {vuln_type}: {e}")
        return []
    
    return []

The PluginManager wraps calls in try-except blocks wshawk/plugin_system.py:355-359, but plugins should handle their own domain-specific errors.

Sources: wshawk/plugin_system.py:355-371

3. Performance Considerations

  • Avoid expensive operations in __init__(): Initialization happens during lazy load, but before payloads are needed
  • Use lazy attributes for file I/O: Load files only when get_payloads() is called
  • Return empty lists for unsupported types: Don't raise exceptions, as the manager queries all plugins
  • Leverage LRU caching: The manager caches results, so don't duplicate caching logic

Sources: wshawk/plugin_system.py:335

4. Documentation

Include docstrings that specify:

  • Supported vuln_type values
  • Payload characteristics (e.g., "includes time-based blind techniques")
  • External dependencies in requires field
  • Version history in changelog format

Sources: wshawk/plugin_system.py:446-453


Plugin Discovery and Loading

Plugins must be placed in the plugins/ directory to be automatically discovered. The directory structure should be:

wshawk/
├── wshawk/
│   └── plugin_system.py
└── plugins/
    ├── README.md
    ├── custom_xss.py         # Auto-discovered
    ├── advanced_sqli.py      # Auto-discovered
    └── __pycache__/          # Ignored (starts with _)

Files are discovered based on:

Sources: wshawk/plugin_system.py:141-156, plugins/README.md:1-37


Testing Your Plugin

To verify your plugin implementation:

  1. Manual Registration Test:
from wshawk.plugin_system import PluginManager
from plugins.my_plugin import MyPayloadPlugin

manager = PluginManager()
plugin = MyPayloadPlugin()

# Test metadata
metadata = plugin.get_metadata()
print(f"Name: {metadata.name}, Version: {metadata.version}")

# Test payload retrieval
payloads = plugin.get_payloads("xss")
print(f"Loaded {len(payloads)} XSS payloads")

# Test registration
if manager.register_plugin(plugin):
    print("Registration successful")
  1. Automatic Discovery Test:
manager = PluginManager()
manager.load_all_plugins()  # Force load all plugins

plugins = manager.list_plugins()
print(f"Loaded plugins: {plugins['payload_plugins']}")

# Test retrieval through manager
payloads = manager.get_payloads("xss")
print(f"Total XSS payloads from all plugins: {len(payloads)}")

Sources: wshawk/plugin_system.py:498-550


Integration with WSHawk Scanner

Once registered, payload plugins are automatically integrated into the scanning workflow:

flowchart TB
    Scanner["WSHawkV2 Scanner"] --> PayloadGen["Generate Test Payloads"]
    PayloadGen --> BuiltIn["WSPayloads<br/>(22,000+ built-in)"]
    PayloadGen --> Manager["PluginManager.get_payloads()"]
    
    Manager --> Load["Lazy load plugins"]
    Load --> Query["Query all PayloadPlugin instances"]
    Query --> Plugin1["Plugin 1<br/>get_payloads()"]
    Query --> Plugin2["Plugin 2<br/>get_payloads()"]
    Query --> PluginN["Plugin N<br/>get_payloads()"]
    
    Plugin1 --> Merge["Merge Results"]
    Plugin2 --> Merge
    PluginN --> Merge
    BuiltIn --> Merge
    
    Merge --> Inject["Inject payloads<br/>into WebSocket"]
    Inject --> Verify["VulnerabilityVerifier<br/>analyze response"]

Plugins extend but do not replace the core payload system. Both built-in and plugin-provided payloads are used in scans.

Sources: wshawk/plugin_system.py:335-373