rapid7 / ruby_smb

A native Ruby implementation of the SMB Protocol Family
Other
79 stars 82 forks source link

Add more Fscc information classes to the server #217

Closed zeroSteiner closed 2 years ago

zeroSteiner commented 2 years ago

This adds server support for two new Fscc information classes FILE_ID_FULL_DIRECTORY_INFORMATION and FILE_ALL_INFORMATION. FILE_ALL_INFORMATION is actually a compound field that is composed of a whole bunch of others that have also all been added. FILE_ALL_INFORMATION support fixes trying to read files using smbclient and FILE_ID_FULL_DIRECTORY_INFORMATION fixes trying to list directories on the server using RubySMB's own example/list_directory.rb script (thus closing #208).

Testing

zeroSteiner commented 2 years ago

Marking this as draft. I'm looking into a vulnerability right now where I really need the FileNormalizedNameInformation class to be defined which makes sense to add here. I'm also going to see about adding a flexible query example which will help me with my research as well as can be used to test these examples with the server. I'm hoping to have it all done and undrafted by the end of today.

zeroSteiner commented 2 years ago

You can use this example script to query the different information classes. I'm not including it because it's very much a partial implementation. It does not support SMB1 at all or querying directories. If we were to include this we'd probably want a generic #query method of some kind on the trees for SMB 1 and 2 and it should support both files and directories which isn't easy right now since the caller has to know which it is that they're querying.

query_file.rb Example usage: ```  : ruby_smb:feat/fscc/file-all-information13:32:57 ruby_smb ruby examples/query_file.rb --info-class FileNormalizedNameInformation 192.168.159.128 Share Dir\\test.txt SMB3 : (0x00000000) STATUS_SUCCESS: The operation completed successfully. Connected to \\192.168.159.128\Share successfully! {:file_name_length=>24, :file_name=>"Dir\\test.txt"}  : ruby_smb:feat/fscc/file-all-information13:32:57 ruby_smb ruby examples/query_file.rb --info-class FileNetworkOpenInformation 192.168.159.128 Share Dir\\test.txt SMB3 : (0x00000000) STATUS_SUCCESS: The operation completed successfully. Connected to \\192.168.159.128\Share successfully! {:create_time=>132957267670000000, :last_access=>132957267670000000, :last_write=>132957267670000000, :last_change=>132957267670000000, :allocation_size=>4096, :end_of_file=>13, :file_attributes=>{:normal=>1, :device=>0, :archive=>0, :directory=>0, :volume=>0, :system=>0, :hidden=>0, :read_only=>0, :reserved=>0, :encrypted=>0, :content_indexed=>0, :offline=>0, :compressed=>0, :reparse_point=>0, :sparse=>0, :temp=>0, :reserved2=>0, :reserved3=>0}, :reserved=>0}  : ruby_smb:feat/fscc/file-all-information13:33:07 ruby_smb ruby examples/query_file.rb --info-class FileAllInformation 192.168.159.128 Share Dir\\test.txt SMB3 : (0x00000000) STATUS_SUCCESS: The operation completed successfully. Connected to \\192.168.159.128\Share successfully! {:basic_information=>{:create_time=>132957267670000000, :last_access=>132957267670000000, :last_write=>132957267670000000, :last_change=>132957267670000000, :file_attributes=>{:normal=>1, :device=>0, :archive=>0, :directory=>0, :volume=>0, :system=>0, :hidden=>0, :read_only=>0, :reserved=>0, :encrypted=>0, :content_indexed=>0, :offline=>0, :compressed=>0, :reparse_point=>0, :sparse=>0, :temp=>0, :reserved2=>0, :reserved3=>0}, :reserved=>"\x00\x00\x00\x00"}, :standard_information=>{:allocation_size=>4096, :end_of_file=>13, :number_of_links=>0, :delete_pending=>0, :directory=>0, :reserved=>"\x00\x00"}, :internal_information=>{:file_id=>4076690445}, :ea_information=>{:ea_size=>0}, :access_information=>{:access_flags=>129}, :position_information=>{:current_byte_offset=>13}, :mode_information=>{:flags=>{:reserved=>0, :file_synchronous_io_nonalert=>0, :file_synchronous_io_alert=>0, :file_no_intermediate_buffering=>0, :file_sequential_only=>0, :file_write_through=>0, :reserved2=>0, :reserved3=>0, :file_delete_on_close=>0, :reserved4=>0, :reserved5=>0}}, :alignment_information=>{:alignment_requirement=>0}, :name_information=>{:file_name_length=>16, :file_name=>"test.txt"}}  : ruby_smb:feat/fscc/file-all-information13:33:23 ruby_smb  ``` ```ruby #!/usr/bin/ruby require 'bundler/setup' require 'optparse' require 'ruby_smb' info_classes = RubySMB::Fscc::FileInformation.constants.select { |name| name =~ /File\w+Information/ }.map(&:to_s) def query_file(tree, filename: nil, type: RubySMB::Fscc::FileInformation::FileNormalizedNameInformation) file_id = tree.open_file(filename: filename).guid query_request = RubySMB::SMB2::Packet::QueryInfoRequest.new query_request.info_type = RubySMB::SMB2::SMB2_INFO_FILE query_request.file_information_class = type::CLASS_LEVEL query_request.file_id = file_id query_request.output_buffer_length = 0x400 query_request = tree.set_header_fields(query_request) response = tree.client.send_recv(query_request, encrypt: tree.tree_connect_encrypt_data) query_response = RubySMB::SMB2::Packet::QueryInfoResponse.read(response) unless query_response.status_code == WindowsError::NTStatus::STATUS_SUCCESS raise RubySMB::Error::UnexpectedStatusCode.new(query_response.status_code) end type.read(query_response.buffer) end args = ARGV.dup options = { domain: '.', username: '', password: '', share: nil, smbv1: true, smbv2: true, smbv3: true, target: nil, info_class: 'FileNetworkOpenInformation' } options[:file] = args.pop options[:share] = args.pop options[:target ] = args.pop optparser = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename(__FILE__)} [options] target share file" opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1| options[:smbv1] = smbv1 end opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2| options[:smbv2] = smbv2 end opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3| options[:smbv3] = smbv3 end opts.on("--username USERNAME", "The account's username (default: #{options[:username]})") do |username| if username.include?('\\') options[:domain], options[:username] = username.split('\\', 2) else options[:username] = username end end opts.on("--password PASSWORD", "The account's password (default: #{options[:password]})") do |password| options[:password] = password end opts.on("--info-class INFO_CLASS", "The information class to query (default: #{options[:info_class]})") do |info_class| options[:info_class] = info_class end end optparser.parse!(args) if options[:target].nil? || options[:share].nil? || options[:file].nil? abort(optparser.help) end unless info_classes.include?(options[:info_class]) puts "Invalid information class: #{options[:info_class]}" puts "Must be one of:" info_classes.each do |info_class| puts " - #{info_class}" end exit(1) end info_class = RubySMB::Fscc::FileInformation.const_get(options[:info_class]) path = "\\\\#{options[:target]}\\#{options[:share]}" sock = TCPSocket.new options[:target], 445 dispatcher = RubySMB::Dispatcher::Socket.new(sock) client = RubySMB::Client.new(dispatcher, smb1: options[:smbv1], smb2: options[:smbv2], smb3: options[:smbv3], username: options[:username], password: options[:password], domain: options[:domain], always_encrypt: false) protocol = client.negotiate status = client.authenticate puts "#{protocol} : #{status}" unless status == WindowsError::NTStatus::STATUS_SUCCESS puts 'Authentication failed!' exit(1) end begin tree = client.tree_connect(path) puts "Connected to #{path} successfully!" rescue StandardError => e puts "Failed to connect to #{path}: #{e.message}" exit(1) end response = query_file(tree, filename: options[:file], type: info_class) puts response.inspect ```
cdelafuente-r7 commented 2 years ago

Thanks @zeroSteiner for updating this. Everything looks good now. I tested using the query_file.rb script, querying every information class structures, and verified it worked as expected. I'll go ahead and land it.

Example output

Server:

❯ ruby examples/file_server.rb --path /tmp/test/ --username msfuser --password 123456 --share public

D, [2022-05-04T14:42:11.039055 #73271] DEBUG -- : Adding disk share: public
server is running
received connection
I, [2022-05-04T14:46:10.337675 #73271]  INFO -- : Negotiated dialect: SMB v3.1.1
D, [2022-05-04T14:46:10.347969 #73271] DEBUG -- : Dispatching request to do_session_setup_smb2 (session: nil)
D, [2022-05-04T14:46:10.356954 #73271] DEBUG -- : Dispatching request to do_session_setup_smb2 (session: #<Session id: 531316813, user_id: nil, state: :in_progress>)
D, [2022-05-04T14:46:10.357534 #73271] DEBUG -- : NTLM authentication request received for .\msfuser
I, [2022-05-04T14:46:10.357682 #73271]  INFO -- : NTLM authentication request succeeded for .\msfuser
D, [2022-05-04T14:46:10.370424 #73271] DEBUG -- : Dispatching request to do_tree_connect_smb2 (session: #<Session id: 531316813, user_id: "WORKGROUP\\msfuser", state: :valid>)
D, [2022-05-04T14:46:10.370624 #73271] DEBUG -- : Received TREE_CONNECT request for share: public
D, [2022-05-04T14:46:10.385133 #73271] DEBUG -- : Dispatching request to do_create_smb2 (session: #<Session id: 531316813, user_id: "WORKGROUP\\msfuser", state: :valid>)
D, [2022-05-04T14:46:10.385195 #73271] DEBUG -- : Received CREATE request for share: public
D, [2022-05-04T14:46:10.399636 #73271] DEBUG -- : Dispatching request to do_query_info_smb2 (session: #<Session id: 531316813, user_id: "WORKGROUP\\msfuser", state: :valid>)
D, [2022-05-04T14:46:10.399706 #73271] DEBUG -- : Received QUERY_INFO request for share: public

Client:

❯ ruby examples/query_file.rb --username msfuser --password 123456 --info-class FileAllInformation 127.0.0.1 public payload.dll
SMB3 : (0x00000000) STATUS_SUCCESS: The operation completed successfully.
Connected to \\127.0.0.1\public successfully!
{:basic_information=>{:create_time=>132930391790000000, :last_access=>132938986530000000, :last_write=>132938986440000000, :last_change=>132938986440000000, :file_attributes=>{:normal=>1, :device=>0, :archive=>0, :directory=>0, :volume=>0, :system=>0, :hidden=>0, :read_only=>0, :reserved=>0, :encrypted=>0, :content_indexed=>0, :offline=>0, :compressed=>0, :reparse_point=>0, :sparse=>0, :temp=>0, :reserved2=>0, :reserved3=>0}, :reserved=>"\x00\x00\x00\x00"}, :standard_information=>{:allocation_size=>12288, :end_of_file=>8704, :number_of_links=>0, :delete_pending=>0, :directory=>0, :reserved=>"\x00\x00"}, :internal_information=>{:file_id=>2960559540}, :ea_information=>{:ea_size=>0}, :access_information=>{:access_flags=>129}, :position_information=>{:current_byte_offset=>8704}, :mode_information=>{:flags=>{:reserved=>0, :file_synchronous_io_nonalert=>0, :file_synchronous_io_alert=>0, :file_no_intermediate_buffering=>0, :file_sequential_only=>0, :file_write_through=>0, :reserved2=>0, :reserved3=>0, :file_delete_on_close=>0, :reserved4=>0, :reserved5=>0}}, :alignment_information=>{:alignment_requirement=>0}, :name_information=>{:file_name_length=>78, :file_name=>"payload.dll"}}