Omnispray: Building a Lockout-Aware Password Spraying Framework with Python Asyncio
Hook
Most password spraying tools will happily lock out hundreds of accounts in seconds. Omnispray treats lockout policies as first-class citizens in its architecture—because the difference between a successful red team engagement and a costly incident response is often just a timer.
Context
Password spraying—attempting a small number of commonly-used passwords across many accounts—remains one of the most effective initial access vectors in penetration testing and red team operations. Unlike traditional brute force attacks that hammer a single account with many passwords, spraying distributes attempts across accounts to stay under lockout thresholds. The challenge is that modern cloud platforms like Office 365, Okta, and Azure AD each have different authentication endpoints, response formats, and lockout policies.
Before unified frameworks like Omnispray, security professionals juggled separate tools for each target: o365spray for Office 365, a different script for OWA, custom PowerShell for Exchange. Each tool had its own command syntax, logging format, and approach to rate limiting. Worse, most treated lockout policies as an afterthought—you manually calculated spray windows, tracked password counts per account, and hoped you'd configured the delays correctly. Omnispray emerged to solve this coordination problem by abstracting the common patterns of authentication spraying into a reusable framework with pluggable modules for different targets.
Technical Insight
At its core, Omnispray is built around Python's asyncio for concurrent execution and a plugin architecture that separates target-specific logic from spray orchestration. The framework handles the hard problems—request throttling, lockout timers, proxy rotation, result aggregation—while modules focus purely on authentication mechanics for specific platforms.
The module system is elegantly simple. Each module inherits from a base class that defines three key methods: spray() for password attempts, enum() for user enumeration, and response parsing. Here's what a simplified module structure looks like:
class Module:
def __init__(self, *args, **kwargs):
self.module_name = 'example'
self.auth_endpoint = 'https://example.com/api/auth'
async def spray(self, username, password, *args, **kwargs):
# Perform authentication attempt
response = await self.execute(username, password)
return self.validate(response)
def validate(self, response):
# Parse response to determine success/failure/lockout
if 'valid_credentials' in response.text:
return {'valid': True, 'locked': False}
elif 'account_locked' in response.text:
return {'valid': False, 'locked': True}
return {'valid': False, 'locked': False}
The framework's scheduler coordinates these modules with sophisticated timing controls. When you configure a spray with --lockout 30 and --count 2, Omnispray tracks how many passwords each account has received and enforces a 30-minute pause after every 2 attempts per user. This isn't just a simple sleep timer—the engine maintains a priority queue of accounts, moving those that have hit their count limit to the back and calculating when they'll be eligible for the next spray cycle.
The async architecture shines in concurrent execution. Rather than spraying sequentially through a user list, Omnispray fans out requests with configurable parallelism (--rate flag). Here's where the jitter system becomes critical for evasion:
# Simplified from core/spray.py
async def execute_spray_wave(self, accounts, password):
tasks = []
for account in accounts:
# Add random jitter before each request
jitter_delay = random.uniform(0, self.jitter_max)
await asyncio.sleep(jitter_delay)
task = asyncio.create_task(
self.module.spray(account, password)
)
tasks.append(task)
# Rate limiting: only N concurrent requests
if len(tasks) >= self.rate_limit:
results = await asyncio.gather(*tasks)
tasks = []
await self.process_results(results)
This pattern prevents the thundering herd problem where hundreds of authentication requests hit a target simultaneously—a signature that security tools easily detect. Instead, requests arrive in waves with randomized spacing that mimics organic login patterns.
The proxy integration deserves special attention. Omnispray supports standard HTTP proxies, but also integrates with FireProx—a tool that creates disposable AWS API Gateway endpoints to rotate source IPs. When you enable FireProx mode, Omnispray inserts custom headers (X-My-X-Forwarded-For) that the gateway uses to proxy requests, giving each spray wave a different origin IP without managing proxy lists manually. This is particularly effective against cloud services that rate-limit by source IP.
Result tracking happens in real-time with a color-coded terminal output and structured logging to files. The framework distinguishes between valid credentials, invalid users, locked accounts, and errors—critical for post-spray analysis. When a module's validate() method returns locked: True, that account is immediately flagged and excluded from remaining spray waves to prevent further lockout extension.
Gotcha
Omnispray's lockout management only works if you correctly configure the target's actual policy. The tool doesn't automatically detect that Office 365 locks accounts after 10 attempts in 5 minutes—you need to research the target environment and set --lockout and --count accordingly. Misconfigure these values and you'll either lock accounts (too aggressive) or spray painfully slowly (too conservative). The framework trusts your policy knowledge; it doesn't probe or adapt.
Module coverage is the other limitation. While O365 and OWA modules are mature, you'll need to write custom modules for less common targets. The documentation for module development is sparse—mostly reading existing modules as examples rather than following a comprehensive guide. If your target uses non-standard authentication flows (multi-step OAuth, CAPTCHA, custom MFA implementations), you'll spend significant time reverse-engineering responses and handling edge cases. The async architecture also means debugging failed requests is harder than synchronous scripts; errors bubble up through asyncio's gather/await chain with less intuitive stack traces.
Verdict
Use Omnispray if you're conducting authorized penetration tests or red team engagements against multiple cloud platforms and need professional-grade lockout policy management, proxy rotation, and a consistent interface across targets. It's ideal when you're operating at scale—hundreds of accounts, multiple spray cycles, and operational security requirements that demand request jitter and IP rotation. The framework shines for experienced penetration testers who understand authentication attack vectors and can configure lockout policies correctly. Skip it if you're targeting a single platform where dedicated tools like o365spray are more battle-tested, if you need extensive documentation and community support for troubleshooting, if you lack authorization for password spraying (this is an offensive tool with legal risks), or if you prefer GUI-based tools over command-line workflows. This is a power tool for professionals who accept the responsibility of avoiding account lockouts and operating within legal boundaries.