mirror of
https://github.com/jfjallid/go-smb
synced 2026-06-08 15:02:50 +00:00
Support ACCESS_*_OBJECT_ACE and add GUID helpers
Parse and marshal the object-ACE family (MS-DTYP 2.4.4.3-2.4.4.6): read the Flags field and the optional ObjectType / InheritedObjectType GUIDs (gated by IsObjectAceType) before the SID, leaving plain ACEs untouched so existing Mask/Sid callers are unaffected. Expose the GUIDs through AcePermissions and add GuidToString/GuidFromString for the little-endian object-ACE wire layout, with round-trip tests.
This commit is contained in:
+100
-11
@@ -33,7 +33,7 @@ import (
|
||||
var (
|
||||
le = binary.LittleEndian
|
||||
be = binary.BigEndian
|
||||
log = golog.Get("github.com/jfjallid/go-smb/msdtyp")
|
||||
log = golog.Get("github.com/jfjallid/go-smb/msdtyp").SetDisplayName("msdtyp")
|
||||
)
|
||||
|
||||
// MS-DTYP Section 2.4.6 Security_Descriptor Control Flag
|
||||
@@ -126,6 +126,32 @@ var aceFlagsMap = map[byte]string{
|
||||
FailedAccessAceFlag: "FailedAccessAce",
|
||||
}
|
||||
|
||||
// MS-DTYP Section 2.4.4.3 ACCESS_ALLOWED_OBJECT_ACE - Flags field bits that
|
||||
// indicate which of the optional ObjectType / InheritedObjectType GUIDs are
|
||||
// present in the ACE body.
|
||||
const (
|
||||
AceObjectTypePresent uint32 = 0x00000001
|
||||
AceInheritedObjectTypePresent uint32 = 0x00000002
|
||||
)
|
||||
|
||||
var objectAceTypes = map[byte]bool{
|
||||
AccessAllowedObjectAceType: true,
|
||||
AccessDeniedObjectAceType: true,
|
||||
SystemAuditObjectAceType: true,
|
||||
SystemAlarmObjectAceType: true,
|
||||
AccessAllowedCallbackObjectAceType: true,
|
||||
AccessDeniedCallbackObjectAceType: true,
|
||||
SystemAuditCallbackObjectAceType: true,
|
||||
SystemAlarmCallbackObjectAceType: true,
|
||||
}
|
||||
|
||||
// IsObjectAceType reports whether an ACE type carries the optional Flags +
|
||||
// ObjectType + InheritedObjectType GUID fields (the ACCESS_*_OBJECT_ACE family,
|
||||
// MS-DTYP 2.4.4.3-2.4.4.6) rather than the plain Mask|SID body.
|
||||
func IsObjectAceType(t byte) bool {
|
||||
return objectAceTypes[t]
|
||||
}
|
||||
|
||||
const (
|
||||
AccessMaskGenericRead = "GENERIC_READ"
|
||||
AccessMaskGenericWrite = "GENERIC_WRITE"
|
||||
@@ -185,11 +211,17 @@ type ACEHeader struct {
|
||||
Size uint16 //Includes header size?
|
||||
}
|
||||
|
||||
// MS-DTYP Section 2.4.4.2 ACCESS_ALLOWED_ACE
|
||||
// MS-DTYP Section 2.4.4.2 ACCESS_ALLOWED_ACE and 2.4.4.3 ACCESS_ALLOWED_OBJECT_ACE.
|
||||
// ObjectFlags/ObjectType/InheritedObjectType are only populated (and only
|
||||
// (un)marshalled) for the object-ACE family, see IsObjectAceType. They are zero
|
||||
// for basic ACEs, so existing callers reading Mask/Sid are unaffected.
|
||||
type ACE struct {
|
||||
Header ACEHeader
|
||||
Mask uint32
|
||||
Sid SID //Must be multiple of 4
|
||||
Header ACEHeader
|
||||
Mask uint32
|
||||
ObjectFlags uint32 // object ACEs only; which GUIDs follow
|
||||
ObjectType [16]byte // present iff ObjectFlags&AceObjectTypePresent
|
||||
InheritedObjectType [16]byte // present iff ObjectFlags&AceInheritedObjectTypePresent
|
||||
Sid SID //Must be multiple of 4
|
||||
}
|
||||
|
||||
// MS-DTYP Section 2.4.2.3 RPC_SID
|
||||
@@ -215,11 +247,13 @@ type SecurityDescriptor struct {
|
||||
}
|
||||
|
||||
type AcePermissions struct {
|
||||
AceType string
|
||||
AceFlags byte
|
||||
AceFlagStrings string
|
||||
Permissions []string
|
||||
Sid string
|
||||
AceType string
|
||||
AceFlags byte
|
||||
AceFlagStrings string
|
||||
Permissions []string
|
||||
Sid string
|
||||
ObjectType string // GUID string; empty unless an object ACE with ObjectType present
|
||||
InheritedObjectType string // GUID string; empty unless an object ACE with InheritedObjectType present
|
||||
}
|
||||
|
||||
type PaclPermissions struct {
|
||||
@@ -517,6 +551,29 @@ func (s *ACE) MarshalBinary() (ret []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Object ACEs carry a Flags field followed by 0-2 GUIDs before the SID.
|
||||
if IsObjectAceType(s.Header.Type) {
|
||||
err = binary.Write(w, le, s.ObjectFlags)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
if s.ObjectFlags&AceObjectTypePresent != 0 {
|
||||
err = binary.Write(w, le, s.ObjectType)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if s.ObjectFlags&AceInheritedObjectTypePresent != 0 {
|
||||
err = binary.Write(w, le, s.InheritedObjectType)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Encode ACE SID
|
||||
sidBuf, err := s.Sid.MarshalBinary()
|
||||
if err != nil {
|
||||
@@ -557,6 +614,29 @@ func readACE(r *bytes.Reader) (a *ACE, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Object ACEs carry a Flags field followed by 0-2 GUIDs before the SID.
|
||||
if IsObjectAceType(a.Header.Type) {
|
||||
err = binary.Read(r, le, &a.ObjectFlags)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
if a.ObjectFlags&AceObjectTypePresent != 0 {
|
||||
err = binary.Read(r, le, &a.ObjectType)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if a.ObjectFlags&AceInheritedObjectTypePresent != 0 {
|
||||
err = binary.Read(r, le, &a.InheritedObjectType)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sid, err := ReadSID(r)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
@@ -665,13 +745,22 @@ func (s *PACL) UnmarshalBinary(buf []byte) (err error) {
|
||||
func (a ACE) Permissions() AcePermissions {
|
||||
perms := ParseAccessMask(a.Mask)
|
||||
sidStr := a.Sid.ToString()
|
||||
return AcePermissions{
|
||||
p := AcePermissions{
|
||||
Sid: sidStr,
|
||||
Permissions: perms,
|
||||
AceType: AceTypeMap[a.Header.Type],
|
||||
AceFlags: a.Header.Flags,
|
||||
AceFlagStrings: ParseAceFlags(a.Header.Flags),
|
||||
}
|
||||
if IsObjectAceType(a.Header.Type) {
|
||||
if a.ObjectFlags&AceObjectTypePresent != 0 {
|
||||
p.ObjectType = GuidToString(a.ObjectType)
|
||||
}
|
||||
if a.ObjectFlags&AceInheritedObjectTypePresent != 0 {
|
||||
p.InheritedObjectType = GuidToString(a.InheritedObjectType)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *PACL) Permissions() PaclPermissions {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// MIT License
|
||||
//
|
||||
// # Copyright (c) 2025 Jimmy Fjällid
|
||||
//
|
||||
// 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 msdtyp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// resetPasswordGUID is the extended-right GUID for User-Force-Change-Password.
|
||||
const resetPasswordGUID = "00299570-246d-11d0-a768-00aa006e0529"
|
||||
|
||||
func mustSID(t *testing.T, s string) SID {
|
||||
t.Helper()
|
||||
sid, err := ConvertStrToSID(s)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertStrToSID(%q): %v", s, err)
|
||||
}
|
||||
return *sid
|
||||
}
|
||||
|
||||
func TestGuidStringRoundTrip(t *testing.T) {
|
||||
b, err := GuidFromString(resetPasswordGUID)
|
||||
if err != nil {
|
||||
t.Fatalf("GuidFromString: %v", err)
|
||||
}
|
||||
// Verify the documented little-endian wire layout for the first 3 groups.
|
||||
want := []byte{0x70, 0x95, 0x29, 0x00, 0x6d, 0x24, 0xd0, 0x11,
|
||||
0xa7, 0x68, 0x00, 0xaa, 0x00, 0x6e, 0x05, 0x29}
|
||||
if !bytes.Equal(b[:], want) {
|
||||
t.Fatalf("GuidFromString wire bytes = %x, want %x", b[:], want)
|
||||
}
|
||||
if got := GuidToString(b); got != resetPasswordGUID {
|
||||
t.Fatalf("GuidToString = %q, want %q", got, resetPasswordGUID)
|
||||
}
|
||||
// Braces and uppercase should be tolerated and normalised.
|
||||
if _, err := GuidFromString("{" + resetPasswordGUID + "}"); err != nil {
|
||||
t.Fatalf("GuidFromString with braces: %v", err)
|
||||
}
|
||||
if _, err := GuidFromString("not-a-guid"); err == nil {
|
||||
t.Fatalf("GuidFromString accepted an invalid GUID")
|
||||
}
|
||||
}
|
||||
|
||||
func aceRoundTrip(t *testing.T, in ACE) ACE {
|
||||
t.Helper()
|
||||
buf, err := in.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("ACE.MarshalBinary: %v", err)
|
||||
}
|
||||
if int(in.Header.Size) != len(buf) {
|
||||
t.Fatalf("Header.Size %d != marshalled length %d", in.Header.Size, len(buf))
|
||||
}
|
||||
var out ACE
|
||||
if err := out.UnmarshalBinary(buf); err != nil {
|
||||
t.Fatalf("ACE.UnmarshalBinary: %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestBasicACERoundTrip(t *testing.T) {
|
||||
sid := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-512")
|
||||
size := 4 + 4 + 8 + 4*len(sid.SubAuthorities) // header+mask+sid
|
||||
in := ACE{
|
||||
Header: ACEHeader{Type: AccessAllowedAceType, Flags: ContainerInheritAce, Size: uint16(size)},
|
||||
Mask: 0x000F01FF,
|
||||
Sid: sid,
|
||||
}
|
||||
out := aceRoundTrip(t, in)
|
||||
if out.Header.Type != AccessAllowedAceType || out.Mask != 0x000F01FF {
|
||||
t.Fatalf("basic ACE header/mask mismatch: %+v", out)
|
||||
}
|
||||
if out.Sid.ToString() != sid.ToString() {
|
||||
t.Fatalf("basic ACE SID = %s, want %s", out.Sid.ToString(), sid.ToString())
|
||||
}
|
||||
// A basic ACE must not consume/emit any object fields.
|
||||
if out.ObjectFlags != 0 || out.ObjectType != ([16]byte{}) {
|
||||
t.Fatalf("basic ACE leaked object fields: %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectACERoundTripObjectTypeOnly(t *testing.T) {
|
||||
sid := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-1104")
|
||||
guid, _ := GuidFromString(resetPasswordGUID)
|
||||
sidLen := 8 + 4*len(sid.SubAuthorities)
|
||||
size := 4 + 4 + 4 + 16 + sidLen // header+mask+objflags+1guid+sid
|
||||
in := ACE{
|
||||
Header: ACEHeader{Type: AccessAllowedObjectAceType, Size: uint16(size)},
|
||||
Mask: 0x00000100, // ADS_RIGHT_DS_CONTROL_ACCESS
|
||||
ObjectFlags: AceObjectTypePresent,
|
||||
ObjectType: guid,
|
||||
Sid: sid,
|
||||
}
|
||||
out := aceRoundTrip(t, in)
|
||||
if out.ObjectFlags != AceObjectTypePresent {
|
||||
t.Fatalf("ObjectFlags = %#x, want %#x", out.ObjectFlags, AceObjectTypePresent)
|
||||
}
|
||||
if out.ObjectType != guid {
|
||||
t.Fatalf("ObjectType = %x, want %x", out.ObjectType, guid)
|
||||
}
|
||||
if out.InheritedObjectType != ([16]byte{}) {
|
||||
t.Fatalf("InheritedObjectType should be zero, got %x", out.InheritedObjectType)
|
||||
}
|
||||
if out.Sid.ToString() != sid.ToString() {
|
||||
t.Fatalf("object ACE SID = %s, want %s", out.Sid.ToString(), sid.ToString())
|
||||
}
|
||||
p := out.Permissions()
|
||||
if p.ObjectType != resetPasswordGUID {
|
||||
t.Fatalf("Permissions().ObjectType = %q, want %q", p.ObjectType, resetPasswordGUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectACERoundTripBothGUIDs(t *testing.T) {
|
||||
sid := mustSID(t, "S-1-5-11")
|
||||
ot, _ := GuidFromString(resetPasswordGUID)
|
||||
iot, _ := GuidFromString("bf967aba-0de6-11d0-a285-00aa003049e2") // user class
|
||||
sidLen := 8 + 4*len(sid.SubAuthorities)
|
||||
size := 4 + 4 + 4 + 16 + 16 + sidLen
|
||||
in := ACE{
|
||||
Header: ACEHeader{Type: AccessAllowedObjectAceType, Flags: ContainerInheritAce, Size: uint16(size)},
|
||||
Mask: 0x00000010,
|
||||
ObjectFlags: AceObjectTypePresent | AceInheritedObjectTypePresent,
|
||||
ObjectType: ot,
|
||||
InheritedObjectType: iot,
|
||||
Sid: sid,
|
||||
}
|
||||
out := aceRoundTrip(t, in)
|
||||
if out.ObjectType != ot || out.InheritedObjectType != iot {
|
||||
t.Fatalf("GUID mismatch: ot=%x iot=%x", out.ObjectType, out.InheritedObjectType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityDescriptorWithObjectACE exercises the full SD marshal/unmarshal
|
||||
// path with a DACL containing both a basic and an object ACE.
|
||||
func TestSecurityDescriptorWithObjectACE(t *testing.T) {
|
||||
owner := mustSID(t, "S-1-5-32-544")
|
||||
basicSID := mustSID(t, "S-1-5-18")
|
||||
objSID := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-1104")
|
||||
guid, _ := GuidFromString(resetPasswordGUID)
|
||||
|
||||
basic := ACE{
|
||||
Header: ACEHeader{Type: AccessAllowedAceType, Size: uint16(8 + 8 + 4*len(basicSID.SubAuthorities))},
|
||||
Mask: 0x000F01FF,
|
||||
Sid: basicSID,
|
||||
}
|
||||
obj := ACE{
|
||||
Header: ACEHeader{Type: AccessAllowedObjectAceType, Size: uint16(4 + 4 + 4 + 16 + 8 + 4*len(objSID.SubAuthorities))},
|
||||
Mask: 0x00000100,
|
||||
ObjectFlags: AceObjectTypePresent,
|
||||
ObjectType: guid,
|
||||
Sid: objSID,
|
||||
}
|
||||
basicBuf, _ := basic.MarshalBinary()
|
||||
objBuf, _ := obj.MarshalBinary()
|
||||
aclSize := 8 + len(basicBuf) + len(objBuf)
|
||||
dacl := &PACL{
|
||||
AclRevision: 4, // ACL_REVISION_DS
|
||||
AclSize: uint16(aclSize),
|
||||
ACLS: []ACE{basic, obj},
|
||||
}
|
||||
sd := &SecurityDescriptor{
|
||||
Revision: 1,
|
||||
Control: SecurityDescriptorFlagSR,
|
||||
OwnerSid: &owner,
|
||||
Dacl: dacl,
|
||||
}
|
||||
buf, err := sd.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("SecurityDescriptor.MarshalBinary: %v", err)
|
||||
}
|
||||
var out SecurityDescriptor
|
||||
if err := out.UnmarshalBinary(buf); err != nil {
|
||||
t.Fatalf("SecurityDescriptor.UnmarshalBinary: %v", err)
|
||||
}
|
||||
if out.OwnerSid == nil || out.OwnerSid.ToString() != owner.ToString() {
|
||||
t.Fatalf("owner mismatch: %+v", out.OwnerSid)
|
||||
}
|
||||
if out.Dacl == nil || len(out.Dacl.ACLS) != 2 {
|
||||
t.Fatalf("expected 2 DACL ACEs, got %+v", out.Dacl)
|
||||
}
|
||||
if out.Dacl.ACLS[0].Sid.ToString() != basicSID.ToString() {
|
||||
t.Fatalf("basic ACE SID mismatch: %s", out.Dacl.ACLS[0].Sid.ToString())
|
||||
}
|
||||
got := out.Dacl.ACLS[1]
|
||||
if got.Header.Type != AccessAllowedObjectAceType ||
|
||||
got.ObjectType != guid ||
|
||||
got.Sid.ToString() != objSID.ToString() {
|
||||
t.Fatalf("object ACE mismatch: %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ package msdtyp
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
@@ -473,6 +474,56 @@ func ConvertStrToSID(s string) (sid *SID, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GuidToString renders a GUID stored in the standard little-endian wire layout
|
||||
// (as used inside object ACEs: Data1 uint32 LE, Data2/Data3 uint16 LE, Data4 8
|
||||
// bytes as-is) to the canonical "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" form.
|
||||
func GuidToString(b [16]byte) string {
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
le.Uint32(b[0:4]),
|
||||
le.Uint16(b[4:6]),
|
||||
le.Uint16(b[6:8]),
|
||||
be.Uint16(b[8:10]),
|
||||
b[10:16],
|
||||
)
|
||||
}
|
||||
|
||||
// GuidFromString parses a canonical GUID string into the little-endian wire
|
||||
// layout expected inside object ACEs. Surrounding braces are tolerated.
|
||||
func GuidFromString(s string) (b [16]byte, err error) {
|
||||
s = strings.TrimSuffix(strings.TrimPrefix(s, "{"), "}")
|
||||
parts := strings.Split(s, "-")
|
||||
if len(parts) != 5 {
|
||||
err = fmt.Errorf("invalid GUID %q", s)
|
||||
return
|
||||
}
|
||||
d1, err := strconv.ParseUint(parts[0], 16, 32)
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("invalid GUID %q: %w", s, err)
|
||||
}
|
||||
d2, err := strconv.ParseUint(parts[1], 16, 16)
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("invalid GUID %q: %w", s, err)
|
||||
}
|
||||
d3, err := strconv.ParseUint(parts[2], 16, 16)
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("invalid GUID %q: %w", s, err)
|
||||
}
|
||||
g4, err := hex.DecodeString(parts[3])
|
||||
if err != nil || len(g4) != 2 {
|
||||
return b, fmt.Errorf("invalid GUID %q: bad group 4", s)
|
||||
}
|
||||
g5, err := hex.DecodeString(parts[4])
|
||||
if err != nil || len(g5) != 6 {
|
||||
return b, fmt.Errorf("invalid GUID %q: bad group 5", s)
|
||||
}
|
||||
le.PutUint32(b[0:4], uint32(d1))
|
||||
le.PutUint16(b[4:6], uint16(d2))
|
||||
le.PutUint16(b[6:8], uint16(d3))
|
||||
copy(b[8:10], g4)
|
||||
copy(b[10:16], g5)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func ConvertToFiletime(t time.Time) uint64 {
|
||||
// Credit to https://github.com/Azure/go-ntlmssp/blob/master/unicode.go for logic
|
||||
ft := uint64(t.UnixNano()) / 100
|
||||
|
||||
Reference in New Issue
Block a user