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:
_is_valid_version()checks for three dot-separated numeric components wshawk/plugin_system.py:237-243_is_compatible_version()enforces major version match and minor version >= requirement wshawk/plugin_system.py:245-261- Validation occurs before registration in
_register_plugin_internal()wshawk/plugin_system.py:263-313
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:
- Scan Phase wshawk/plugin_system.py:141-156:
_scan_available_plugins()discovers all.pyfiles in the plugin directory without importing them - Lazy Load Phase wshawk/plugin_system.py:158-201:
_load_plugin_lazy()imports and instantiates the plugin only whenget_payloads()is called - Caching Phase wshawk/plugin_system.py:335: Results are cached with
@lru_cacheto 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_typevalues - Payload characteristics (e.g., "includes time-based blind techniques")
- External dependencies in
requiresfield - 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:
- Located in
plugin_dir(default:"plugins/") wshawk/plugin_system.py:109 - Filename ends with
.pywshawk/plugin_system.py:147 - Filename does NOT start with
_wshawk/plugin_system.py:147
Sources: wshawk/plugin_system.py:141-156, plugins/README.md:1-37
Testing Your Plugin
To verify your plugin implementation:
- 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")
- 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