Powershell in WinPE

The winner of the 2012 scripting games in the advanced category – Rohn Edwards (congrats, man!) – was invited on the scripting podcast and asked Jeffrey Snover if powershell would be in the Windows PreInstallation Environment (called WinPE). Yes, it’s already in WinPE 4.0.
It’s already available and was shipped in the beta version of ADK (Assessment and Deployment Kit), the next version of WAIK.

But let’s have a deeper look into it and check what’s inside the powershell embedded into the next version of WinPE. What are the modules available? how many commands do we have?…

Sometimes a picture is worth a thousand words, enjoy 🙂
powershell in winpe

Changing properties of a local account

It’s super easy to add a new local user account by typing in command prompt

net user test Sup3rP@ssW0rD /add

However, it’s much more complicated to change its properties. Usually, you may want people to avoid changing its password and avoid its password expiring.
To achieve this task on multiple computers, I’ve written the following function based on the idea of the following post. This also allowed me to stop using some old vbscript lines embedded in batch files.

Function Set-LocalUserAttributes            
{            
[CmdletBinding()]            
param(            
            
    [Parameter(Mandatory=$false,ValueFromPipeline=$true)]            
    [System.String[]]$ComputerName = $env:COMPUTERNAME,            
            
    [parameter(Mandatory=$true,Position=0)]            
    [System.String]$UserName = $null,            
            
    [parameter(Mandatory=$true,Position=1)]            
    [ValidateSet('Add','Remove')]            
    [System.String]$Action = $null,            
            
    [parameter(Mandatory=$true,Position=2)]            
    [System.String[]]$Attributes = $null            
)            
            
Begin {}            
Process            
{            
    $UserFlag = $null            
    Write-Verbose -Message "Building the array of flags to be set"            
    $Attributes | ForEach-Object -Process {            
            
        Write-Verbose "Processing flag parameter $_"            
        # ADS_USER_FLAG_ENUM Enumeration http://msdn.microsoft.com/en-us/library/aa772300(VS.85).aspx            
        switch($_)            
        {            
            ADS_UF_SCRIPT                          { $UserFlag += 1 } # 0x1            
            ADS_UF_ACCOUNTDISABLE                  { $UserFlag += 2 } # 0x2            
            ADS_UF_HOMEDIR_REQUIRED                { $UserFlag += 8 } # 0x8            
            ADS_UF_LOCKOUT                         { $UserFlag += 16 } # 0x10            
            ADS_UF_PASSWD_NOTREQD                  { $UserFlag += 32 } # 0x20            
            ADS_UF_PASSWD_CANT_CHANGE              { $UserFlag += 64 } # 0x40            
            ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED { $UserFlag += 128 } # 0x80            
            ADS_UF_TEMP_DUPLICATE_ACCOUNT          { $UserFlag += 256 } # 0x100            
            ADS_UF_NORMAL_ACCOUNT                  { $UserFlag += 512 } # 0x200            
            ADS_UF_INTERDOMAIN_TRUST_ACCOUNT       { $UserFlag += 2048 } # 0x800            
            ADS_UF_WORKSTATION_TRUST_ACCOUNT       { $UserFlag += 4096 } # 0x1000            
            ADS_UF_SERVER_TRUST_ACCOUNT            { $UserFlag += 8192 } # 0x2000            
            ADS_UF_DONT_EXPIRE_PASSWD              { $UserFlag += 65536 } # 0x10000            
            ADS_UF_MNS_LOGON_ACCOUNT               { $UserFlag += 131072 } # 0x20000            
            ADS_UF_SMARTCARD_REQUIRED              { $UserFlag += 262144 } # 0x40000            
            ADS_UF_TRUSTED_FOR_DELEGATION          { $UserFlag += 524288 } # 0x80000            
            ADS_UF_NOT_DELEGATED                   { $UserFlag += 1048576 } # 0x100000            
            ADS_UF_USE_DES_KEY_ONLY                { $UserFlag += 2097152 } # 0x200000            
            ADS_UF_DONT_REQUIRE_PREAUTH            { $UserFlag += 4194304 } # 0x400000            
            ADS_UF_PASSWORD_EXPIRED                { $UserFlag += 8388608 } # 0x800000            
            ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION { $UserFlag += 16777216 } # 0x1000000            
            default {            
                Write-Warning -Message "Unknown flag $_ not being added"            
            }            
        }            
    }            
    Write-Verbose -Message "Final flag being set $UserFlag"            
    $ComputerName | ForEach-Object -Process {            
        $Computer = $_            
        if (Test-Connection -ComputerName $Computer -Count 1 -Quiet)            
        {            
            try            
            {            
                $u = [adsi]"WinNT://$Computer/$UserName,user"            
                Switch ($Action)            
                {            
                    add {            
                        Write-Verbose -Message "Adding flag on user $UserName on computer $Computer"            
                        $u.invokeSet("userFlags", ($u.userFlags[0] -BOR $UserFlag))            
                    }            
                    remove {            
                        if ($u.UserFlags[0] -BAND $UserFlag)            
                        {            
                            Write-Verbose -Message "Removing flag on user $UserName on computer $Computer"            
                            $u.invokeSet("userFlags", ($u.userFlags[0] -BXOR $UserFlag))            
                        } else {            
                            Write-Verbose -Message "Already removed flag on user $UserName on computer $Computer"            
                        }            
                    }            
                }            
                $u.commitChanges()            
            } catch {            
                Write-Warning -Message "Couldn't do action $Action on $UserName on computer $Computer because $($_.Exception.Message)"            
            }            
        } else {            
            Write-Warning -Message "Computer $_ seems unreachable"            
        }            
    }            
}            
End {}            
            
            
} # end of function

Delete ‘default’ value under a registry key

I was working on my main script responsible for reviewing scan results and taking actions based on results. One of the actions consists in iterating through registry values and deleting the unwanted/unknown values that didn’t fall in my whitelist filter.

As long as registry values have a different name than ‘(default)’, there isn’t any problem.
I’ve been using some of the functions provided by Shay Levy in the PSRemoteRegistry module to delete these values.

However, some applications do add the ‘(default)’ value under registry keys upon installation whereas it isn’t required and wasn’t present beforehand. This triggers a false positive in scanning results. Worse, it fails if you do:

# Add the culprit with the built-in windows command            
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /ve            
            
# Check that the unwanted (default) empty value has been added            
Get-Item HKCU:\Software\Microsoft\Windows\CurrentVersion\Run            
            
# Try to remove it            
Remove-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Run -Name '(default)'            
# -> it failed as you can see            
            
# Clearing the content works but that's not what we wanted            
 Clear-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Run -Name '(default)'            
 # See!            
 Get-Item HKCU:\Software\Microsoft\Windows\CurrentVersion\Run            

A related error has already been mentioned and reported on this page.
The comment from Microsoft even says that

Posted by Microsoft on 25/09/2009 at 18:18
Your bug has not been fixed in PowerShell 2.0. However we have kept the bug active for next version

So my workaround on V2 is:

Function Remove-DefaultValue            
{            
    [CmdletBinding()]            
    param(            
      [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]              
      [Alias("CN","__SERVER","IPAddress")]            
      [string[]]$ComputerName="",              
            
      [Parameter(Mandatory=$true,Position=1,ValueFromPipelineByPropertyName=$true)]            
      [string]$Key,            
                        
      [switch]$Ping            
    )            
    Begin {}            
    Process            
    {            
        Foreach ($c in $ComputerName)            
        {            
            if($Ping)            
   {            
                if( !(Test-Connection -ComputerName $c -Count 1 -Quiet))            
    {            
        Write-Warning "$c doesn't respond to ping."            
                    return            
                }            
            }            
            try            
            {            
                Invoke-Command -ComputerName $c -ArgumentList $key -ErrorAction Stop -Script {            
                        param($mainkey)             
                        & (Get-Command reg) @("delete",($mainkey -replace ":",""),"/ve","/f")            
                 }            
             } catch {            
                Write-Warning $_.Exeception.Message            
             }            
        }                     
    }            
    End{}            
} # end of function

Update-Help fails for certain modules

I was trying to update the help of powershell on Windows 8 Beta server and got the following message:

Update-Help -Force -UICulture $host.CurrentUICulture
Update-Help : Failed to update Help for the module(s) 'BestPractices' with UI culture(s) {en-US} : For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.
At line:1 char:1
+ Update-Help -Force -UICulture $host.CurrentUICulture
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Update-Help], Exception
    + FullyQualifiedErrorId : HelpInfoXmlValidationFailure,Microsoft.PowerShell.Commands.UpdateHelpCommand

To avoid this error, I ended doing:

Remove-Module -Name BestPractices -Force            
Update-Help -Force -UICulture $host.CurrentUICulture            

An example of least privilege implementation (part III)

This is the last part of the 3 posts serie where we’ll examine how we can ensure that the user remains in the local administrators group for only 1 hour.

We just need a watchdog who runs at user logoff, runs every 15 minutes and at computer startup. It seems simple but it has to be very robust because of the nature of the tasks to be achieved, i.e. remove users from the local administrators group. Here comes fully powershell into play and like any security measure, there’s a trade-off between risks, costs and performance.

We will define 2 scheduled tasks. The tasks that run at logoff and startup launch the same powershell script and are therefore merged into a single task that has actually multiple triggers.
UAC watchdog tasks

The task at startup and logff just runs the following script with the -Force parameter whereas it runs without parameters every 15 minutes.

#Requires -version 2.0            
[CmdletBinding()]            
param            
(            
    [Parameter(Mandatory=$false, Position=0)]            
    [System.Management.Automation.SwitchParameter]${Force}            
)            
            
# Format date             
$date = (Get-Date).ToString('yyyyMMddHHmmss')            
            
# Get a unique id            
$guid = $Host.InstanceId.Guid.ToString()            
             
# Define a header            
$header = "$date ; $guid"            
            
Function Remove-UsersFromLocalGroup            
{            
    param            
    (            
    [parameter(Mandatory=$true,Position=0)]            
    [system.string]$GroupName,            
            
    [parameter(Mandatory=$true,Position=1)]            
    [system.string[]]$Userids            
    )             
    Begin            
    {            
        $ADSIcomputer = [ADSI]("WinNT://.,computer")             
        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()            
        $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                               
            
        if (-not($IsAdmin))            
        {            
            Write-Warning "Must run powerShell as Administrator to perform these actions"            
            return            
        }             
    }            
    Process            
    {            
        try            
        {            
            $group = $ADSIcomputer.psbase.children.find($GroupName)            
        } catch {            
            Write-Warning -Message "Encountered the following error while searching group $GroupName : $($_.Exception.Message)"            
            return            
        }            
            
        $Userids | ForEach-Object -Process {            
            try            
            {            
                $group.Remove($_)            
            } catch {            
                Write-Warning -Message "Encountered the following error while removing user: $($_.Exception.Message)"            
                return            
            }            
            "$header ; $_ removed from $GroupName on $($env:computername)" | Out-File -filepath "$env:systemroot\UACjob.log" -Append -NoClobber -Encoding ASCII            
        }            
    }            
    End {}            
} # end of function            
            
Function Get-LocalGroupMembers             
{            
    [CmdletBinding()]            
    param(            
    [parameter(Mandatory=$true,ValueFromPipeLine=$true,Position=0)]            
    [ValidateNotNullOrEmpty()]            
    [system.string[]]$GroupName            
    )             
    Begin {            
     $isPartofDomain = (Get-WmiObject Win32_ComputerSystem).PartOfDomain            
     if ($isPartofDomain)            
     {            
      $Domain = $env:userdomain            
     } else {            
      $Domain = (Get-WmiObject Win32_ComputerSystem).Workgroup            
     }            
    }            
    Process {            
        $GroupName | ForEach-Object -Process {            
            try            
            {            
                @(([ADSI]"WinNT://$env:computername/$_").psbase.Invoke("Members")) | ForEach-Object -Process {            
                                
                    switch($_.GetType().InvokeMember("AdsPath","GetProperty",$null,$_,$null).ToString())            
                    {            
                        {$_ -match [regex]"^WinNT://S\-\d{1}\-"} {$_}            
                        {$_ -match [regex]"^WinNT://$Domain/$env:computername/"} {$_ -replace "$Domain/", ""}            
                        default {$_}            
                    } # end of switch            
                }            
            } catch {            
                Write-Warning -Message "Encountered the following error while group members: $($_.Exception.Message)"            
                Return            
            }            
        }            
    }            
    End{}            
} # end of function             
            
# Create a global whitelist            
$whitelist = @()            
$whitelist += "$env:computername/Administrator"            
$whitelist += "$env:userdomain/Domain Admins"            
$whitelist += "S-1-5-21-2224021702-1381044862-585381713-1002"            
            
# Add specific locally defined users             
if (Test-Path $env:systemroot\noUAC.ok)            
{            
    Get-Content $env:systemroot\noUAC.ok -Encoding ASCII | ForEach-Object -Process {            
        # Replace a \ by a / as \ is the escape character and add the user to our whitelist            
        $whitelist += $_ -replace "\\","/"            
    }            
}            
            
# Get the list of local administrators' group members            
$members = Get-LocalGroupMembers -GroupName "Administrators"            
            
if ($Force)            
{            
    # Remove any user that does not appear to be in the whitelist            
    $members | ForEach-Object -Process {            
        if(-not($whitelist | Select-String -Pattern (($_ -replace "WinNT://","")+"$") -Quiet))            
        {            
            Remove-UsersFromLocalGroup -GroupName 'Administrators' -Userids $_            
        }            
    }            
} else {            
    # Import the list of allowed users and get an array of unique usernames filtered by removing whitelisted users            
    if (Test-Path "$env:systemroot\UAC.Allowed.dat")            
    {            
        $allUACAllowedUsers = Import-Csv -Path "$env:systemroot\UAC.Allowed.dat" -Delimiter ";" -Header Ticks,Username            
        $uniqueUsernames = $allUACAllowedUsers | ForEach-Object -Process {            
            if ($_.Username -ne $null)            
            {            
                $_.Username -replace "\\","/"            
            }            
        } | Sort-Object -Unique | Select-String -Pattern $whitelist -NotMatch             
    }            
            
    # Cycle through unique username and get the lastest timestamp            
    foreach ($item in $uniqueUsernames)            
    {            
        $tempitem = @()            
        $tempitem += $allUACAllowedUsers | Where-Object -FilterScript { ($_.Username -replace "\\","/") -eq $item} | Sort-Object -Descending:$true -Property Ticks            
        if ($tempitem.Count -ne $null)            
        {            
            if ($tempitem[0].Username -ne $null)            
            {            
                if ((Get-Date).Ticks -gt $tempitem[0].Ticks)            
                {            
                    Write-Host -ForegroundColor Yellow -Object ("Attempt to remove: " + $tempitem[0].Username)            
                    $id = $tempitem[0].Username -replace "\\","/"            
                    if ($members | Select-String -Pattern "WinNT://$id$" -Quiet)            
                    {            
                        Remove-UsersFromLocalGroup -GroupName 'Administrators' -Userids "WinNT://$id"            
                    } else {            
                        Write-Host -ForegroundColor Yellow -Object ("Has already been removed: " + $tempitem[0].Username)            
                    }            
                }            
            }            
        }            
    }            
}

An example of least privilege implementation (part II)

This is the 2nd post of a 3 posts serie where I’ll now expose the heart of the solution.

To be able to gain administrative privilege from a standard user account, only 2 holes needs to be drilled.

  • Change NTFS permissions to a file so that standard user accounts can write data into.
  • # Create an empty txt file            
    $null | Out-File $env:systemroot\tmpadmin.txt -Force -Encoding ascii            
    # Let authenticated users modify this file            
    & (Get-Command icacls)  @("$env:systemroot\tmpadmin.txt","/grant",'"Authenticated users":M')            
    
  • Give standard user accounts the permission to execute a task running as a local system account.
  • # Grant read and execute to the Authenticated users group            
    & (Get-Command icacls)  @("$env:systemroot\system32\Tasks\Elevate","/grant",'"Authenticated users":RX')            
    

    Now, let’s examine more deeply what the Elevate tasks does and how it’s created:

    • It’s created from an XML template so that we make sure it’ll run even when the laptop is on batteries.
    • The task runs under the system account
    $elevateTNxmltemplate = @"
    <?xml version="1.0" encoding="UTF-16"?>
    <Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
      <RegistrationInfo>
        <Date>2011-05-10T19:33:03</Date>
        <Author>SYSTEM</Author>
      </RegistrationInfo>
      <Triggers>
        <TimeTrigger>
          <StartBoundary>2011-05-10T00:00:00</StartBoundary>
          <Enabled>true</Enabled>
        </TimeTrigger>
      </Triggers>
      <Principals>
        <Principal id="Author">
          <UserId>S-1-5-18</UserId>
          <RunLevel>HighestAvailable</RunLevel>
        </Principal>
      </Principals>
      <Settings>
        <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
        <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
        <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
        <AllowHardTerminate>true</AllowHardTerminate>
        <StartWhenAvailable>false</StartWhenAvailable>
        <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
        <IdleSettings>
          <StopOnIdleEnd>true</StopOnIdleEnd>
          <RestartOnIdle>false</RestartOnIdle>
        </IdleSettings>
        <AllowStartOnDemand>true</AllowStartOnDemand>
        <Enabled>true</Enabled>
        <Hidden>false</Hidden>
        <RunOnlyIfIdle>false</RunOnlyIfIdle>
        <WakeToRun>false</WakeToRun>
        <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
        <Priority>7</Priority>
      </Settings>
      <Actions Context="Author">
        <Exec>
          <Command>c:\Windows\tmpadmin.bat</Command>
        </Exec>
      </Actions>
    </Task>
    "@
    # Create the task
    Write-Output -InputObject $elevateTNxmltemplate | Out-File -FilePath ("$env:temp\tmp.xml") -Encoding Unicode -Force 
    Invoke-Expression -Command ("schtasks.exe /create /RU SYSTEM /TN Elevate /F /XML $env:temp\tmp.xml")
    
    • The tasks actually just launches C:\windows\tmpadmin.bat
    • C:\windows\tmpadmin.bat just takes what it’s written into tmpadmin.txt and add it to the local administrators group
    @echo off
    for /f %%i in (%systemroot%\tmpadmin.txt) do @ net localgroup administrators %%i /add
    
  • Now, the 3rd thing required is a script that the user launches:
  • :: <#;
    @echo off
    
    echo %userdomain%\%username% > %systemroot%\tmpadmin.txt
    
    schtasks /run /tn Elevate
    
    "%systemroot%\system32\WindowsPowershell\v1.0\powerShell.exe" -WindowStyle Hidden -ExecutionPolicy Unrestricted -NoProfile -Command "$ErrorActionPreference = 'Continue';([scriptblock]::Create(((gc '%0' -ReadCount 0 -Encoding ASCII) -replace ';$|^\:\:',\"`n\"))).Invoke()"
    
    goto eof
    :: #>
    
    $ErrorActionPreference = 'Continue';
    
    ((Get-Date).AddHours(+1).Ticks).ToString() + ";" + "$env:userdomain\$env:username"| Out-File -FilePath "$env:systemroot\UAC.Allowed.dat" -Encoding ASCII -Force -NoClobber -Append;
    $timeout = 20;
    # Get the display language;
    switch($host.CurrentUICulture.LCID)
    {
        1036 {
            $titlemsg = "Message d'information:";
            $txtboxlabel = "Votre compte a acquis les droits d'administration de cet ordinateur pour une heure."
        }
        default {
            $titlemsg = "Information message: ";
            $txtboxlabel = "Your account has been granted administrative privileges on this computer for an hour."
        }
    }
    
    # Display the popup to the user  ;
    $obj = new-object -comobject wscript.shell;
    $obj.popup($txtboxlabel,$timeout,$titlemsg,0);
    
    <#;
    :eof
    :: ;#>
    

    As you can see, whenever the user launches the above script:

    • his username is written into c:\windows\tmpadmin.txt (1rst hole)
    • the elevate tasks is triggered (2nd hole)
    • the system account executes the c:\windows\tmpadmin.bat that adds his username written in c:\windows\tmpadmin.txt to the local administrators group
    • he obtains a popup window informing him that he can perform administrative tasks with his own credential within the next hour
    • most important, the expiry date (when his temporary admin credential should be removed) is appended to the C:\Windows\UAC.Allowed.dat CSV file
    • Of course, the file C:\Windows\UAC.Allowed.dat should be created the same way the tmpadmin.txt is, like this
    # Create an empty txt file                        
    $null | Out-File $env:systemroot\UAC.Allowed.dat -Force -Encoding ascii                        
    # Let authenticated users modify this file                        
    & (Get-Command icacls)  @("$env:systemroot\UAC.Allowed.dat","/grant",'"Everyone":M')

    This concludes the 2nd part and we’ll see in the 3rd part how to use the content of UAC.allowed.dat and ensure that the user is removed automatically from the local administrators group after 1 hour.

    An example of least privilege implementation (part I)

  • Goal
  • The goal of this serie of 3 posts is to show how powershell can be used to help implement the least privilege model on Windows 7 workstations.

    Let me first introduce what is the ‘least privilege’ model. It’s running under a standard user account and being able to achieve any administrative tasks like adding a (printer,…) driver, disabling the wireless adapter, etc. If we transpose this concept to the Linux world, it’s defining a standard user account as a member of the sudoers group, i.e. allowing them to use their own account credential to achieve highly privileged tasks instead of giving them the password of the root account.

    Of course, the ‘least privilege’ model isn’t a new concept. On a larger perspective, it’s one of the answers to more than 10 years of bad habits in the Windows ecosystem where we all used to run as administrator and have (had actually) of course legitimate reasons to do so. It’s now a fully mature containment measure of the IT strategy that can be implemented. It is also quoted as an example by Scott Charney in his Keynote at RSA Conference 2012 and his new Trustworthy Computing Next white paper.

    If you want more granularity like the sudo features provides in Linux, you can also achieve it under Windows with powershell constrained endpoints of Windows Remoting (WinRM). I diverge and here we will only focus on a scenario that involves the vast majority of users using exclusively mouse clicks…

    As you may already know, all the commands or features in windows 7 have been designed by default so that the standard user session is hermetic in terms of privilege. Here are some of the various ways of sudoing in Windows by either impersonating or calling the runas verb:

    • the built-in windows command c:\windows\system32\runas.exe,
    • Win32_Process WMI class create method,
    • start-process cmdlet in powershell or the .Net System.Diagnostics.Process,
    • the execute method of the shell com object
    • the built-in windows command c:\windows\system32\schtasks.exe

    Let me be clear, if you find a way to gain access to administrators privilege while running as a standard user, it’s a security flaw called an EoP (escalation of privilege) that needs to be addressed as soon as possible by the MSRC before it’s used by malware authors. In other words, session tokens are isolated.

  • Context
  • There are many ways to implement the least security privilege model and it depends on your environment, your needs and the process you’ve in your organisation, your IT strategy,…

    To be more specific, here are the requirements I’ve tried to meet:

    • trust my users, train them (or at least communicate about this security paradigm shift in my environment) and make anybody (especially security stakeholders) accept that I allow my users to mess up only with their own (single) computer. This security risk already existed beforehand and it’s now being mitigated or let’s say better contained.
    • be able to achieve administrators tasks without giving an administator credential
    • be able to achieve administrators tasks by using their own credential
    • have it as a self service (whenever it’s required) even when the user’s laptop is at home totally disconnected from your corporate network
      • it means that we cannot rely on Active Directory
      • this also means that we cannot rely on a ‘helpdesk’ operator who moves the users to another OU or another AD group
    • never run all the user processes under administrative privilege but only a specific task (disabling the wireless card) or a process (installing software).
    • have it run any administrative task during an hour
    • have a global whitelist of permanent administrators
    • have an additional local whitelist of permanent administrators
  • Additional resources