Files
Gaffner d029698149 smbclient ACL Support (#1240)
* Added smbcacls

* Changed example script

* improve ACL manage

* improve acl manage

* improved acl

* Closing all handles

* fixed directory and handle is open bug in acl.py

* Changed help a little bit

* Ignore from empty ACL

* fix indentation

* removee example to different PR

---------

Co-authored-by: john57 <your_email_address@example.com>
2026-02-18 00:42:34 -03:00

622 lines
21 KiB
Python

# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies
#
# All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Description:
# Windows ACL (Access Control List) handling for SMB file permissions.
# Provides structures and utilities for reading and modifying NTFS security descriptors.
#
# Author:
# Gefen Altshuler (@gaffner)
#
from impacket.structure import Structure
from impacket.dcerpc.v5 import lsad, lsat
from impacket.dcerpc.v5.transport import SMBTransport
from impacket.smbconnection import SMBConnection
from impacket.smb3structs import FileSecInformation, FILE_OPEN_REPARSE_POINT, GENERIC_ALL, READ_CONTROL
import struct
# ACE FLAGS
SMB_ACE_FLAG_OI = 0x1
SMB_ACE_FLAG_CI = 0x2
SMB_ACE_FLAG_IO = 0x8
SMB_ACE_FLAG_NP = 0x4
SMB_ACE_FLAG_I = 0x10
# STANDARD RIGHTS
SEC_INFO_STANDARD_WRITE = 0x8
SEC_INFO_STANDARD_READ = 0x2
SEC_INFO_STANDARD_DELETE = 0x1
# SPECIFIC RIGHTS
SEC_INFO_SPECIFIC_WRITE = 0x116
SEC_INFO_SPECIFIC_EXECUTE = 0x20
SEC_INFO_SPECIFIC_FULL = 0x1FF
# COMBINED RIGHTS
SEC_READ_RIGHT = 0x00120089
SUPPORTED_PERMISSIONS = {
"R": 0x00120089,
"W": 0x00100116,
"D": 0x00110000,
"X": 0x00000020,
"F": 0x001F01FF,
}
# NT User (DACL) ACL
class FileNTUser( Structure ):
structure = (
("Revision", "<H=1"),
("Size", "<H=1"),
("NumACEs", "<I=1"),
("Buffer", ":"),
)
# SID
class ACL_SID( Structure ):
structure = (
("Revision", "<B"),
("NumAuth", "<B"),
("Authority", "6s"),
("Subauthorities", ":"),
)
def __repr__(self):
n = len( self["Subauthorities"] ) // 4
# Authority is 6 bytes stored as big-endian (most significant byte last for Windows SIDs)
authority = struct.unpack(">H", self["Authority"][4:6])[0]
return "-".join(
map(
str,
["S", int( self["Revision"] ), authority]
+ list( struct.unpack( "<{}I".format(int(n)), self["Subauthorities"] ) ),
)
)
@staticmethod
def build_from_string(data):
items = data.split( "-" )[1:] # delete the S prefix
revision = int( items[0] )
numAuth = int( items[1] )
sub_length = len( items ) - 2 # minus the revision and numAuth
subauthorities = struct.pack( "<{}I".format(sub_length), *tuple( map( int, items[2:] ) ) )
raw_sid = (
struct.pack( "<2B", revision, numAuth )
+ b"\x00" * 5
+ struct.pack( "<B", numAuth )
+ subauthorities
)
return ACL_SID( raw_sid )
def __str__(self):
return self.__repr__()
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
def split(self, *args, **kwargs):
return self.__str__().split( *args, **kwargs )
# NT ACE
class FileNTACE( Structure ):
structure = (
("Type", "<B"),
("NTACE_Flags", "<B"),
("Size", "<H"),
("SpecificRights", "<H"),
("StandardRights", "<B"),
("GenericRights", "<B"),
("_SID", "_-SID", '(self["Size"] - 8)'),
("SID", ':=""', ACL_SID),
)
def __str__(self):
flags = self.get_readable_ntace_flags()
specific = self.get_readable_specific_rights()
# If we have full control (F), don't show redundant standard rights
if specific == "(F)":
return flags + specific
# Otherwise show standard rights along with any specific rights
standard = self.get_readable_standard_rights()
return flags + standard + specific
def get_readable_ntace_flags(self):
"""
Return the NTACE flags in readable format
(OI) - object inherit
(CI) - container inherit
(IO) - inherit only
(NP) - don't propagate inherit
(I) - permission inherited from parent container
"""
flags = ""
if self["NTACE_Flags"] & SMB_ACE_FLAG_OI:
flags += "(OI)"
if self["NTACE_Flags"] & SMB_ACE_FLAG_CI:
flags += "(CI)"
if self["NTACE_Flags"] & SMB_ACE_FLAG_IO:
flags += "(IO)"
if self["NTACE_Flags"] & SMB_ACE_FLAG_NP:
flags += "(NP)"
if self["NTACE_Flags"] & SMB_ACE_FLAG_I:
flags += "(I)"
if flags == "":
return "" # No flags set
return flags
def get_readable_standard_rights(self):
"""
Return the standard rights in readable format
NOTE: does not cover all standard rights (WRITE_DAC and SYNC)
R - read-only access
W - write-only access
D - delete access
"""
flags = ""
if self["StandardRights"] & SEC_INFO_STANDARD_READ:
flags += "(R)"
if self["StandardRights"] & SEC_INFO_STANDARD_WRITE:
flags += "(w)"
if self["StandardRights"] & SEC_INFO_STANDARD_DELETE:
flags += "(D)"
return flags
def get_readable_specific_rights(self):
"""
Return the specific rights in readable format
NOTE: does not cover all specific rights (only write, execute, full control)
W - write access
X - execute access
F - full control
"""
if self["SpecificRights"] & SEC_INFO_SPECIFIC_FULL == SEC_INFO_SPECIFIC_FULL:
return "(F)" # Full control, no need to waste time
flags = ""
if self["SpecificRights"] & SEC_INFO_SPECIFIC_WRITE == SEC_INFO_SPECIFIC_WRITE:
flags += "(W)"
if (
self["SpecificRights"] & SEC_INFO_SPECIFIC_EXECUTE
== SEC_INFO_SPECIFIC_EXECUTE
):
flags += "(X)"
return flags
class SecurityAttributes:
"""
This class represents the security attributes of a file
"""
def __init__(self, owner, group):
self.owner = owner
self.group = group
self.dacls = {}
self.readable_dacls = {} # {name: dacls}
def __repr__(self):
return (
"Owner:\t{}\n"
"Group:\t{}\n"
"ACLs:\n\t"
"{}".format(self.owner, self.group,
"\n\t".join([str( self.readable_dacls[sid] ) for sid in self.readable_dacls])))
def __str__(self):
return self.__repr__()
def __eq__(self, other):
return self.__repr__() == other.__repr__()
class SMBFileACL:
"""
Manage Windows file ACLs over SMB. You can view, set, or remove ACLs.
You must have the appropriate permissions to do so.
"""
def __init__(self, ip=None, remote_name=None, username='', password='', domain='', lmhash='', nthash='', aesKey=None, doKerberos=False, kdcHost=None, smb_connection=None):
"""
@param ip: target server's remote address (IPv4, IPv6) or FQDN
@param remote_name: Remote NetBIOS name
@param username: username
@param password: password
@param domain: domain where the account is valid for
@param lmhash: LM hash for NTLM authentication
@param nthash: NT hash for NTLM authentication
@param aesKey: AES key for Kerberos authentication
@param doKerberos: Use Kerberos authentication
@param kdcHost: KDC hostname or IP address
@param smb_connection: existing SMBConnection to reuse (if provided, other auth params are ignored)
"""
self.sid_to_name = {
ACL_SID.build_from_string("S-1-5-18"): "NT AUTHORITY\\SYSTEM"
}
self.rid_to_name = {
"544": "BUILTIN\\Administrators",
"513": "Domain Users",
}
self._owns_connection = smb_connection is None
if smb_connection is not None:
self.connection = smb_connection
else:
self.connection = SMBConnection(remote_name, ip)
if doKerberos:
self.connection.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, kdcHost)
else:
self.connection.login(username, password, domain, lmhash, nthash)
self.dce_rpc = self.start_dce_rpc()
self.policy_handle = self.open_policy_handle()
self.transport = None
self.tid = None
self.fid = None
def close_connection(self):
"""
Disconnect from the tree id, close the file
and disconnect from the smb server
"""
# close policy handle and transport
if self.policy_handle:
lsad.hLsarClose(self.dce_rpc, self.policy_handle)
if self.transport:
self.transport.disconnect()
# close the smb connection only if we created it
if self._owns_connection:
self.connection.close()
def close_file(self):
"""
Close the tree ID and file ID handles
"""
if self.fid:
self.connection.closeFile(self.tid, self.fid)
if self.tid:
self.connection.disconnectTree(self.tid)
def open_file(self, share_name, file_name, desired_access=READ_CONTROL):
"""
Open the given file in the specified share
@param share_name: share to connect to
@param file_name: file to open
@return: tuple of (tid, fid)
"""
self.tid = self.connection.connectTree(share_name)
self.fid = self.connection.openFile(
self.tid, file_name, desiredAccess=desired_access, creationOption=FILE_OPEN_REPARSE_POINT
)
return self.tid, self.fid
def start_dce_rpc(self):
"""
Start a new DCE/RPC connection over SMB
@return: DCE/RPC connection
"""
self.transport = SMBTransport(
self.connection.getRemoteName(),
smb_connection=self.connection,
filename="lsarpc",
)
self.transport.connect()
dce = self.transport.get_dce_rpc()
return dce
def open_policy_handle(self):
"""
Open a new handle to MSRPC_UUID_LSAD
@return: policy handle
"""
self.dce_rpc.bind(lsad.MSRPC_UUID_LSAD)
policy_handle = lsad.hLsarOpenPolicy2(self.dce_rpc, lsad.POLICY_LOOKUP_NAMES)[
"PolicyHandle"
]
return policy_handle
def set_sid_to_name(self, sids, resp):
"""
Set the sid_to_name dictionary according to the given SIDs
:param sids: SIDs to translate to their names
:param resp: the response containing the names
:return: None
"""
names = [name["Name"] for name in resp["TranslatedNames"]["Names"]]
for i, name in enumerate(names):
# should check here if the name is real sid
if name in ("None", b""):
name = sids[i]
rid = name.split("-")[-1]
if rid in self.rid_to_name:
self.sid_to_name[ACL_SID.build_from_string(name)] = self.rid_to_name[rid]
elif sids[i] not in self.sid_to_name:
self.sid_to_name[sids[i]] = name
def sids_to_names(self, sids):
"""
Resolve SIDs to names using LSA_LookupSids
:param sids: list of SIDs
:return: list of usernames
"""
try:
resp = lsat.hLsarLookupSids2(self.dce_rpc, self.policy_handle, sids)
except lsat.DCERPCSessionError as session_error:
resp = session_error.packet
self.set_sid_to_name(sids, resp)
def name_to_sid(self, name):
"""
Translate name to SID using LSA_LookupNames
"""
try:
resp = lsat.hLsarLookupNames3(self.dce_rpc, self.policy_handle, [name])
return resp["TranslatedSids"]["Sids"][0]["Sid"].getData()[4:] # don't include the 'count' attribute
except Exception as e:
raise Exception(f"Failed to resolve name '{name}' to SID: {str(e)}")
def permissions_to_ace(self, username, permissions, action='grant'):
"""
Convert given permissions and user to binary format
@param username: username to add/remove permissions
@param permissions: permissions in the icacls format
@param action: 'grant', 'revoke', or 'delete'
"""
access_required = 0x00000000
invalid_perms = []
if permissions:
for permission in permissions.split(","):
try:
access_required |= SUPPORTED_PERMISSIONS[permission.upper()]
except KeyError:
invalid_perms.append(permission)
# Warn about invalid permissions
if invalid_perms:
import logging
logging.warning(f"Ignoring unsupported permissions: {', '.join(invalid_perms)}")
# check if we couldn't resolve any of the permissions
if not access_required:
raise Exception("No valid permissions specified")
sid_bytes = self.name_to_sid(username)
total_size = 8 + len(sid_bytes) # nt ace attributes length + sid length
permissions_as_bytes = (
struct.pack("<BBHI", 0x00, 0x00, total_size, access_required) + sid_bytes
)
ace = FileNTACE(permissions_as_bytes)
# Store the action type in the ACE for later use
ace.action = action
return ace
def get_security_attributes(self, sec):
"""
Gets the security information of the given FileSecInformation object
:param sec: FileSecInformation instance
:return: SecurityAttributes
"""
# get owner SID and Group SID
owner = ACL_SID(sec.rawData[sec["OffsetToOwner"]: sec["OffsetToGroup"]])
group = ACL_SID(sec.rawData[sec["OffsetToGroup"]: sec["OffsetToDACL"]])
self.sids_to_names([owner, group])
try:
owner_name = self.sid_to_name[owner]
except KeyError:
owner_name = owner
try:
group_name = self.sid_to_name[group]
except KeyError:
group_name = group
security_attributes = SecurityAttributes(owner_name, group_name)
# get all dacl's
nt = sec.rawData[sec["OffsetToDACL"]:]
ntuser = FileNTUser(nt)
ntace = ntuser["Buffer"]
while len(ntace):
face = FileNTACE(ntace) # set new FileNTACE
ntace = ntace[face["Size"]:] # slice the buffer
if face["SID"] == b'': continue
sid = ACL_SID(face["SID"]) # get the DACL SID
security_attributes.dacls[sid] = face
# Resolve all DACL SIDs to names
self.sids_to_names(list(security_attributes.dacls.keys()))
for sid, permissions in security_attributes.dacls.items():
try:
name = self.sid_to_name[sid]
except KeyError:
name = sid
security_attributes.readable_dacls[sid] = "{}:{}".format(name, permissions)
return security_attributes
def get_permissions(self, share_name, file_name):
"""
Connect to the given share and get the file permissions
@param share_name: the share name where the file is to be opened
@param file_name: file to get permissions from
@return: SecurityAttributes
"""
# set the file and tree handles for the given file
self.open_file(share_name=share_name, file_name=file_name)
try:
# query the file security information
result = self.connection._SMBConnection.queryInfo(
self.tid,
self.fid,
fileInfoClass=0,
infoType=3,
additionalInformation=0x00000017,
)
sec = FileSecInformation(result)
# get security attributes
security_attributes = self.get_security_attributes(sec)
return security_attributes
finally:
# ensure file handles are always closed, even on error
self.close_file()
@staticmethod
def insert_permission(sec, permission):
"""
This function will get the current security descriptor, and then
insert the given permission to it. Supports different actions:
- grant: Add permissions to existing ACE (OR operation)
- revoke: Remove specific permissions from existing ACE (AND NOT operation)
- delete: Remove the entire ACE
@param sec: current security descriptor
@param permission: new permission ACE with action attribute
"""
ntuser = FileNTUser(sec.rawData[sec["OffsetToDACL"]:])
ntace = ntuser["Buffer"]
new_buffer = b""
sid_found = False
ace_deleted = False
action = permission.action
# enumerate the current permissions and search for the given permission sid
while len(ntace):
delete_ace = False
face = FileNTACE(ntace) # set new FileNTACE
sid = ACL_SID(face["SID"]) # get the DACL SID
if sid.rawData == permission["SID"]:
sid_found = True
if action == 'grant':
# Add the new permissions to the current permission (OR operation)
face["SpecificRights"] |= permission["SpecificRights"]
face["StandardRights"] |= permission["StandardRights"]
face["GenericRights"] |= permission["GenericRights"]
elif action == 'revoke':
# Remove specific permissions (AND NOT operation)
face["SpecificRights"] &= ~permission["SpecificRights"]
face["StandardRights"] &= ~permission["StandardRights"]
face["GenericRights"] &= ~permission["GenericRights"]
# If all permissions are revoked, remove the ACE entirely (Windows behavior)
if (face["SpecificRights"] == 0 and
face["StandardRights"] == 0 and
face["GenericRights"] == 0):
delete_ace = True
ace_deleted = True
elif action == 'delete':
delete_ace = True
ace_deleted = True
# Only keep ACE if not marked for deletion
if not delete_ace:
new_buffer += face.getData()
ntace = ntace[face["Size"]:] # slice the buffer
if sid_found:
# replace the current buffer with the modified one
ntuser["Buffer"] = new_buffer
ntuser["Size"] = len(new_buffer) + 8 # add the nt user attribute size
# Decrement ACE count if an ACE was deleted
if ace_deleted:
ntuser["NumACEs"] -= 1
elif action != 'delete' and action != 'revoke':
# insert the permissions on the top of the other permissions
# (only for grant when ACE doesn't exist yet)
ntuser["Size"] += len(
permission.getData()
) # add the nt user attribute size
ntuser["NumACEs"] += 1
ntuser["Buffer"] = permission.getData() + ntuser["Buffer"]
owner = sec.rawData[sec["OffsetToOwner"]: sec["OffsetToGroup"]]
group = sec.rawData[sec["OffsetToGroup"]: sec["OffsetToDACL"]]
sec_info_blob = sec.getData() + owner + group + ntuser.getData()
return sec_info_blob
def set_permissions(self, share_name, file_name, user, permissions, action='grant'):
"""
Add, remove, or modify permissions for a given user to the given file
@param share_name: the share name where the file is to be opened
@param file_name: file to set permissions to
@param user: user to edit permissions for; can be a SID as well
@param permissions: permissions in the icacls format (example: R,W,X,D).
NOTE: not all permission types are supported; currently supporting:
R - read-only access
W - write-only access
D - delete access
X - execute access
F - full control
@param action: action to perform - 'grant' (add), 'revoke' (remove), 'delete' (remove ACE)
@return: bool. whether the operation succeeded or not
"""
# open file descriptor
self.tid, self.fid = self.open_file(share_name, file_name, GENERIC_ALL)
try:
# permissions_to_ace function returns the new ACE to add
permission = self.permissions_to_ace(user, permissions, action)
result = self.connection._SMBConnection.queryInfo(
self.tid,
self.fid,
fileInfoClass=0,
infoType=3,
additionalInformation=0x00000017,
)
sec = FileSecInformation(result)
security_descriptor = self.insert_permission(sec=sec, permission=permission)
result = self.connection._SMBConnection.setInfo(
self.tid,
self.fid,
fileInfoClass=0,
infoType=3,
additionalInformation=0x04,
inputBlob=security_descriptor,
)
return result
finally:
# ensure file handles are always closed, even if permission resolution fails
self.close_file()