Sad Rendel

Ссылка на таск без vpn конфига не работает

И его сорцы

Немного истории

Таск был взят с Moscow School Ctf 2021, на которой я играл в команде Shadow Kitties (8 место).

Удивлен, что его решило всего 2 команды. Решил я только этот таск. В свое оправдание скажу, что в тот день писал олимпиаду Верченко по криптографии, пол дня провел в городе и приехал домой мертвый.

Решение

Заглянем в main.py и обнаружим условие получения флага.

from cookies import encrypt_cookie, decrypt_cookie


def get_user(request):
    cookie = request.cookies.get('sess')
    if cookie is None:
        return None
    session_data = decrypt_cookie(cookie)
    return int(session_data["user_id"])

async def get_flag(request):
    user = get_user(request)

    if user is None:
        return templates.TemplateResponse('flag_page.html', {
            'request': request,
            'flag': 'Flag is not available to anonymous users'
        })
    if user == 1:
        return templates.TemplateResponse('flag_page.html', {
            'request': request,
            'flag': FLAG
        })
    else:
        return templates.TemplateResponse('flag_page.html', {
            'request': request,
            'flag': 'Only admin can read a flag'
        })

Разобравшись, что get_user берет информацию о пользователе из куки и возвращает его user_id, используя при этом функцию decrypt_cookie, идем в cookies.py к этой самой функции.

def decrypt_cookie(encrypted_cookie):
    envelope_data = json.loads(base64.b64decode(encrypted_cookie))

    iv = base64.b64decode(envelope_data['iv'])
    cbc = AES.new(KEY2, AES.MODE_CBC, iv)
    second_layer = base64.b64decode(envelope_data['data'])
    first_layer = cbc.decrypt(second_layer)

    nonce = base64.b64decode(envelope_data['nonce'])
    ctr = AES.new(KEY1, AES.MODE_CTR, nonce=nonce)
    data = ctr.decrypt(first_layer)
    return json.JSONDecoder().raw_decode(data.decode('utf8', errors='replace'))[0]

Так как нам доступен IV, мы можем провести атаку bit flip на CBC.

Суть в том, что P = Dec(C) ⊕ IV, соответственно мы можем подменить часть IV (в нашем случае 1 байт, потому что user_id занимает именно столько), чтобы получить желаемый плейнтекст (нам известен исходный P , поэтому проблем не возникнет).

Второй этап расшифрования не представляет угрозы, так как при CTR шифротекст не используется в функции Dec.

Итак, заходим в бурп, логинимся, берем куку, подменяем в ней IV и забираем флаг.

Вот код решения.

import json
from base64 import b64decode, b64encode


iv_user_id_index = json.dumps({"user_id":9}).index('9') # всегда 12
def bit_flip(cook, user_id=0):
    user_id ^= 1
    recv_json = json.loads(b64decode(cook).decode())
    iv = recv_json['iv']
    iv = list(b64decode(iv))
    iv[iv_user_id_index] ^= user_id
    recv_json['iv'] = b64encode(bytes(iv)).decode()
    return b64encode(json.dumps(recv_json).encode())

cook = input()
user_id = int(input())
print(bit_flip(cook, user_id).decode())

Опа флаг

Я до конца думал, что это unintended решение (иначе зачем класть таск на крипту вместе с тасками на веб), но содержимое флага развеяло сомнения

Эпилог

Мой первый райтап, мог написать дичь, не стесняйтесь критиковать.


Last modified on 2021-11-30