Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NText column parsing to MSSQL #19054

Merged
merged 1 commit into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker; I don't think this follows the format that's expected for module documentation: #19054 (comment)


```
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)
27 changes: 26 additions & 1 deletion lib/rex/proto/mssql/client_mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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", '')
Copy link
Contributor

@adfoster-r7 adfoster-r7 Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a directive for null terminated strings as an alternative to this gsub approach 👀

https://apidock.com/ruby/String/unpack
Z | String | null-terminated string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless this is needed for utf16 support?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is, but I'm not positive. Do you have any suggestions for verifying this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circling back - this had some unexpected behavior. ntext doesn't just end in null, but separates characters with it, and unpack with Z was removing characters to the right of null values


when 104
col[:id] = :bitn
col[:int_size] = data.slice!(0, 1).unpack('C')[0]
Expand Down Expand Up @@ -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]

Expand Down
32 changes: 15 additions & 17 deletions modules/auxiliary/scanner/mssql/mssql_hashdump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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

Expand All @@ -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

Expand Down
6 changes: 5 additions & 1 deletion modules/auxiliary/scanner/mssql/mssql_schemadump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions spec/acceptance/mssql_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
lines: {
all: {
required: [
'Instance Name:'
/Instance Name: "\w+"/,
]
},
}
Expand Down Expand Up @@ -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:'
]
},
}
Expand All @@ -78,9 +82,7 @@
lines: {
all: {
required: [
# Default module query
"Response",
# Result
"Microsoft SQL Server",
]
},
Expand Down
9 changes: 9 additions & 0 deletions test/modules/post/test/mssql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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);"})}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll probably want this to use the convention of the other PR, but can fix separately

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") }
Expand Down
Loading