sddl/sddlString.go (279 lines of code) (raw):

// Copyright © Microsoft <wastore@microsoft.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package sddl import ( "errors" "fmt" "regexp" "sort" "strings" ) // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f4296d69-1c0f-491f-9587-a960b292d070 // https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-definition-language // Owner and group SIDs need replacement type SDDLString struct { OwnerSID, GroupSID string DACL, SACL ACLList } type ACLList struct { Flags string ACLEntries []ACLEntry } // field 5 and field 6 will contain SIDs. // field 5 is a lone SID, but field 6 will contain SIDs under SID(.*) type ACLEntry struct { Sections []string } func (s *SDDLString) PortableString() string { output := "" if s.OwnerSID != "" { tx, err := translateSID(s.OwnerSID) if err != nil { output += "O:" + s.OwnerSID } else { output += "O:" + tx } } if s.GroupSID != "" { tx, err := translateSID(s.GroupSID) if err != nil { output += "G:" + s.GroupSID } else { output += "G:" + tx } } if s.DACL.Flags != "" || len(s.DACL.ACLEntries) != 0 { output += "D:" + s.DACL.PortableString() } if s.SACL.Flags != "" || len(s.SACL.ACLEntries) != 0 { output += "S:" + s.SACL.PortableString() } return output } var LiteralSIDRegex = regexp.MustCompile(`SID\(.*?\)`) var StringRegex = regexp.MustCompile(`("")|(".*?[^\\]")`) // PortableString returns a SDDL that's been ported from non-descript, well known SID strings (such as DU, DA, etc.) // to domain-specific strings. This allows us to not mix up the admins from one domain to another. // Azure Files requires that we do this. func (a *ACLList) PortableString() string { output := a.Flags for _, v := range a.ACLEntries { output += "(" for k, s := range v.Sections { // Append a ; after the last section if k > 0 { output += ";" } if k == 5 { // This section is a lone SID, so we can make a call to windows and translate it. tx, err := translateSID(strings.TrimSpace(s)) if err != nil { output += s } else { output += tx } } else if k == 6 { // This section will potentially have SIDs unless it's not a conditional ACE. // They're identifiable as they're inside a literal SID container. ex "SID(S-1-1-0)" workingString := "" lastAddPoint := 0 if v.Sections[0] == "XA" || v.Sections[0] == "XD" || v.Sections[0] == "XU" || v.Sections[0] == "ZA" { // We shouldn't do any replacing if we're inside of a string. // In order to handle this, we'll handle it as a list of events that occur. stringEntries := StringRegex.FindAllStringIndex(s, -1) sidEntries := LiteralSIDRegex.FindAllStringIndex(s, -1) eventMap := map[int]int{} // 1 = string start, 2 = string end, 3 = SID start, 4 = SID end. eventList := make([]int, 0) inString := false SIDStart := -1 processSID := false // Register string beginnings and ends for _, v := range stringEntries { eventMap[v[0]] = 1 eventMap[v[1]] = 2 eventList = append(eventList, v...) } // Register SID beginnings and ends for _, v := range sidEntries { eventMap[v[0]] = 3 eventMap[v[1]] = 4 eventList = append(eventList, v...) } // sort the list sort.Ints(eventList) // Traverse it. // Handle any SIDs outside of strings. for _, v := range eventList { event := eventMap[v] switch event { case 1: // String start inString = true // Add everything prior to this workingString += s[lastAddPoint:v] lastAddPoint = v case 2: inString = false // Add everything prior to this workingString += s[lastAddPoint:v] lastAddPoint = v case 3: processSID = !inString SIDStart = v // If we're going to process this SID, add everything prior to this. if processSID { workingString += s[lastAddPoint:v] lastAddPoint = v } case 4: if processSID { // We have to process the sid string now. sidString := strings.TrimSuffix(strings.TrimPrefix(s[SIDStart:v], "SID("), ")") tx, err := translateSID(strings.TrimSpace(sidString)) // It seems like we should probably still add the string if we error out. // However, this just gets handled exactly like we're not processing the SID. // When the next event happens, we just add everything to the string, including the original SID. if err == nil { workingString += "SID(" + tx + ")" lastAddPoint = v } } } } } if workingString != "" { if lastAddPoint != len(s) { workingString += s[lastAddPoint:] } s = workingString } output += s } else { output += s } } output += ")" } return strings.TrimSpace(output) } func (a *ACLList) String() string { output := a.Flags for _, v := range a.ACLEntries { output += "(" for k, s := range v.Sections { if k > 0 { output += ";" } output += s } output += ")" } return strings.TrimSpace(output) } func (s *SDDLString) String() string { output := "" if s.OwnerSID != "" { output += "O:" + s.OwnerSID } if s.GroupSID != "" { output += "G:" + s.GroupSID } if s.DACL.Flags != "" || len(s.DACL.ACLEntries) != 0 { output += "D:" + s.DACL.String() } if s.SACL.Flags != "" || len(s.SACL.ACLEntries) != 0 { output += "S:" + s.SACL.String() } return output } // place an element onto the current ACL func (s *SDDLString) putACLElement(element string, aclType rune) error { var aclEntries *[]ACLEntry switch aclType { case 'D': aclEntries = &s.DACL.ACLEntries case 'S': aclEntries = &s.SACL.ACLEntries default: return fmt.Errorf("%s ACL type invalid", string(aclType)) } aclEntriesLength := len(*aclEntries) if aclEntriesLength == 0 { return errors.New("ACL Entries too short") } entry := (*aclEntries)[aclEntriesLength-1] entry.Sections = append(entry.Sections, element) (*aclEntries)[aclEntriesLength-1] = entry return nil } // create a new ACL func (s *SDDLString) startACL(aclType rune) error { var aclEntries *[]ACLEntry switch aclType { case 'D': aclEntries = &s.DACL.ACLEntries case 'S': aclEntries = &s.SACL.ACLEntries default: return fmt.Errorf("%s ACL type invalid", string(aclType)) } *aclEntries = append(*aclEntries, ACLEntry{Sections: make([]string, 0)}) return nil } func (s *SDDLString) setACLFlags(flags string, aclType rune) error { var aclFlags *string switch aclType { case 'D': aclFlags = &s.DACL.Flags case 'S': aclFlags = &s.SACL.Flags default: return fmt.Errorf("%s ACL type invalid", string(aclType)) } *aclFlags = strings.TrimSpace(flags) return nil } func (s SDDLString) Compare(other SDDLString) bool { matching := true s, _ = ParseSDDL(s.PortableString()) o, _ := ParseSDDL(other.PortableString()) matching = matching && (s.OwnerSID == o.OwnerSID) // Compare owners matching = matching && (s.GroupSID == o.GroupSID) // compare flags matching = matching && compareFlags(strings.TrimSuffix(s.DACL.Flags, "NO_ACCESS_CONTROL"), strings.TrimSuffix(o.DACL.Flags, "NO_ACCESS_CONTROL")) matching = matching && compareFlags(strings.TrimSuffix(s.SACL.Flags, "NO_ACCESS_CONTROL"), strings.TrimSuffix(o.SACL.Flags, "NO_ACCESS_CONTROL")) // compare ACEs matching = matching && compareACEs(s.DACL.ACLEntries, o.DACL.ACLEntries) matching = matching && compareACEs(s.SACL.ACLEntries, o.SACL.ACLEntries) return matching } func compareFlags(a, b string) bool { a = strings.ToUpper(a) b = strings.ToUpper(b) if len(a) != len(b) { // obvious indicator return false } aEntries := make(map[string]bool) if len(a)%2 != 0 { // this only happens with P (protected). It could also happen with A or D, but we don't use this function for ACE type. aidx := strings.IndexByte(a, 'P') bidx := strings.IndexByte(b, 'P') a = a[:aidx] + a[aidx+1:] b = b[:bidx] + b[bidx+1:] } for i := 0; i < len(a); i += 2 { // flags, outside of NO_ACCESS_CONTROL (which should be trimmed before hitting this) are pairs of two upper-case letters. aEntries[a[i:i+2]] = true } for i := 0; i < len(b); i += 2 { str := b[i : i+2] if ok := aEntries[str]; ok { delete(aEntries, str) } else { return false } } if len(aEntries) > 0 { return false } return true } func compareACEs(a, b []ACLEntry) bool { if len(a) != len(b) { // obvious indicator return false } aMismatches := make([]ACLEntry, len(a)) copy(aMismatches, a) for _, bACE := range b { foundMatch := false for k, aACE := range aMismatches { aceMatch := true if len(aACE.Sections) != len(bACE.Sections) { continue // not a match, for sure } aceMatch = aceMatch && (aACE.Sections[0] == bACE.Sections[0]) // match ace type aceMatch = aceMatch && compareFlags(aACE.Sections[1], bACE.Sections[1]) // compare ace flags aceMatch = aceMatch && compareFlags(aACE.Sections[2], bACE.Sections[2]) // compare rights aceMatch = aceMatch && aACE.Sections[3] == bACE.Sections[3] // compare object guid aceMatch = aceMatch && aACE.Sections[4] == bACE.Sections[4] // compare inherit object guid aceMatch = aceMatch && aACE.Sections[5] == bACE.Sections[5] // compare SID if len(aACE.Sections) == 7 { aceMatch = aceMatch && aACE.Sections[6] == bACE.Sections[6] // compare resource attribute (in a naive way, since we don't use them in tests.) } if aceMatch { // delete matches. aMismatches = append(aMismatches[:k], aMismatches[k+1:]...) foundMatch = true break } } if !foundMatch { return false } } return true }