Rating: 5.0

## meta

There have been many nice and high quality challenges of this ctf, but Imposter was my personal favorite, because it is kind of self-contained: One doesn't need any prior special knowledge and everything needed for the solution can be learned in a reasonable time frame. Even if the solution is not clear from the beginning, there are clear objectives one can solve one after the other. Every little step gives a rewarding feeling. I was so HAPPY finally seeing this flag!

This write-up got quite long, but I tried to include the train of thought, because that is something I sometimes miss at some write-ups. At the end everything is obvious and SOO EASY, but for hours it was not.

Since the challenge author probably spend some time for creating this well done challenge, I want to pay my respect by sharing this write-up. Also, there is no tl;dr.

## poking around

The website greets you with an error notice `Invalid session please sign in`
and a login form, where the user is supposed to enter username, password and a 2fa otp.

Since the challenge name is "imposter", it "MUST" have to do something with session hijacking (so I was sure).

Trying `admin:admin` and some other combination lead to nothing, so further discovery:

### password reset
Besides the login, there is a "password reset" function that I just tried once.
The user is supposed to enter the username. In the next step, if the username is valid, the form displays fields with disabled inputs for username and e-mail, prefilled with the data from the username entered in the first step. Oddly enough, for user `admin`, the form shows `[email protected]` which is very unusual and probably the first flaw.
Submission of the form show "Password changed successfully" - but there never was the possibility to actually enter a new password!
Strange, so I ignored this reset form and decided to dig somewhere else.

The registration form looks normal: username e-mail, password, password confirmation. Second step of the registration is to scan a qr code to enable two-factor authentication. I did as I was told to and scanned the code with the Google authenticator app and got a working 2fa code which allowed me to log in with the fresh created account.

After log in, the page allows storing notes. To view a note one must enter the 2fa code again.

It was pretty obvious, that the flag will be hidden in a note of the admin account. But how to log in as `admin`!?

## Cookie

Since the name of the challenge was "imposter", it was clear that it "HAS" to do something with the cookie, therefore I tried to make sense of the cookie. It was displayed as an array with 3 values, for example `eyJpZCI6M30.YE6WjQ.pk1cB9AQUY6sA3Xuin62MJJ-CWI` (entries separated by dots).
But they behaved strangely. Even after an account change, the first value did not change at all. But even after long starring and thinking and trying I was not able to make further sense out of those values, therefore I decided that the cookie is not too important and that I should dig somewhere else.
(Yes, I missed something very obvious here!)

## 2fa

The next step was to further investigate this 2fa: A quick online search showed that there are some security problems with those qr-codes: many vendors and 2fa apps include the username and the issuer together with the code, for users convenience. But the question is: would you write your houses address next to your house key? [[why you shouldn't scan two-factor authentication qr codes](https://medium.com/crypto-punks/why-you-shouldnt-scan-two-factor-authentication-qr-codes-e2a44876a524)]

Inspired by this I decided to scan the qr code with another app and check what details are visible. The codes are in the form of `otpauth://totp/2Password:cfnjo?secret=MNTG42TPGEZDGNBVGY3TQOI%3D&issuer=2Password` (standard usage for topt, as I learned now). One problem (it's not really a problem, it's by design) is, that if someone is able to find the secret, an attacker would be able to calculate the 2fa-code. Therefore, I searched for "the algorithm" to generate these secrets. As far as I have seen (again: maybe/probably missed something) there doesn't is "the" algorithm for it. Another dead end?

One sentence in one of the rfcs ([rfc6238](https://tools.ietf.org/html/rfc6238) and some more) made me think. It was something like "the secret SHOULD be random". By resetting and re-register the same account again, I tried if the secret would change. It did not. Same user-details, same secret! OK, this has to be something!

### sledgehammer against nuts (going nuts?)

After this finding, I started to systematically create accounts with different usernames: "a", "aa", "aaa", "b", "bb", "c", "d" ... and so on. The secret only changed at the first digits!
"From now on, this must be easy!" (haha)

Now I started to get rid of the browser ui and wrote a python script, that registered all accounts near the name "admin". For every letter, there are neighbors. For 'c', the 2 neighbors are 'b' and 'd'. The neighbors of "admin" are e.g. "bdmin", "aemin" and so on.

After some brain cracking, how to permute all the neighbor letters (this stuff always gives me headaches, maybe I should not do computers science? -- Or science at all?), I went for this probably not optimal python solution (if its stupid, and it works, it's not stupid!):

```python
import requests

def neighbours(letter: str, offset: int = 2):
if offset == 0:
return [letter]

if offset > 0:
ords = ord(letter)
mins = max(97, ords - offset)
maxs = min(122, ords + offset)

ret = []
for x in range(mins, maxs + 1):
ret.append(chr(x))

return ret

if __name__ == '__main__':

address = "http://challenge.nahamcon.com:30327/signup"

ranges = []

ax = neighbours("a")
dx = neighbours("d")
mx = neighbours("m")
ix = neighbours("i")
nx = neighbours("n")

for a in ax:
for d in dx:
for m in mx:
for i in ix:
for n in nx:
username = a + d + m + i + n

email = username + "@hier.da"
password = username
data = {"username": username, "email": email, "password": password, "password2": password}
response = requests.post(address, json=data)
print(username, response.json())

```

It seems to work!
I registered all the neighbors within a range of 2 letters: 1874 accounts. (That is why I named this part the "sledgehammer").

```
admhn {'url': 'otpauth://totp/2Password:admhn?secret=MFSG22DOGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admho {'url': 'otpauth://totp/2Password:admho?secret=MFSG22DPGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admhp {'url': 'otpauth://totp/2Password:admhp?secret=MFSG22DQGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admil {'url': 'otpauth://totp/2Password:admil?secret=MFSG22LMGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admim {'url': 'otpauth://totp/2Password:admim?secret=MFSG22LNGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admin {'message': 'This username is already taken please choose another one'}
admio {'url': 'otpauth://totp/2Password:admio?secret=MFSG22LPGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admip {'url': 'otpauth://totp/2Password:admip?secret=MFSG22LQGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admjl {'url': 'otpauth://totp/2Password:admjl?secret=MFSG22TMGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admjm {'url': 'otpauth://totp/2Password:admjm?secret=MFSG22TNGEZDGNBVGY3TQOI%3D&issuer=2Password'}
admjn {'url': 'otpauth://totp/2Password:admjn?secret=MFSG22TOGEZDGNBVGY3TQOI%3D&issuer=2Password'}
```

It really looks like it is a direct mapping of "a" to `MF`, "d" to `SG`, "m" to `22`, "i" to `LM`???
Noooo, sometimes "i" maps to `LP` or `LQ`.

This hit me hard. I was seriously frustrated and began to think about some life decisions (I *really* did!).

This was the second time, I went to bed after not solving this challenge. (I already did some other challenges, so I was still a little bit confident, that I might get this too at the end).

## Cookie 2
Next day I started to talk to my teammate about this. After presenting all my findings, he told me that I should check the cookie again. And we did: The entry `eyJpZCI6M30` is base64 `{"id":3}`. But even with this, the other values didn't make sense. (What did we miss here??)

## being ugly

Since I was sure that the main part of the challenge is to get the 2fa, I even tried the first few-thousand passwords of the [rockyou.txt](https://www.kaggle.com/wjburns/common-password-list-rockyoutxt) -- of course with no success. Sorry orga team, but at this point I felt like YOU made me do it!

This was only possible because the error message differentiate between "wrong credentials" and "wrong otp", so theoretically it would have been possible to brute force the password, even without the 2fa.

## password reset 2
The big thing follows: He asked if the script really sends a passwort-reset mail. For me, it was clear, that a ctf challenge usually does not really send real e-mails. But just to be sure, we checked. We created an [e-mail webhook](webhook.site/) and tried to reset the password of a new registered account: **There really is a reset mail!**

Within the reset-mail there is a reset-token and the url and with this information it was possible to reset account passwords.
But how to reset the `admin`? The token changes for every request, and it seems like it is implemented with a proper method.

As written at the beginning, it was quite odd that the request reset form showed the e-mail of the account. Obvious try was to remove the blocking of the input and enter our webhook email. But this would have been too simple and ended in an error message "The provided email does not contail the user".
After some search for the term "contail" there was another down.

So I decided to poke this form with a stick, and created a little python script to better interact with it. After some thinking it hit me in the face (yes, sometimes I am slow...) that the error wasn't "is not the same" but it was "does not contai[n]". So I tried `[email protected]` AND I got a `success`!

"From now on everything is easy!" (How often did I write this now?)

So we (my teammate returned) thought about how to make this form to send the reset token to our address. We tried to append our address to the real address, separated by `+` (knowing that some (most?) e-mail provider accept `[email protected]` as the same as `[email protected]`). But of course this did not work.

The next Idea was that the tool responsible for sending mails must be able to handle more than one entry in the `TO:` field, so we thought about how it would be possible to give it a list of addresses. But what separator? To make sure not to miss anything (and to further practice python), I changed the script to try all the ascii characters as separator between `[email protected]` and `[email protected]`.
This again was a little bit sledgehammering... but it got the correct result, and we got many reset token.

Now it was easy to change the script to just ask for one mail.
(There have been some difficulties with the requests, so we `try` until the request makes it.)

```python
import requests

if __name__ == '__main__':
address = "http://challenge.nahamcon.com:30714/reset_password"

username = "admin"

his = "[email protected]"
our = "[email protected]"

joiner = ","

email = our + joiner + his
data = {"username": username, "email": email}
print(data)

while True:
try:
response = requests.post(address, json=data)
except:
continue
break

if "The provided email does not contail the user" in response.text:
print(joiner, "does not contail")
elif "Error sending email: HTTP Error 400: Bad Request" in response.text:
print(joiner, "Error sending email: HTTP Error 400: Bad Request")
else:
print(joiner, response.text)

```

With his reset-mail send to both accounts, his and ours, we were able to reset the `admin` password!

## 2fa (again)

But there still was this problem with the not working 2fa.
My teammate killed me with "but you have tried to decode the secret with base32??". No I have not, and I feel utterly stupid for this.
For example `MJSG22LOGEZDGNBVGY3TQOI`, the secret I got for registering the account `bdmin`, is `bdmin123456789`.

Okay, from now on it REALLY was easy.
`admin123456789` is the secret `MFSG22LOGEZDGNBVGY3TQOI` and therefore I now can create the correct qr code to import it to the Google 2fa software. (There are more ways to get the otp, once you have the secret, but this was easiest.) One super easy way to create a qr code is to use [DuckDuckGo](https://duckduckgo.com/?q=qrcode+%27otpauth%3A%2F%2Ftotp%2F2Password%3Aadmin%3Fsecret%3DMFSG22LOGEZDGNBVGY3TQOI%253D%26issuer%3D2Password%27&t=ffab&ia=answer).

## win

Now it was possible to log in with the admin password we changed and the correct otp.

## what I have learned

Very much!
Not just about 2fa otp, an idea how the code actually is calculated, that it is a free and open standard, that one may use any 2fa app and stuff.

At the end I really look forward to other write-ups for this challenge, because I really want to know what I missed for the cookie!