ASIS CTF web - hello
The task hello was published on ASIS CTF Quals 2023.
The task includes a link to a webpage:
The code is a short PHP script. The script checks if, in the GET request in URL, there is a variable equals `x` and if it does not contain the words `file` or `next`, if conditions are met, then the `curl` is executed for this `url` that is wrapped in `escapeshellarg`, quoting from php.net manual:
adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument.
The general idea for this step is to bypass these conditions to construct a string equivalent to `file:///next.txt`.
Let’s look at the curl manpage in a terminal: `man curl`. One of the beginning sections is the one below. It says that we can use URL globbing to specify URLs.
Curly braces specify a list of strings that can be matched like in this example `http://site.{one,two,three}.com`. We can construct a similar list with one word that is a substring for `file` and `next` to bypass the check:
http://45.147.231.180:8000/?x={f}ile:///{n}ext.txt
It worked, and the next step showed:
‘wow... so next step is available at http://45.147.231.180:8001/39c8e…’
We enter the URL and get it. The webpage says:
‘did you know i can read files?? amazing right,,, maybe try /39c8e…/read/?file=/proc/self/cmdline’
Let’s try to execute this URL in a web browser. And we get base64 encoded string:
`L2Jpbi9idW4tMS4wLjIAL2FwcC9pbmRleC5qcwA= `
After decoding we have:
`/bin/bun-1.0.2�/app/index.js�`
This file `/proc/self/cmdline` provides information about command line argument that was used to launch currently executing process. We can read them by changing the URL that we want to read. `/app/index.js` seems useful to figure out what to do next.
const fs = require('node:fs');
const path = require('path')
/*
I wonder what is inside /next.txt
*/
const secret = '39c8e9...'
const server = Bun.serve({
port: 8000,
fetch(req) {
let url = new URL(req.url);
let pname = url.pathname;
if(pname.startsWith(`/${secret}`)){
if(pname.startsWith(`/${secret}/read`)){
try{
let fpath = url.searchParams.get('file');
console.log(path.basename(fpath));
if(path.basename(fpath).indexOf('next') == -1){
return new Response(fs.readFileSync(fpath).toString('base64'));
} else {
return new Response('no way');
}
} catch(e){ }
return new Response("Couldn't read your file :(");
}
return new Response(`did you know i can read files?? amazing right,,, maybe try /${secret}/read/?file=/proc/self/cmdline`);
}
return
}
});
The hint says that we have to read `next.txt` file. This script runs a server that fetches methods to handle incoming requests. We parse a URL. Then, search for file arguments in the URL. Next, we have interesting fragment:
‘ if(path.basename(fpath).indexOf('next') == -1) ’.
We should take a closer look at it. We parse `fpath` with a `basename` function and then check if the string `next` is included in it, but what `basename` returns? It extracts the filename part of the path. For example, from `/a/b/c/d` it extracts `d`. So this condition checks if we do not try to read a `next.txt` file. Then, if the condition is met we read a file that is base64 encoded and returned in a request. The rest of the code handles other cases and responses to requests.
At this stage, we know that we need to read `next.txt` file, but it any path that point to it will not be allowed!
We need to find a string that meets the condition, but it somehow treated differently by `fs.readFileSync` and `basename`.
`fs` is a system module that allows us to work with a file system. We know that somewhere under the hood, on Linux, system calls take NULL-terminated strings, so we can wonder how the NULL character (`%00`) is handled. If there is no special handing in Node.js standard library, it might just make the system call ignore the rest of the string. Let’s try:
'http://45.147.231.180:8001/39c8e9953fe8ea40ff1c59876e0e2f28/read/?file=/next.txt%00/foo'
It works! basename(“/next.txt\0/foo”) = “/foo”, so the path passes the filter.
Now it's time for a whitebox challenge.
Find the hidden subdomain and then a secret endpoint and only then you may receive your flag.
Link to the website: `anVzdCBraWRkaW5nLiBBU0lTe2dvb2Rfam9iX2J1bn0gCg==`
Let’s decode:
just kidding. ASIS{FLAG}
Happy hacking! Bye!