Search code examples
windowsaclprivilegeselevated-privileges

Why does enabled SeRestorePrivilege allow for READ_CONTROL access to a file


According to Windows API documentation,

SeBackupPrivilege

causes the system to grant all read access control to any file, regardless of the access control list (ACL) specified for the file. Any access request other than read is still evaluated with the ACL. The following access rights are granted if this privilege is held: READ_CONTROL, ACCESS_SYSTEM_SECURITY, FILE_GENERIC_READ, FILE_TRAVERSE

SeRestorePrivilege:

causes the system to grant all write access control to any file, regardless of the ACL specified for the file. Any access request other than write is still evaluated with the ACL. The following access rights are granted if this privilege is held: WRITE_DAC, WRITE_OWNER, ACCESS_SYSTEM_SECURITY, FILE_GENERIC_WRITE, FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, DELETE

I noticed, however, that if a process token has SeRestorePrivilege, opening a file with READ_CONTROL succeeds. Why is it so?

Code:

import os
import sys

import ntsecuritycon
import win32con
import win32file
import win32security
import win32api


def print_token_info():
    tok = win32security.OpenProcessToken(win32api.GetCurrentProcess(), win32security.TOKEN_QUERY)
    sid, attr = win32security.GetTokenInformation(tok, win32security.TokenUser)
    name, domain, sid_name_use = win32security.LookupAccountSid(None, sid)
    print(f"User: {name}@{domain}")

    sid = win32security.GetTokenInformation(tok, win32security.TokenOwner)
    name, domain, sid_name_use = win32security.LookupAccountSid(None, sid)
    print(f"Owner of new files: {name}@{domain}")

    privileges = win32security.GetTokenInformation(tok, win32security.TokenPrivileges)
    print("Privileges")
    priv_attr_flags = {
        win32security.SE_PRIVILEGE_ENABLED: 'SE_PRIVILEGE_ENABLED',
        win32security.SE_PRIVILEGE_ENABLED_BY_DEFAULT: 'SE_PRIVILEGE_ENABLED_BY_DEFAULT',
        win32security.SE_PRIVILEGE_REMOVED: 'SE_PRIVILEGE_REMOVED',
        win32security.SE_PRIVILEGE_USED_FOR_ACCESS: 'SE_PRIVILEGE_USED_FOR_ACCESS',
    }
    for luid, attr in privileges:
        priv_name = win32security.LookupPrivilegeName(None, luid)
        flag_str = []
        for flag in priv_attr_flags:
            if (attr & flag) == flag:
                flag_str.append(priv_attr_flags[flag])
        print(f"- {priv_name} -> {attr} ({', '.join(flag_str)})")
    win32api.CloseHandle(tok)


def disable_privilege(privilege_name):
    print(f"Disabling privilege {privilege_name}")

    tok = win32security.OpenProcessToken(
        win32api.GetCurrentProcess(), win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY
    )
    luid = win32security.LookupPrivilegeValue(None, privilege_name)
    se_privilege_disabled = 0
    new_state = [(luid, se_privilege_disabled)]
    win32security.AdjustTokenPrivileges(tok, 0, new_state)
    win32api.CloseHandle(tok)


def enable_privilege(privilege_name):
    print(f"Enabling privilege {privilege_name}")

    tok = win32security.OpenProcessToken(
        win32api.GetCurrentProcess(), win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY
    )
    luid = win32security.LookupPrivilegeValue(None, privilege_name)
    new_state = [(luid, win32security.SE_PRIVILEGE_ENABLED)]
    win32security.AdjustTokenPrivileges(tok, 0, new_state)
    win32api.CloseHandle(tok)


def open_handle(path):
    try:
        handle = win32file.CreateFileW(
            path,
            ntsecuritycon.READ_CONTROL,
            0,
            None,
            win32con.OPEN_EXISTING,
            win32file.FILE_FLAG_OPEN_REPARSE_POINT | win32file.FILE_FLAG_BACKUP_SEMANTICS,
            None,
        )
        handle.Close()
        print(f"Successfully opened {path}")
    except Exception as e:
        print(f"Failed to open {path}: {e}")


if __name__ == '__main__':
    path = sys.argv[1]

    with open(path, "w") as fo:
        pass

    dacl = win32security.ACL()
    dacl.SetEntriesInAcl([])
    win32security.SetNamedSecurityInfo(
        path,
        win32security.SE_FILE_OBJECT,
        win32security.DACL_SECURITY_INFORMATION | win32security.PROTECTED_DACL_SECURITY_INFORMATION,
        None,
        None,
        dacl,
        None,
    )

    print_token_info()
    open_handle(path)
    print()

    enable_privilege(win32security.SE_BACKUP_NAME)
    # print_token_info()
    open_handle(path)
    print()

    disable_privilege(win32security.SE_BACKUP_NAME)
    # print_token_info()
    open_handle(path)
    print()

    enable_privilege(win32security.SE_RESTORE_NAME)
    # print_token_info()
    open_handle(path)
    print()

    disable_privilege(win32security.SE_RESTORE_NAME)
    # print_token_info()
    open_handle(path)
    print()

Output on elevated console:

> python .\test_access.py C:\tmp\test\file-10
User: dev@CBIT-02
Owner of new files: Administrators@BUILTIN
Privileges
- SeIncreaseQuotaPrivilege -> 0 ()
- SeSecurityPrivilege -> 0 ()
- SeTakeOwnershipPrivilege -> 0 ()
- SeLoadDriverPrivilege -> 0 ()
- SeSystemProfilePrivilege -> 0 ()
- SeSystemtimePrivilege -> 0 ()
- SeProfileSingleProcessPrivilege -> 0 ()
- SeIncreaseBasePriorityPrivilege -> 0 ()
- SeCreatePagefilePrivilege -> 0 ()
- SeBackupPrivilege -> 0 ()
- SeRestorePrivilege -> 0 ()
- SeShutdownPrivilege -> 0 ()
- SeDebugPrivilege -> 2 (SE_PRIVILEGE_ENABLED)
- SeSystemEnvironmentPrivilege -> 0 ()
- SeChangeNotifyPrivilege -> 3 (SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_ENABLED_BY_DEFAULT)
- SeRemoteShutdownPrivilege -> 0 ()
- SeUndockPrivilege -> 0 ()
- SeManageVolumePrivilege -> 0 ()
- SeImpersonatePrivilege -> 3 (SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_ENABLED_BY_DEFAULT)
- SeCreateGlobalPrivilege -> 3 (SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_ENABLED_BY_DEFAULT)
- SeIncreaseWorkingSetPrivilege -> 0 ()
- SeTimeZonePrivilege -> 0 ()
- SeCreateSymbolicLinkPrivilege -> 2 (SE_PRIVILEGE_ENABLED)
- SeDelegateSessionUserImpersonatePrivilege -> 0 ()
Failed to open C:\tmp\test\file-10: (5, 'CreateFileW', 'Access is denied.')

Enabling privilege SeBackupPrivilege
Successfully opened C:\tmp\test\file-10

Disabling privilege SeBackupPrivilege
Failed to open C:\tmp\test\file-10: (5, 'CreateFileW', 'Access is denied.')

Enabling privilege SeRestorePrivilege
Successfully opened C:\tmp\test\file-10

Disabling privilege SeRestorePrivilege
Failed to open C:\tmp\test\file-10: (5, 'CreateFileW', 'Access is denied.')

Solution

  • As noted in the question, SeRestorePrivilege with backup semantics grants the following rights: ACCESS_SYSTEM_SECURITY, WRITE_DAC, WRITE_OWNER, DELETE, FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, and FILE_GENERIC_WRITE.

    FILE_GENERIC_WRITE is the access mask that gets mapped to GENERIC_WRITE access for a file object. It includes READ_CONTROL, SYNCHRONIZE, FILE_WRITE_ATTRIBUTES (0x0100), FILE_WRITE_EA (0x0010) , FILE_APPEND_DATA (0x0004), and FILE_WRITE_DATA (0x0002). Note that last two are the same value as FILE_ADD_SUBDIRECTORY (0x0004) and FILE_ADD_FILE (0x0002), so the documentation of SeRestorePrivilege is redundant.

    The READ_CONTROL right in the mask comes from STANDARD_RIGHTS_WRITE. Most generic access masks include one of the following standard-rights masks:

    STANDARD_RIGHTS_READ     = READ_CONTROL
    STANDARD_RIGHTS_WRITE    = READ_CONTROL
    STANDARD_RIGHTS_EXECUTE  = READ_CONTROL
    STANDARD_RIGHTS_REQUIRED = READ_CONTROL | WRITE_DAC | WRITE_OWNER | DELETE
    

    The above all include the READ_CONTROL right, which ensures that requesting generic access includes the right to query the discretionary and mandatory security of an object.


    Note that your experiment also needs to control for implicit owner rights. If a file's owner is the requesting user or one of the user's enabled groups, then the user is implicitly granted READ_CONTROL and WRITE_DAC access. You can control for this either by ensuring the file is owned by a different security principle, such as "SYSTEM", or by adding an ACE for the "OWNER_RIGHTS" security principle that grants no access, which overrides implicit owner rights.

    Note also that CreateFile implicitly requests SYNCHRONIZE and FILE_READ_ATTRIBUTES access, even if dwDesiredAccess is 0. SeRestorePrivilege does not grant FILE_READ_ATTRIBUTES access. In your experiment, this right must have been granted by filesystem policy. Regardless of a file's security descriptor, a user is granted the right to read attributes if the user has FILE_READ_DATA (i.e. FILE_LIST_DIRECTORY) access to the parent directory. This makes the file API consistent because most file metadata (e.g. file attributes, timestamps and size) can be obtained by listing the parent directory. To control your experiment, you need to use a directory that does not grant the user read-data access. Then you will observe that SeRestorePrivilege by itself does not enable CreateFile to succeed.