mirror of
https://github.com/fortra/impacket
synced 2026-06-08 14:15:13 +00:00
d029698149
* 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>
622 lines
21 KiB
Python
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()
|