cliBrowser (Reversing) - 0x41414141 CTF [EN]

This challenge consisted of a hint:

Man I am so tired of that cringe web stack, it’s such a joke. So I made my web app in the cli, because what’s better than fancy terminal

And a binary named client that upon execution printed a banner (very cool btw) and displayed a prompt.

First impressions and reversing

I started by entering some random input and got back invalid input provided, so the logical step was to search for this string. I jumped into IDA but I couldn’t find it in the strings subview and, in the process, observed that the binary was rather complex. This made me suspect that I wasn’t dealing with a binary written in C and using file I learned that it was a Go binary:

$ file client
client: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=KzYxyAuY9brNuX9NUS2u/ym8cURI9XeMgdiUoZr5J/DKah0rAnpfyTzOgMU17e/c5XvA9s2PmKZA8gTeXuL, not stripped

However, I could find the text invalid input provided using the “sequence of bytes” search option and it appeared that all strings were concatenated together as one long string that IDA failed to recognize.

output

It turns out that in Go, strings are not stored as null-terminated strings in the compiled binary. They are concatenated one after the other in the .rodata section. Each sequence of bytes is pointed from a struct that also contains the string length. This was the struct that I defined:

output

It was referenced by a function called main_get_input which was kind of complicated xD.

output

But I found that it compared some backward strings like exit or help.

output

And entering help as input printed a menu that displayed the valid inputs:

➜ help
help page :
- login 
- hello 
- ping
- flag
- exit

However, there was one more string in the function that was not listed in the help menu: secret. When used as input the binary printed a string that appeared to be some kind of easter egg.

➜ secret
0x41414141 is 1337

Intercepting network traffic

The name of the binary, the options displayed, and the hint suggested that the binary was just an interface for some API. So I decided to use the program while listening to the network traffic using Wireshark.

I tried to log in as admin and get the flag but failed. Then, I repeated the process with a random user but failed again with a different error. However, the packets captured revealed some interesting information:

POST /login HTTP/1.1
Host: 161.97.176.150:6666
User-Agent: Go-http-client/1.1
Content-Length: 29
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip

password=admin&username=admin

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Www-Authenticate: JWT realm=test zone
Date: Mon, 25 Jan 2021 16:24:30 GMT
Content-Length: 55

{"code":401,"message":"incorrect Username or Password"}

GET /auth/flag HTTP/1.1
Host: 161.97.176.150:6666
User-Agent: Go-http-client/1.1
Authorization: Bearer
Accept-Encoding: gzip

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Www-Authenticate: JWT realm=test zone
Date: Mon, 25 Jan 2021 16:24:32 GMT
Content-Length: 46

{"code":401,"message":"cookie token is empty"}

When I tried to log in as admin the binary made a POST request to /login, but the response code was 401. A GET request was made to /auth/flag when I tried to retrieve the flag and, again, the response was a 401. It called my attention that the Authorization header was set in the request but there was no token and both responses had the www-Authenticate header type set to JWT. Now comes the interesting part. When I logged in as a random user, I got back a token and, when requested the flag, the response code changed to 200:


POST /login HTTP/1.1
Host: 161.97.176.150:6666
User-Agent: Go-http-client/1.1
Content-Length: 27
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip

password=test&username=test

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 25 Jan 2021 16:09:50 GMT
Content-Length: 211

{"code":200,"expire":"2021-01-25T18:09:50+01:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE1OTQ1OTAsImlkIjoidGVzdCIsIm9yaWdfaWF0IjoxNjExNTkwOTkwfQ.NmQ-N0hqJaY4NtjGJdJm1XFlsozyt-tB4CYLdtL4QoM"}

GET /auth/flag HTTP/1.1
Host: 161.97.176.150:6666
User-Agent: Go-http-client/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE1OTQ1OTAsImlkIjoidGVzdCIsIm9yaWdfaWF0IjoxNjExNTkwOTkwfQ.NmQ-N0hqJaY4NtjGJdJm1XFlsozyt-tB4CYLdtL4QoM
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 25 Jan 2021 16:09:51 GMT
Content-Length: 64

{"flag":"sorry but you don't have access to this functionality"}

So what are JWTs anyway?

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

They are frequently used in web applications where, after a client successfully authenticates, the server generates and sends a token that has the claim “logged in as someusername”. The client can then use the token to prove to the server that it has logged in correctly. These tokens can be signed using a private key (usually called secret) that the server can use to verify their authenticity.

Now that I had a token, I went to jwt.io to decode its claim. The content was:

{
  "exp": 1611594590,
  "id": "test",
  "orig_iat": 1611590990
}

And it was signed using HMAC-SHA256.

Profit

Up to this point, I knew that:

  • The binary was an interface for an API.
  • Whenever a user authenticated, the API returned a JWT.
  • The JWT contained the username in the id field.
  • Authentication worked for any user but admin (I tried some more :P).
  • The token was verified by the server when I tried to retrieve the flag.

It was reasonable to think that if I crafted a JWT claiming to be the admin I could get the flag. The only problem was that I didn’t have the secret to sign it. Then, I remembered that the binary had the secret option that printed 0x41414141 is 1337. It turned out that this was the private key used to sign the JWTs, so I crafted a new token claiming to be the admin and used it to get the flag:

$ curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE1OTg0MDcsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYxMTU5NDgwN30.-w7i7E_b5BsmVFZuMS4HTUtmKuWFzXVetbzTr9ywc9I" http://161.97.176.150:6666/auth/flag
{"flag":"flag{3v3ry7h1ng_1s_ins3cure_:P}"}