Add LDAP search variants that carry controls (Fixes #1104) - #1105
Merged
Conversation
Query built its search request with the control list fixed at nil, and the five scope wrappers were built on that signature, so no search in the package could carry an LDAP control. Session.connection is unexported, so a caller could not issue its own search either. QueryWithControls takes the control list and holds what Query used to do; Query is now a one-line delegation with nil, so its behaviour is unchanged by construction and none of its call sites move. QueryWholeSubtreeWithControls is the subtree form, which is the shape a security descriptor sweep needs: reading nTSecurityDescriptor requires LDAP_SERVER_SD_FLAGS_OID, and GetNtSecurityDescriptorOf can only do it one base object at a time. The four *_SECURITY_INFORMATION values move out of the body of GetNtSecurityDescriptorOf to package level, so a caller building its own SD flags control does not have to redeclare them, and SECURITY_INFORMATION_DEFAULT names the everything-but-the-SACL set that an unprivileged read has to ask for. GetNtSecurityDescriptorOf now uses that constant and sends the value it sent before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked Issue
Closes #1104Root Cause
Query()was written with the control list of itsldap.NewSearchRequestcall fixedat
nil, and the five scope wrappers (QueryBaseObject,QuerySingleLevel,QueryWholeSubtree,QueryChildren,QueryAllNamingContexts) were then built on topof that signature. Nothing in the package carries a control through to the wire, and
because
Session.connectionis unexported a caller cannot go around the package andbuild its own search either. The result is that an entire class of LDAP search - any
search whose meaning is set by a control - cannot be expressed through this API.
nTSecurityDescriptoris where that bites hardest. Per MS-ADTS 3.1.1.3.4.1.11 the readis scoped by
LDAP_SERVER_SD_FLAGS_OID; without it a domain controller attempts toinclude the SACL, and a client that does not hold
SE_SECURITY_NAMEgets the attributeback absent from the entry rather than an error.
GetNtSecurityDescriptorOfalreadyknows this and sends
OWNER | GROUP | DACL, but it is aScopeBaseObjectread of asingle DN, so reading the descriptor of every object of a naming context costs one
round trip per object.
Fix Description
QueryWithControlstakes the control list and holds the bodyQueryused to have.Querybecomes a one-line delegation passingnil, so its behaviour is unchanged byconstruction rather than by inspection, and none of its call sites move - which matters,
since
Queryand its wrappers have 25 call sites inside this repository and many moreacross the sibling tool repositories.
QueryWholeSubtreeWithControlsis added besideQueryWholeSubtreebecause the subtree sweep is the shape that actually needs acontrol.
The four
*_SECURITY_INFORMATIONvalues move from inside the body ofGetNtSecurityDescriptorOfto package level, so a caller building its own SD flagscontrol does not have to redeclare them, and
SECURITY_INFORMATION_DEFAULTnames theeverything-but-the-SACL set that an unprivileged read has to ask for.
GetNtSecurityDescriptorOfnow uses that constant and sends exactly the value it sentbefore.
An additive variant was chosen over changing the signature of
Query(which wouldtouch every call site in every repository) and over a session-level
SetSearchControls([]ldap.Control). The latter would reach all five wrappers withoutnew functions, but it makes controls sticky and invisible at the call site: a control
set once for one search would silently ride along on every later search of that
session. For something that changes what the server returns, an explicit parameter is
the safer shape.
How Verified
go build ./network/ldap/,go vet ./network/ldap/andgofmt -l network/ldap/areall clean.
go test ./network/ldap/passes.Queryatnetwork/ldap/query.go:31now contains a singlereturndelegating with
nil, and the request built atnetwork/ldap/query.go:88-107is otherwise byte-for-byte the code that ran before,so a caller of
Querysends the same search it always did.SECURITY_INFORMATION_DEFAULTis asserted to be0x7byTestSecurityInformationDefaultExcludesSACL, which is the valueGetNtSecurityDescriptorOfinlined before this change.manticore-aclmonitorreads securitydescriptors with, and it builds and runs against this branch.
Test Coverage
Added:
network/ldap/security_descriptors_test.goTestSecurityInformationDefaultExcludesSACL- pinsSECURITY_INFORMATION_DEFAULTto0x7and asserts it does not carrySACL_SECURITY_INFORMATION. Letting the SACL intothat set would make a domain controller return the
nTSecurityDescriptorattributeempty, with no error, for every client without
SE_SECURITY_NAME- a silent failureworth a regression test.
TestControlMicrosoftSDFlagsGetControlType- checks the OID the control is sent under.TestControlMicrosoftSDFlagsEncode- walks the encoded control and checks the OID andthe flags integer are where a server reads them, across three cases (default flags,
with the SACL, and with criticality set, which changes the child count).
The search functions themselves need a bound connection and are not unit-testable here;
the delegation that keeps
Queryunchanged is verified statically, as described above.Scope of Change
network/ldap/query.go,network/ldap/security_descriptors.go,network/ldap/security_descriptors_test.goRisk and Rollout
Low blast radius. Existing callers reach the same code through one added call frame with
nilcontrols, and the only behavioural surface that could regress isQuery, which isnow defined in terms of the new function rather than duplicated. Safe to merge without a
staged rollout.
Notes
Related to #1103, which reports the other half of the same constraint:
SearchWithPagingis called with a hardcoded page size of 1000 that no caller can change, for the same
underlying reason. That is a separate fix and is not touched here. Note that
QueryWithControlsstill goes throughSearchWithPaging, which appends its ownpaged-results control to whatever the caller passed - the behaviour a caller wants here,
but it does mean this cannot be used to send a search with no control at all.