На каждом компьютере Windows, подключенном к сети, есть встроенная учетная запись администратора. Управление паролем этой учетной записи имеет большое значение, поскольку от этого зависит полный административный доступ к компьютеру. Групповая политика позволяет задавать пароль учетной записи администратора, но объект групповой политики (GPO) не обеспечивает его безопасного хранения. Для защиты пароля применяется запутывание кода, обфускация, а не шифрование. Установка административных паролей с использованием групповой политики идет вразрез с рекомендациями Microsoft, поскольку это считается небезопасным.
Одним из возможных решений проблемы является применение сценария, реализующего сброс пароля администратора на компьютерах. Это относительно просто сделать с помощью сценария на VBScript (см. листинг 1) или Windows PowerShell (см. листинг 2). Однако такой метод имеет два недостатка:
- Пароль сохраняется как открытый текст прямо в сценарии, что еще менее безопасно, чем обфускация, предусматриваемая групповой политикой.
- В сценарии жестко закодировано имя учетной записи, Administrator, поэтому если политика предусматривает переименование учетной записи администратора или если используется неанглоязычная версия Windows, сценарий придется изменить.
Обе проблемы решаются с помощью PowerShell. Сначала рассмотрим первую из них.
Microsoft. NET Framework имеет объект SecureString – безопасное представление строки. Многие команды PowerShell и объекты. NET задействуют объекты SecureString вместо строк открытого текста. Например, параметр -AccountPassword команды New-ADUser использует аргумент SecureString. Объект SecureString легко создать с помощью команды Read-Host с параметром -AsSecureString. Ниже приведен пример команды, маскирующей строку на экране во время ее ввода и сохранения в объекте SecureString:
$ss = Read-Host «Enter string» -AsSecureString
PowerShell не позволяет напрямую восстанавливать защищенную строку, сохраненную в объекте SecureString, то есть извлекать ее в виде открытого текста, однако это может делать функция ConvertTo-String (см. листинг 3). С помощью. NET Framework защищенная строка, сохраненная в объекте SecureString, копируется и преобразуется в базовую строку (BSTR). Затем BSTR копируется и преобразуется в открытый текст, сохраняемый в объекте. NET String.
После этого BSTR стирается из памяти. Это необходимо, так как BSTR относится к данным неуправляемого типа, а это означает, что. NET Framework не удаляет данные из памяти автоматически. В заключение функция выводит объект String, то есть защищенную строку в виде открытого текста.
При создании новых паролей полезно организовать два запроса (ввод и подтверждение) и сравнивать строки для контроля их идентичности. Именно эту задачу решает сценарий New-SecureString.ps1 (см. листинг 3), который запрашивает два объекта SecureString и сравнивает строки с помощью функции ConvertTo-String. Если строки не совпадают, сценарий повторяет запрос. В заключение выводится объект SecureString. На экране 1 показан сценарий в действии.
Экран 1. Ввод и? подтверждение нового пароля |
PowerShell позволяет конвертировать объект SecureString в зашифрованную стандартную строку, которую можно безопасно хранить и повторно использовать. Такую возможность предусматривают команды ConvertFrom-SecureString и ConvertTo-SecureString. ConvertFrom-SecureString преобразует объект SecureString в зашифрованную стандартную строку, а ConvertTo-SecureString – зашифрованную стандартную строку в объект SecureString.
Команда ConvertFrom-SecureString реализует шифрование с помощью API-интерфейса защиты данных Windows (DPAPI) или заданного ключа шифрования (с использованием параметра -Key или -SecureKey). Если параметр -Key или -SecureKey опущен, то используется API-интерфейс защиты данных Windows (DPAPI). На практике это означает, что зашифрованная строка может быть расшифрована только под той учетной записью, под которой она была зашифрована. Если сохранить зашифрованную строку в файл, то попытка расшифровать ее под другой учетной записью закончится неудачей.
На экране 2 показан пример создания, шифрования и расшифровки объекта SecureString. Первая команда создает объект SecureString. Вторая преобразует объект SecureString в зашифрованную стандартную строку и записывает ее в текстовый файл. Третья команда (Get-ContentEncPass.txt) выводит зашифрованное содержимое текстового файла. Четвертая расшифровывает пароль и сохраняет его во второй объект SecureString (переменная $secureString2). Последняя команда показывает, что переменная $secureString2 действительно содержит объект SecureString.
Экран 2. Создание, шифрование и расшифровка объекта SecureString |
На экране 2 пароль шифруется без применения параметров -Key и –SecureKey, поэтому его можно расшифровать только под той же учетной записью. На экране 3 показан результат попытки войти в систему под другой учетной записью и расшифровать зашифрованный пароль. Как видите, расшифровка завершается отказом, и PowerShell выдает ошибку.
Экран 3. Ошибка расшифровки пароля под другой учетной записью |
Таким образом, чтобы решить проблему хранения паролей в виде открытого текста в сценарии, можно создать объект SecureString, содержащий пароль, зашифровать его с помощью команды ConvertFrom-SecureString (как зашифрованную стандартную строку) и сохранить в текстовый файл. При необходимости зашифрованный пароль можно расшифровать и преобразовать обратно в объект SecureString с помощью команды ConvertTo-SecureString.
Теперь переходим ко второй проблеме. Как отмечалось выше, все компьютеры Windows имеют встроенную учетную запись Administrator, но эта учетная запись не всегда именуется так. Неанглоязычные версии Windows для данной учетной записи используют другое имя. Кроме того, учетная запись может быть переименована вручную или с помощью политики. Поскольку на разных системах имя может различаться, лучше использовать сценарий, который программным способом выясняет правильное имя учетной записи.
Каждая учетная запись имеет идентификатор SID, остающийся неизменным на протяжении всей ее жизни. Пример SID: — S-1-5-21-4535438673-234387905-476317865-1004. Часть SID за последним дефисом (-) называется относительным идентификатором (RID). В нашем примере учетная запись имеет RID -1004. Часть SID до RID определяется доменом или компьютером, на котором находится учетная запись.
Проблема в том, что локальная учетная запись не имеет свойства, заключающего в себе копию строки SID. Вместо этого у локальной учетной записи есть свойство objectSid – массив байтов, а не строка. Чтобы преобразовать значение objectSid в строку, можно создать объект SecurityIdentifier с использованием свойства objectSid в качестве входных данных. Этот метод реализован в сценарии Get-AdminAccount.ps1 (см. листинг 4). На вход берутся все дочерние объекты с текущего компьютера, а объекты, не являющиеся пользователями, пропускаются. Callout A выделяет создание объекта SecurityIdentifier из свойства objectSid. Если свойство Value объекта SecurityIdentifier (строковое представление идентификатора SID) оканчивается на 500, это означает, что учетная запись администратора найдена. Найдя учетную запись администратора, сценарий выводит ее имя и SID и выходит из цикла foreach по оператору break. Пример выходных данных сценария показан на экране 4.
Экран 4. Найденное имя учетной записи администратора |
Таким образом, проблема, связанная с возможностью другого имени у встроенной учетной записи администратора, решается перебором всех объектов пользователей на компьютере, для каждого из которых создается объект SecurityIdentifier. Встроенная учетная запись администратора – это тот объект SecurityIdentifier, у которого свойство Value оканчивается на 500.
Объединение решений
Сценарий Reset-LocalAdminPassword.ps1 объединяет в себе оба решения (см. листинг 5). Синтаксис Reset-LocalAdminPassword.ps1 следующий:
Reset-LocalAdminPassword.ps1 [-ComputerName] [-Password ] [-Confirm] [-Verbose] [-WhatIf]
Параметр -ComputerName позволяет указать имена одного или нескольких компьютеров и выполнять сброс локального пароля администратора на нескольких компьютерах. Параметр -ComputerName – необязательный. На его вход подаются данные с конвейера. Если этот параметр не задан, то сценарий сбрасывает пароль учетной записи администратора на локальном компьютере.
Параметр -Password – обязательный. Если он не задан, то сценарий запрашивает пароль. Так как аргументом параметра является объект SecureString, для безопасного хранения пароля можно создать текстовый файл, содержащий зашифрованную стандартную строку. Для создания объекта -SecureString рекомендуется использовать сценарий New-SecureString.ps1 (см. листинг 3), заставляющий вводить пароль дважды.
Подобно многим другим командам PowerShell, сценарий поддерживает параметры -Confirm, -Verbose и -WhatIf. Параметр -Confirm обеспечивает запрос подтверждения выполняемого действия. Включение параметра –Verbose позволяет выводить подробную информацию. При использовании параметра -WhatIf сценарий отображает действие, которое будет выполнено, до его выполнения.
Проницательные читатели наверняка обратили внимание на то, что метод SetPassword для объекта пользователя (см. листинг 1 и листинг 2) требует копии пароля в виде открытого текста, поэтому сценарий Reset -LocalAdminPassword.ps1 должен использовать функцию ConvertTo-String (см. листинг 3), чтобы сначала преобразовать объект SecureString в объект String. Это означает, что пока выполняется сценарий, открытая строка пароля временно видна в оперативной памяти. К сожалению, этого нельзя избежать, так как метод SetPassword не может использовать объект SecureString. Однако это все же безопаснее обфускации, которую предусматривает групповая политика, или сохранения пароля в сценарии открытым текстом.
Сценарии в действии
На экране 5 показан пример сеанса PowerShell, в ходе которого выполняются сценарии New-SecureString.ps1 и Reset-LocalAdminPassword.ps1. Вначале сценарий New-SecureString.ps1 создает зашифрованный пароль и сохраняет его в текстовый файл P.txt. Напомню, что расшифровать пароль можно только под той учетной записью, под которой этот файл был создан. Затем создается объект SecureString путем расшифровки пароля, сохраненного в файл P.txt. Наконец, выполняется сценарий Reset-LocalAdminPassword.ps1, реализующий сброс пароля учетной записи администратора на локальном компьютере с использованием объекта SecureString. Естественно, для сброса пароля администратора на локальном компьютере требуются повышенные привилегии. В меню, открываемом щелчком правой кнопки мыши на значке PowerShell, я выбрал «Запуск от имени администратора», что отражено в строке заголовка окна PowerShell.
Экран 5. Сценарии New-SecureString.ps1 и Reset-LocalAdminPassword.ps1 в действии |
Поскольку параметр -ComputerName сценария Reset-LocalAdminPassword.ps1 принимает на вход данные с конвейера, можно использовать такую команду:
Get-Content ComputerList.txt | Reset-LocalAdminPassword.ps1 -Password $secureString
Эта команда реализует сброс пароль локального пароля администратора на всех компьютерах, перечисленных в файле ComputerList.txt.
Управление учетной записью администратора
Вовсе необязательно хранить плохо защищенный пароль учетной записи администратора в объекте групповой политики или открытым текстом в сценарии. Объект SecureString и сценарий Reset-LocalAdminPassword.ps1 позволяют организовать безопасный сброс пароля учетной записи администратора на компьютерах вашей сети.
Листинг 1. Код VBScript для переустановки пароля администратора компьютера
Dim ComputerName, AdminPassword ComputerName = «fabrikam23» AdminPassword = «ThisIsThePassword!» Dim AdminUser Set AdminUser = GetObject(«WinNT://» & ComputerName & «/Administrator,User») AdminUser.SetPassword AdminPassword
Листинг 2. Код PowerShell для переустановки пароля администратора компьютера
$computerName = «fabrikam23» $adminPassword = «ThisIsThePassword!» $adminUser = [ADSI] «WinNT://$computerName/Administrator,User» $adminUser.SetPassword($adminPassword)
Листинг 3. Сценарий New-SecureString.ps1
# Return a SecureString as a String. function ConvertTo-String { param( [System.Security.SecureString] $secureString ) $marshal = [System.Runtime.InteropServices.Marshal] try { $intPtr = $marshal::SecureStringToBSTR($secureString) $string = $marshal::PtrToStringAuto($intPtr) } finally { if ( $intPtr ) { $marshal::ZeroFreeBSTR($intPtr) } } $string } do { $ss1 = read-host «Enter string» -assecurestring $ss2 = read-host «Enter again to confirm» -assecurestring $ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2) if (-not $ok ) { write-host «Strings do not match`r`n» } } until ( $ok ) $ss1
Листинг 4. Сценарий Get-AdminAccount.ps1
$computerName = $env:COMPUTERNAME $computer = [ADSI] «WinNT://$computerName,Computer» foreach ( $childObject in $computer.Children ) { # Skip objects that are not users. if ( $childObject.Class -ne «User» ) { continue } $type = «System.Security.Principal.SecurityIdentifier» # BEGIN CALLOUT A $childObjectSID = new-object $type($childObject.objectSid[0],0) # END CALLOUT A if ( $childObjectSID.Value.EndsWith(«-500») ) { «Local Administrator account name: $($childObject.Name[0])» «Local Administrator account SID: $($childObjectSID.Value)» break } }
Листинг 5. Сценарий Reset-LocalAdminPassword.ps1
# Reset-LocalAdminPassword.ps1 # Written by Bill Stewart (bstewart@iname.com) #requires -version 2 <# . SYNOPSIS Resets the local Administrator password on one or more computers. . DESCRIPTION Resets the local Administrator password on one or more computers. The local Administrator account is determined by its RID (-500), not its name. . PARAMETER ComputerName Specifies one or more computer names. The default is the current computer. . PARAMETER Password Specifes the password to use for the local Administrator account. If you don't specify this parameter, you will be prompted to enter a password. #> [CmdletBinding(SupportsShouldProcess=$TRUE)] param( [parameter(ValueFromPipeline=$TRUE)] $ComputerName=[System.Net.Dns]::GetHostName(), [parameter(Mandatory=$TRUE)] [System.Security.SecureString] $Password ) begin { $ScriptName = $MyInvocation.MyCommand.Name $PipelineInput = (-not $PSBoundParameters.ContainsKey(«ComputerName»)) -and (-not $ComputerName) # Returns a SecureString as a String. function ConvertTo-String { param( [System.Security.SecureString] $secureString ) $marshal = [System.Runtime.InteropServices.Marshal] try { $intPtr = $marshal::SecureStringToBSTR($secureString) $string = $marshal::PtrToStringAuto($intPtr) } finally { if ( $intPtr ) { $marshal::ZeroFreeBSTR($intPtr) } } $string } # Writes a custom error to the error stream. function Write-CustomError { param( [System.Exception] $exception, $targetObject, [String] $errorID, [System.Management.Automation.ErrorCategory] $errorCategory=«NotSpecified» ) $errorRecord = new-object System.Management.Automation.ErrorRecord($exception, $errorID,$errorCategory,$targetObject) $PSCmdlet.WriteError($errorRecord) } # Resets the local Administrator password on the specified computer. function Reset-LocalAdminPassword { param( [String] $computerName, [System.Security.SecureString] $password ) $adsPath = «WinNT://$computerName,Computer» try { if ( -not [ADSI]::Exists($adsPath) ) { $message = «Cannot connect to the computer '$computerName' because it does not exist.» $exception = [Management.Automation.ItemNotFoundException] $message Write-CustomError $exception $computerName $ScriptName ObjectNotFound return } } catch [System.Management.Automation.MethodInvocationException] { $message = «Cannot connect to the computer '$computerName' due to the following error: '$($_.Exception.InnerException.Message)'» $exception = new-object ($_.Exception.GetType().FullName)($message,$_.Exception.InnerException) Write-CustomError $exception $computerName $ScriptName return } $computer = [ADSI] $adsPath $localUser = $NULL $localUserName = «" foreach ( $childObject in $computer.Children ) { if ( $childObject.Class -ne»User«) { continue } $childObjectSID = new-object System.Security.Principal.SecurityIdentifier($childObject.objectSid[0],0) if ( $childObjectSID.Value.EndsWith(»-500«) ) { $localUser = $childObject $localUserName = $childObject.Name[0] break } } if ( -not $PSCmdlet.ShouldProcess(»'$computerName\$localUserName'«,"Reset password») ) { return } try { $localUser.SetPassword((ConvertTo-String $password)) } catch [System.Management.Automation.MethodInvocationException] { $message = «Cannot reset password for '$computerName\$localUserName' due the following error: '$($_.Exception.InnerException.Message)'» $exception = new-object ($_.Exception.GetType().FullName)($message,$_.Exception.InnerException) Write-CustomError $exception «$computerName\$user» $ScriptName } } } process { if ( $PipelineInput ) { Reset-LocalAdminPassword $_ $Password } else { $ComputerName | foreach-object { Reset-LocalAdminPassword $_ $Password } } }