Rating: 5.0

# Get Admin - 220


This is a very unexpected gig for me. However, I'm busy with other projects so can you please give me a hand to test this. For free, of course. :-)

Files: [https://dctf.def.camp/dctf-18-quals-81249812/get-admin.zip](https://dctf.def.camp/dctf-18-quals-81249812/get-admin.zip)

Target: [https://admin.dctfq18.def.camp/](https://admin.dctfq18.def.camp/)

## Solution

### Overview
The website is pretty basic, it lets you register an account with your name, password, email and lets you login. If your `id` is `1` (i.e. if you are `admin`), it prints the flag else says `Try Harder` (for all other users). There's an option to update your profile if needed and an option to logout.

When we login to the website, it sets a cookie in our browser which is `AES-128-CBC` encrypted and contains our `id` (automatically set at the time of registration), `username`and `email` along with the `CRC-32 checksum`. Along with this encrypted data, it contains a `length` in the end which gives the length of the decrypted plaintext cookie. Odd. This was an unnecessary piece of information for this encryption/decryption scheme. We'll see later how this was used to exploit the decryption.

### Encryption of the Cookie
Let's get to specifics. As soon as we login, the following cookie is set in `index.php`:
```php
setcookie('user',encryptCookie([
'id' => $userid,
'username' => $_POST['username'],
'email' => $row['email'],
]), time()+60*60*24*30);
```

Here's `encryptCookie()` function from `config.php` along with its helper functions:
```php
function encryptCookie($arr) {
$cookie = compress($arr);
$arr['checksum'] = crc32($cookie);
return encrypt(compress($arr), AES_KEY, AES_IV);
}

function compress($arr) {
return implode('÷', array_map(function ($v, $k) { return $k.'¡'.$v; }, $arr, array_keys($arr) ));
}

function encrypt($plaintext, $key, $iv) {
$length = strlen($plaintext);
$ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($ciphertext) . sprintf('%06d', $length);
}
```

`compress()` simply serializes the array into a string where key-value pairs are separated by a `÷` and inserts `¡` between each key and value. For example if `id = 1337, username = testac, email = [email protected]` then `compress()` returns `id¡1337÷username¡testac÷email¡[email protected]`.

`encryptCookie()` takes `id`, `username` and `email` as inputs, calculates the `CRC-32` checksum of the serialized cookie, appends it to the cookie again. Now we get: `id¡1337÷username¡testac÷email¡[email protected]÷checksum¡2160329226`

This string is then encrypted with `AES-128-CBC` and the length of the above string `70` (`¡` and `÷` are counted as length `2` each) padded with `0s` is appended to the resulting `base64` string. So this is our final cookie:
`Rx5R751nNLFTDmwdj248byPKYFCReDmb8cTlK8m53X3TLG5WpUwYv+8zN0Ur2YVZ0q7giK51kNvFRjr36elyKiunyw6aPYR1BAE9dF6+7KU=000070`

### Decryption of the Cookie

In `index.php`, if the cookie is already set but `_SESSION['userid']` is not, it tries to decrypt the cookie and if the `id` in it is greater than `0`, sets the `_SESSION['userid']` variable and redirects us to `admin.php`. Here's `decryptCookie()` and its helper function from `config.php`:
```php
function decryptCookie($cypher) {
return decompress(decrypt($cypher, AES_KEY, AES_IV));
}

function decrypt($ciphertext, $key, $iv) {
$length = intval(substr($ciphertext, -6, 6));
$ciphertext = substr($ciphertext, 0,-6);
$output = openssl_decrypt(base64_decode($ciphertext), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
if($output == FALSE) {
echo('Decryption error (0).');
die();
}
return substr($output, 0, $length);
}
```

`decryptCookie()` takes the encrypted cookie, separates the `length` from the ciphertext, decrypts the cipher and returns only the first `length` characters of the decrypted cookie. Mhmm. This is then passed to `decompressed()`:

```php
function decompress($cookie) {
if(preg_match('/[^\x00-\x7F]+\ *(?:[^\x00-\x7F]| )*/im',$cookie, $m) == 0) {
echo('Decryption error (1).');
return false;
}

$t = explode("÷", $cookie);

$arr = [];
foreach($t as $el) {
$el = explode("¡", $el);
$arr[$el[0]] = $el[1];
}

if(!isset($arr['checksum'])) {
echo('Decryption error (2).');
return false;
}

$checksum = intval($arr['checksum']);
unset($arr['checksum']);
$cookie = compress($arr);
if($checksum != crc32($cookie)) {
echo('Decryption error (3).');
return false;
}

return $arr;
}
```

The `decrypt()` function:
- Checks if the decrypted cookie matches the regex
- Constructs the array from its serialized form
- Extracts the expected CRC-32 checksum
- Computes the CRC-32 checksum of the remaining data
- Checks if the two checksums match
- Returns the array containing the user data

This is then given back to `index.php` and it then redirects us to `admin.php`. There, if our `id == 1`, the flag is printed but as we are regular users, our `id > 1`.

### The Vulnerability

During registration, the website put no restrictions on the characters entered in the `email` field. Think what happens if I put my email as `[email protected]÷id¡1`.
My cookie would then be: `id¡1337÷username¡testac÷email¡[email protected]÷id¡1` along with its checksum. While decryption, due to the way `decompress()` is constructing the array, the array would be `id¡1÷username¡testac÷email¡[email protected]` as the latter `id` replaces the value of the former one. This is exactly what we want!

But unfortunately the `CRC-32` checksum fails as the expected checksum (`checksum=732808468`) would be of the original data with 2 `id`s whereas the resulting one (`checksum=3870551952`) is of the data with only 1 `id`. Let's inject the checksum too then!

Modifying our email to include the checksum of the data with `id=1`, our new cookie will be: `id¡1337÷username¡testac÷email¡[email protected]÷id¡1÷checksum¡3870551952`

This will then be appended with its checksum giving: `id¡1337÷username¡testac÷email¡[email protected]÷id¡1÷checksum¡3870551952÷checksum¡2104704402`

But now, our checksum gets overwritten by the new one just like we overwrote the previous value of `id`. This is where the `length` comes into picture! If we reduce the `length` only upto the first checksum (i.e. the first `77` characters), the `decompress()` function thinks that's the original checksum and decrypts the cookie without any errors!

That is all we need to do to get `ADMIN ACCESS`!

1. Register with the following details:
- username: `testac`
- email: `[email protected]÷id¡1÷checksum¡3870551952`
2. Login and get your cookie (will result in `Decryption Error (3)`)
3. Logout, change the length in the cookie to `000077` and set the cookie
4. Navigate to admin.php

And we have the flag!

![flag](./flag.png)

**`DCTF{4EF853DFC818AFEC39497CD1B91625F9E6E19D34D8E43E56722026F26A95F13E} `**

Original writeup (https://github.com/AdityaVallabh/ctf-write-ups/tree/master/DCTF/Get%20Admin).