diff --git a/lib/msf/core/exploit/remote/http/spip.rb b/lib/msf/core/exploit/remote/http/spip.rb index 3d7f9f540098..461f44711a50 100644 --- a/lib/msf/core/exploit/remote/http/spip.rb +++ b/lib/msf/core/exploit/remote/http/spip.rb @@ -41,5 +41,71 @@ def spip_version return version ? Rex::Version.new(version) : nil end + + # Determine Spip plugin version by name + # + # @param [String] plugin_name Name of the plugin to search for + # @return [Rex::Version, nil] Version of the plugin as Rex::Version, or nil if not found + def spip_plugin_version(plugin_name) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'spip.php') + ) + + return unless res + + # Check the Composed-By header for plugin version or config.txt URL + composed_by = res.headers['Composed-By'] + return unless composed_by + + # Case 1: Look for config.txt URL in the header + if composed_by =~ %r{(https?://[^\s]+/local/config\.txt)}i + config_url = ::Regexp.last_match(1) + vprint_status("Found config.txt URL: #{config_url}") + + # Fetch and parse the config.txt file directly + config_res = send_request_cgi( + 'method' => 'GET', + 'uri' => config_url + ) + + if config_res&.code == 200 + return parse_plugin_version(config_res.body, plugin_name) + end + end + + # Case 2: Check for plugin version directly in Composed-By + composed_by.split(',').each do |entry| + if entry =~ /#{plugin_name}\((\d+(\.\d+)+)\)/ + return Rex::Version.new(::Regexp.last_match(1)) + end + end + + # Case 3: Fallback to fetching /local/config.txt directly + vprint_status('No version found in Composed-By header. Attempting to fetch /local/config.txt directly.') + config_url = normalize_uri(target_uri.path, 'local', 'config.txt') + config_res = send_request_cgi( + 'method' => 'GET', + 'uri' => config_url + ) + + return parse_plugin_version(config_res.body, plugin_name) if config_res&.code == 200 + + nil + end + + # Parse the plugin version from config.txt or composed-by + # + # @param [String] body The body content to parse + # @param [String] plugin_name Name of the plugin to find the version for + # @return [Rex::Version, nil] Version of the plugin as Rex::Version, or nil if not found + def parse_plugin_version(body, plugin_name) + body.each_line do |line| + if line =~ /#{plugin_name}\((\d+(\.\d+)+)\)/ + return Rex::Version.new(::Regexp.last_match(1)) + end + end + nil + end end end diff --git a/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb b/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb index df7f4846f600..a8a91f10e5b1 100644 --- a/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb +++ b/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb @@ -84,9 +84,18 @@ def check ] vulnerable_ranges.each do |range| - if rversion.between?(range[:start], range[:end]) + next unless rversion.between?(range[:start], range[:end]) + + print_status('SPIP version is in the vulnerable range.') + + plugin_version = spip_plugin_version('porte_plume') + + unless plugin_version + print_warning('Could not determine the version of the porte_plume plugin.') return Exploit::CheckCode::Appears("The detected SPIP version (#{rversion}) is vulnerable.") end + + return Exploit::CheckCode::Appears("The detected SPIP version (#{rversion}) and porte_plume version (#{plugin_version}) are vulnerable.") if plugin_version < Rex::Version.new('3.1.6') end return Exploit::CheckCode::Safe("The detected SPIP version (#{rversion}) is not vulnerable.")