robertchen.cc twitter
This is an account takeover attack I discovered on the open source Secret Hitler game.
By submitting crafted parameters to the /password-reset
endpoint, attackers are able to takeover arbitrary non-staff accounts.
This vulnerability can be mitigated by disabling JSON parsing.
We control all of the parameters passed through req.body
.
const { username, password, password2, tok } = req.body;
The code that searches for a token is as follows.
ResetPassword.findOneAndDelete({ username, token: tok, expirationDate: { $gte: now } })
In order to satisfy this, we can set
tok = {
$ne: 1
}
The $ne
specifier will return any object where the validation token is not equal to 1.
The code that loads the user profile to reset the password for is as follows.
Account.findOne({ username: req.body.username })
Note that the crafted username will have to satisfy both the token search and the user search. In other words, the specifier we use must both have a reset token, and the account of the user we want to takeover.
This last constraint requires a bit more creativity to satisfy. We have two options.
username
specifier to specify a large set of accounts (e.g. {$ne: 1}
), and hope Account.findOne
chooses the account we want.Option 2 offers slightly more promise, and eventually lead me to the arbitrary account takeover.
By creating an account with username z
, I could ensure that in terms of string comparisons, my account was very high on the list.
Then, setting
username = {
$gte: "account_to_takeover"
}
finishes the exploit. As long as there is a valid reset token from account z
, we can takeover any account with name less than z
. Note that by extending the username to zzzzz...
, we can ensure arbitrary account takeover.
The ResetPassword
constraint is satisfied because it will find the reset token generated by z
. The Account
constraint is also satisfied because findOne
will return the first object according to the natural sort order (alphabetically), which would be the account with name equal to the $gte$
condition.
Aside from staff accounts which are hardcoded to never accept any password resets
if (!account || account.staffRole) {
// Error
}
this method suffices.