diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 569baef81b0ab..62f8cca893032 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -12,6 +12,7 @@ module Exploit::Remote::LDAP include Msf::Exploit::Remote::Kerberos::Ticket::Storage include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options include Metasploit::Framework::LDAP::Client + include Msf::OptionalSession::LDAP # Initialize the LDAP client and set up the LDAP specific datastore # options to allow the client to perform authentication and timeout @@ -27,8 +28,6 @@ def initialize(info = {}) super register_options([ - Opt::RHOST, - Opt::RPORT(389), OptBool.new('SSL', [false, 'Enable SSL on the LDAP connection', false]), Msf::OptString.new('DOMAIN', [false, 'The domain to authenticate to']), Msf::OptString.new('USERNAME', [false, 'The username to authenticate with'], aliases: ['BIND_DN']), @@ -96,6 +95,7 @@ def get_connect_opts # @return [Object] The result of whatever the block that was # passed in via the "block" parameter yielded. def ldap_connect(opts = {}, &block) + return yield session.client if session ldap_open(get_connect_opts.merge(opts), &block) end @@ -111,6 +111,7 @@ def ldap_connect(opts = {}, &block) # @return [Object] The result of whatever the block that was # passed in via the "block" parameter yielded. def ldap_open(connect_opts, &block) + return yield session.client if session opts = resolve_connect_opts(connect_opts) Rex::Proto::LDAP::Client.open(opts, &block) end @@ -135,6 +136,7 @@ def resolve_connect_opts(connect_opts) # @yieldparam ldap [Rex::Proto::LDAP::Client] The LDAP connection handle to use for connecting to # the target LDAP server. def ldap_new(opts = {}) + return yield session.client if session ldap = Rex::Proto::LDAP::Client.new(resolve_connect_opts(get_connect_opts.merge(opts))) @@ -169,58 +171,6 @@ def ldap.use_connection(args) yield ldap end - # # Get the naming contexts for the target LDAP server. - # # - # # @param ldap [Rex::Proto::LDAP::Client] The Rex::Proto::LDAP::Client connection handle for the - # # current LDAP connection. - # # @return [Net::BER::BerIdentifiedArray] Array of naming contexts for the target LDAP server. - # def get_naming_contexts(ldap) - # vprint_status("#{peer} Getting root DSE") - # - # unless (root_dse = ldap.search_root_dse) - # print_error("#{peer} Could not retrieve root DSE") - # return - # end - # - # naming_contexts = root_dse[:namingcontexts] - # - # # NOTE: Rex::Proto::LDAP::Client converts attribute names to lowercase - # if naming_contexts.empty? - # print_error("#{peer} Empty namingContexts attribute") - # return - # end - # - # naming_contexts - # end - - # Discover the base DN of the target LDAP server via the LDAP - # server's naming contexts. - # - # @param ldap [Rex::Proto::LDAP::Client] The Rex::Proto::LDAP::Client connection handle for the - # current LDAP connection. - # @return [String] A string containing the base DN of the target LDAP server. - # def discover_base_dn(ldap) - # # @type [Net::BER::BerIdentifiedArray] - # naming_contexts = get_naming_contexts(ldap) - # - # unless naming_contexts - # print_error("#{peer} Base DN cannot be determined") - # return - # end - # - # # NOTE: Find the first entry that starts with `DC=` as this will likely be the base DN. - # naming_contexts.select! { |context| context =~ /^([Dd][Cc]=[A-Za-z0-9-]+,?)+$/ } - # naming_contexts.reject! { |context| context =~ /(Configuration)|(Schema)|(ForestDnsZones)/ } - # if naming_contexts.blank? - # print_error("#{peer} A base DN matching the expected format could not be found!") - # return - # end - # base_dn = naming_contexts[0] - # - # print_good("#{peer} Discovered base DN: #{base_dn}") - # base_dn - # end - # Check whether it was possible to successfully bind to the target LDAP # server. Raise a RuntimeException with an appropriate error message # if not. diff --git a/lib/msf/core/exploit/remote/ldap/error.rb b/lib/msf/core/exploit/remote/ldap/error.rb index 261631c3d1f81..c1369d03c984b 100644 --- a/lib/msf/core/exploit/remote/ldap/error.rb +++ b/lib/msf/core/exploit/remote/ldap/error.rb @@ -1,20 +1,15 @@ # frozen_string_literal: true -module Msf - module Exploit - module Remote - module LDAP - class Error < ::StandardError +module Msf::Exploit::Remote::LDAP - attr_reader :error_code - attr_reader :operation_result - def initialize(message: nil, error_code: nil, operation_result: nil) - super(message || 'LDAP Error') - @error_code = error_code - @operation_result = operation_result - end - end - end + class Error < ::StandardError + + attr_reader :error_code + attr_reader :operation_result + def initialize(message: nil, error_code: nil, operation_result: nil) + super(message || 'LDAP Error') + @error_code = error_code + @operation_result = operation_result end end end diff --git a/lib/msf/core/optional_session/ldap.rb b/lib/msf/core/optional_session/ldap.rb new file mode 100644 index 0000000000000..58b47b503e8bb --- /dev/null +++ b/lib/msf/core/optional_session/ldap.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Msf + module OptionalSession + module LDAP + include Msf::OptionalSession + + RHOST_GROUP_OPTIONS = %w[RHOSTS RPORT DOMAIN USERNAME PASSWORD THREADS] + REQUIRED_OPTIONS = %w[RHOSTS RPORT USERNAME PASSWORD THREADS] + + def initialize(info = {}) + super( + update_info( + info, + 'SessionTypes' => %w[ldap] + ) + ) + + if optional_session_enabled? + register_option_group(name: 'SESSION', + description: 'Used when connecting via an existing SESSION', + option_names: ['SESSION']) + register_option_group(name: 'RHOST', + description: 'Used when making a new connection via RHOSTS', + option_names: RHOST_GROUP_OPTIONS, + required_options: REQUIRED_OPTIONS) + + register_options( + [ + Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]), + Msf::Opt::RHOST(nil, false), + Msf::Opt::RPORT(389, false) + ] + ) + + add_info('New in Metasploit 6.4 - This module can target a %grnSESSION%clr or an %grnRHOST%clr') + else + register_options( + [ + Msf::Opt::RHOST, + Msf::Opt::RPORT(389), + ] + ) + end + end + + def optional_session_enabled? + framework.features.enabled?(Msf::FeatureManager::LDAP_SESSION_TYPE) + end + end + end +end diff --git a/lib/rex/proto/ldap/client.rb b/lib/rex/proto/ldap/client.rb index a084f640193af..ae49b82d9cef0 100644 --- a/lib/rex/proto/ldap/client.rb +++ b/lib/rex/proto/ldap/client.rb @@ -71,10 +71,10 @@ def _open def discover_schema_naming_context result = search(base: '', attributes: [:schemanamingcontext], scope: Net::LDAP::SearchScope_BaseObject) - if result.first && result.first[:schemanamingcontext] + if result.first && !result.first[:schemanamingcontext].empty? schema_dn = result.first[:schemanamingcontext].first ilog("#{peerinfo} Discovered Schema DN: #{schema_dn}") - schema_dn + return schema_dn end wlog("#{peerinfo} Could not discover Schema DN") nil diff --git a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb index 83075d5e9979f..f76d5bac66ee7 100644 --- a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb +++ b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb @@ -109,7 +109,7 @@ def run else print_status('Discovering base DN automatically') - unless (@base_dn = discover_base_dn(ldap)) + unless (@base_dn = ldap.base_dn) fail_with(Failure::NotFound, "Couldn't discover base DN!") end end diff --git a/modules/auxiliary/admin/ldap/rbcd.rb b/modules/auxiliary/admin/ldap/rbcd.rb index 9d8df0f7ac7f3..3abfb6375d640 100644 --- a/modules/auxiliary/admin/ldap/rbcd.rb +++ b/modules/auxiliary/admin/ldap/rbcd.rb @@ -138,7 +138,7 @@ def run else print_status('Discovering base DN automatically') - unless (@base_dn = discover_base_dn(ldap)) + unless (@base_dn = ldap.base_dn) print_warning("Couldn't discover base DN!") end end diff --git a/modules/auxiliary/admin/ldap/shadow_credentials.rb b/modules/auxiliary/admin/ldap/shadow_credentials.rb index 97c262ea872b0..ab4060dff428a 100644 --- a/modules/auxiliary/admin/ldap/shadow_credentials.rb +++ b/modules/auxiliary/admin/ldap/shadow_credentials.rb @@ -5,8 +5,8 @@ class MetasploitModule < Msf::Auxiliary - include Msf::Exploit::Remote::LDAP include Msf::Auxiliary::Report + include Msf::Exploit::Remote::LDAP ATTRIBUTE = 'msDS-KeyCredentialLink'.freeze @@ -114,7 +114,9 @@ def run else print_status('Discovering base DN automatically') - unless (@base_dn = ldap.base_dn) + if (@base_dn = ldap.base_dn) + print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}") + else print_warning("Couldn't discover base DN!") end end diff --git a/modules/auxiliary/gather/asrep.rb b/modules/auxiliary/gather/asrep.rb index 38a0946db168e..150a3012ecb24 100644 --- a/modules/auxiliary/gather/asrep.rb +++ b/modules/auxiliary/gather/asrep.rb @@ -99,7 +99,7 @@ def run_ldap ldap_connect do |ldap| validate_bind_success!(ldap) - unless (base_dn = discover_base_dn(ldap)) + unless (base_dn = ldap.base_dn) fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!") end diff --git a/modules/auxiliary/gather/ldap_hashdump.rb b/modules/auxiliary/gather/ldap_hashdump.rb index 05e49ab6965c1..9d7ebea25b139 100644 --- a/modules/auxiliary/gather/ldap_hashdump.rb +++ b/modules/auxiliary/gather/ldap_hashdump.rb @@ -5,9 +5,9 @@ class MetasploitModule < Msf::Auxiliary - include Msf::Exploit::Remote::LDAP include Msf::Auxiliary::Scanner include Msf::Auxiliary::Report + include Msf::Exploit::Remote::LDAP def initialize(info = {}) super( @@ -33,7 +33,8 @@ def initialize(info = {}) ], 'DefaultAction' => 'Dump', 'DefaultOptions' => { - 'SSL' => true + 'SSL' => true, + 'RPORT' => 636 }, 'Notes' => { 'Stability' => [CRASH_SAFE], @@ -44,7 +45,6 @@ def initialize(info = {}) ) register_options([ - Opt::RPORT(636), # SSL/TLS OptInt.new('MAX_LOOT', [false, 'Maximum number of LDAP entries to loot', nil]), OptInt.new('READ_TIMEOUT', [false, 'LDAP read timeout in seconds', 600]), OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']), diff --git a/modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb b/modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb index a6600b7fd90da..efc3316be489a 100644 --- a/modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb +++ b/modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb @@ -77,7 +77,7 @@ def run else print_status('Discovering base DN automatically') - unless (@base_dn = discover_base_dn(ldap)) + unless (@base_dn = ldap.base_dn) print_warning('Falling back on default base DN dc=vsphere,dc=local') end end diff --git a/spec/acceptance/ldap_spec.rb b/spec/acceptance/ldap_spec.rb index 4c702020d37b5..52684c859b8e5 100644 --- a/spec/acceptance/ldap_spec.rb +++ b/spec/acceptance/ldap_spec.rb @@ -26,7 +26,7 @@ { name: 'auxiliary/gather/ldap_query', platforms: %i[linux osx windows], - targets: [:rhost], + targets: [:session, :rhost], skipped: false, action: 'run_query_file', datastore: { QUERY_FILE_PATH: 'data/auxiliary/gather/ldap_query/ldap_queries_default.yaml' }, @@ -39,7 +39,6 @@ /Running ENUM_ACCOUNTS.../, /Running ENUM_USER_SPNS_KERBEROAST.../, /Running ENUM_USER_PASSWORD_NOT_REQUIRED.../, - ] } } @@ -47,7 +46,7 @@ { name: 'auxiliary/gather/ldap_query', platforms: %i[linux osx windows], - targets: [:rhost], + targets: [:session, :rhost], skipped: false, action: 'enum_accounts', lines: { @@ -62,13 +61,11 @@ { name: 'auxiliary/gather/ldap_hashdump', platforms: %i[linux osx windows], - targets: [:rhost], + targets: [:session, :rhost], skipped: false, lines: { all: { required: [ - /Discovering base DN\(s\) automatically/, - /Dumping data for root DSE/, /Searching base DN='DC=ldap,DC=example,DC=com'/, /Storing LDAP data for base DN='DC=ldap,DC=example,DC=com' in loot/, /266 entries, 0 creds found in 'DC=ldap,DC=example,DC=com'./ @@ -79,13 +76,12 @@ { name: 'auxiliary/admin/ldap/shadow_credentials', platforms: %i[linux osx windows], - targets: [:rhost], + targets: [:session, :rhost], skipped: false, datastore: { TARGET_USER: 'administrator' }, lines: { all: { required: [ - /Discovering base DN automatically/, /Discovered base DN: DC=ldap,DC=example,DC=com/, /The msDS-KeyCredentialLink field is empty./ ] @@ -338,7 +334,9 @@ def with_test_harness(module_test) end) use_module = "use #{module_test[:name]}" - run_module = "run session=#{session_id} Verbose=true" + run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run' + run_module = "#{run_command} session=#{session_id} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true" + replication_commands << use_module console.sendline(use_module) diff --git a/spec/lib/msf/core/exploit/remote/ldap_spec.rb b/spec/lib/msf/core/exploit/remote/ldap_spec.rb index 8c646637017e0..ebf6abb987751 100644 --- a/spec/lib/msf/core/exploit/remote/ldap_spec.rb +++ b/spec/lib/msf/core/exploit/remote/ldap_spec.rb @@ -3,15 +3,19 @@ require 'spec_helper' RSpec.describe Msf::Exploit::Remote::LDAP do + include_context 'Msf::Simple::Framework' subject do mod = ::Msf::Exploit.new mod.extend described_class - mod.send(:initialize) mod end + before(:each) do + allow(subject).to receive(:framework).and_return(framework) + end + let(:rhost) do 'rhost.example.com' end @@ -114,90 +118,90 @@ end end - describe '#get_naming_contexts' do - let(:ldap) do - instance_double(Net::LDAP) - end - context 'Could not retrieve root DSE' do - it do - expect(ldap).to receive(:search_root_dse).and_return(false) - expect(subject.get_naming_contexts(ldap)).to be(nil) - end - end - - context 'Empty naming contexts' do - let(:root_dse) do - { namingcontexts: [] } - end - it do - expect(ldap).to receive(:search_root_dse).and_return(root_dse) - expect(subject.get_naming_contexts(ldap)).to be(nil) - end - end - - context 'Naming contexts are present' do - - let(:naming_contexts) { - %w[context1 context2] - } - - let(:root_dse) do - { namingcontexts: naming_contexts } - end - - it do - expect(ldap).to receive(:search_root_dse).and_return(root_dse) - expect(subject.get_naming_contexts(ldap)).to be(naming_contexts) - end - end - end - - describe '#discover_base_dn' do - let(:ldap) do - instance_double(Net::LDAP) - end - - context 'No naming contexts' do - it do - expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(nil) - expect(subject.discover_base_dn(ldap)).to be(nil) - end - end - - context 'Invalid naming contexts' do - let(:invalid_naming_contexts) do - %w[invalid1 invalid2] - end - it do - expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(invalid_naming_contexts) - expect(subject.discover_base_dn(ldap)).to be(nil) - end - end - - context 'Valid naming contexts' do - let(:base_dn) do - 'DC=abcdef' - end - let(:valid_naming_contexts) do - [base_dn] - end - it do - expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts) - expect(subject.discover_base_dn(ldap)).to be(base_dn) - end - end - - context 'Valid naming contexts (lowercase dc)' do - let(:base_dn) do - 'dc=abcdef' - end - let(:valid_naming_contexts) do - [base_dn] - end - it do - expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts) - expect(subject.discover_base_dn(ldap)).to be(base_dn) - end - end - end + # describe '#get_naming_contexts' do + # let(:ldap) do + # instance_double(Net::LDAP) + # end + # context 'Could not retrieve root DSE' do + # it do + # expect(ldap).to receive(:search_root_dse).and_return(false) + # expect(subject.get_naming_contexts(ldap)).to be(nil) + # end + # end + + # context 'Empty naming contexts' do + # let(:root_dse) do + # { namingcontexts: [] } + # end + # it do + # expect(ldap).to receive(:search_root_dse).and_return(root_dse) + # expect(subject.get_naming_contexts(ldap)).to be(nil) + # end + # end + + # context 'Naming contexts are present' do + # + # let(:naming_contexts) { + # %w[context1 context2] + # } + # + # let(:root_dse) do + # { namingcontexts: naming_contexts } + # end + # + # it do + # expect(ldap).to receive(:search_root_dse).and_return(root_dse) + # expect(subject.get_naming_contexts(ldap)).to be(naming_contexts) + # end + # end + # end + + # describe '#discover_base_dn' do + # let(:ldap) do + # instance_double(Net::LDAP) + # end + # + # context 'No naming contexts' do + # it do + # expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(nil) + # expect(subject.discover_base_dn(ldap)).to be(nil) + # end + # end + # + # context 'Invalid naming contexts' do + # let(:invalid_naming_contexts) do + # %w[invalid1 invalid2] + # end + # it do + # expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(invalid_naming_contexts) + # expect(subject.discover_base_dn(ldap)).to be(nil) + # end + # end + # + # context 'Valid naming contexts' do + # let(:base_dn) do + # 'DC=abcdef' + # end + # let(:valid_naming_contexts) do + # [base_dn] + # end + # it do + # expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts) + # expect(subject.discover_base_dn(ldap)).to be(base_dn) + # end + # end + # + # context 'Valid naming contexts (lowercase dc)' do + # let(:base_dn) do + # 'dc=abcdef' + # end + # let(:valid_naming_contexts) do + # [base_dn] + # end + # it do + # expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts) + # expect(subject.discover_base_dn(ldap)).to be(base_dn) + # end + # end + # end end diff --git a/spec/lib/rex/proto/ldap/client_spec.rb b/spec/lib/rex/proto/ldap/client_spec.rb index a37c547f774bc..0342e762642a4 100644 --- a/spec/lib/rex/proto/ldap/client_spec.rb +++ b/spec/lib/rex/proto/ldap/client_spec.rb @@ -14,4 +14,145 @@ end it_behaves_like 'session compatible client' + + let(:base_dn) { 'DC=ldap,DC=example,DC=com' } + let(:schema_dn) { 'CN=Schema,CN=Configuration,DC=ldap,DC=example,DC=com' } + + let(:root_dse_result_ldif) do + "dn: \n" \ + "namingcontexts: #{base_dn}\n" \ + "namingcontexts: CN=Configuration,DC=ldap,DC=example,DC=com\n" \ + "namingcontexts: CN=Schema,CN=Configuration,DC=ldap,DC=example,DC=com\n" \ + "namingcontexts: DC=DomainDnsZones,DC=ldap,DC=example,DC=com\n" \ + "namingcontexts: DC=ForestDnsZones,DC=ldap,DC=example,DC=com\n" \ + "supportedldapversion: 2\n" \ + "supportedldapversion: 3\n" \ + "supportedsaslmechanisms: GSS-SPNEGO\n" \ + "supportedsaslmechanisms: GSSAPI\n" \ + "supportedsaslmechanisms: NTLM\n" + end + + let(:schema_naming_context) do + "dn: \n" \ + "schemanamingcontext: #{schema_dn}\n" + end + + let(:empty_response) do + "dn: \n" + end + + let(:schema_naming_context_result) do + root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(schema_naming_context)) + root_dse_dataset.to_entries + end + + let(:root_dse_result) do + root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(root_dse_result_ldif)) + root_dse_dataset.to_entries[0] + end + + let(:empty_response_result) do + root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(empty_response)) + root_dse_dataset.to_entries + end + + describe '#naming_contexts' do + + before(:each) do + allow(subject).to receive(:search_root_dse).and_return(root_dse_result) + end + + it 'should cache the result' do + expect(subject).to receive(:search_root_dse) + subject.naming_contexts + expect(subject).not_to receive(:search_root_dse) + subject.naming_contexts + end + + context 'when no naming contexts are available' do + let(:root_dse_result_ldif) do + "dn: \n" \ + "supportedldapversion: 2\n" \ + "supportedldapversion: 3\n" \ + "supportedsaslmechanisms: GSS-SPNEGO\n" \ + "supportedsaslmechanisms: GSSAPI\n" \ + "supportedsaslmechanisms: NTLM\n" + end + + it 'returns an empty array' do + expect(subject.naming_contexts).to be_empty + end + end + + context 'when naming contexts are available' do + it 'contains naming contexts' do + expect(subject.naming_contexts).not_to be_empty + end + end + end + + describe '#base_dn' do + + before(:each) do + allow(subject).to receive(:search_root_dse).and_return(root_dse_result) + end + + it 'should cache the result' do + expect(subject).to receive(:discover_base_dn).and_call_original + subject.base_dn + expect(subject).not_to receive(:discover_base_dn) + subject.base_dn + end + + context 'when no naming contexts are available' do + let(:root_dse_result_ldif) do + "dn: \n" \ + "supportedldapversion: 2\n" \ + "supportedldapversion: 3\n" \ + "supportedsaslmechanisms: GSS-SPNEGO\n" \ + "supportedsaslmechanisms: GSSAPI\n" \ + "supportedsaslmechanisms: NTLM\n" + end + + it 'should not find the base dn' do + expect(subject.base_dn).to be_nil + end + end + + context 'when naming contexts are available' do + it 'contains naming contexts' do + expect(subject.base_dn).to eql(base_dn) + end + end + end + + describe '#schema_dn' do + + before(:each) do + allow(subject).to receive(:search).and_return(schema_naming_context_result) + end + + it 'should cache the result' do + expect(subject).to receive(:discover_schema_naming_context).and_call_original + subject.schema_dn + expect(subject).not_to receive(:discover_schema_naming_context) + subject.schema_dn + end + + context 'when the response does not contain the schema_dn' do + before(:each) do + allow(subject).to receive(:search).and_return(empty_response_result) + end + + it 'does not find the schema_dn' do + expect(subject.schema_dn).to be_nil + end + end + + context 'when the response does contain the schema_dn' do + it 'finds the schema_dn' do + expect(subject.schema_dn).to eql(schema_dn) + end + end + end end diff --git a/test/ldap/docker-compose.yml b/test/ldap/docker-compose.yml index 1544a1670e256..6c4239e2751b6 100644 --- a/test/ldap/docker-compose.yml +++ b/test/ldap/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: ldap: tty: true diff --git a/test/smb/docker-compose.yml b/test/smb/docker-compose.yml index 10980403919bd..69afa08940385 100644 --- a/test/smb/docker-compose.yml +++ b/test/smb/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: samba: tty: true