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.

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.

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.

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.

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.

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).

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.

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.

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

Solution

Putting everything together in pwntools to automate the attack.

Flag: corctf{tc4ch3_r3u5e_p1u5_0v3rfl0w_equ4l5_r007}

Last updated