Пару лет назад в журнале была опубликована моя статья «Сброс пароля локального администратора» (см. «Windows IT Pro/RE» № 11 за 2014 год). С тех пор я получил от читателей множество вопросов. Большинство из них касалось сценария Reset-LocalAdminPassword.ps1 и сводилось к следующему:

  1. Как безопасно ввести новый пароль?
  2. Можно ли с помощью сценария сменить пароль для локальной учетной записи, отличной от встроенной учетной записи Administrator?
  3. Можно ли указать альтернативные учетные данные при смене паролей локальных учетных записей?

В данной статье я представлю новый сценарий, Reset-LocalAccountPassword.ps1, в котором эти вопросы учтены, а процедура смены паролей локальных учетных записей на компьютерах упрощена и стала более безопасной. Код сценария приведен в листинге.

Использование Reset-LocalAccountPassword.ps1

Синтаксис сценария следующий:

Reset-LocalAccountPassword
   [[-ComputerName]
   ] -AdminAccount [-Password
   ] [-Credential
    ]

или

Reset-LocalAccountPassword
   [[-ComputerName]
   ] -AccountName
    [-Password
] [-Credential ]
   [-Confirm] [-WhatIf] [-Verbose]

Параметр -ComputerName обозначает одно или несколько имен компьютеров. Подстановочные знаки не разрешены, но можно указать несколько имен компьютеров в командной строке сценария или в конвейерном вводе. Можно опустить имя параметра (-ComputerName), если указать имя компьютера (или список имен компьютеров) в начале командной строки сценария или при использовании конвейерного ввода. Для смены пароля необходимо быть членом локальной группы Administrators на любом компьютере, для которого требуется сменить пароль, а окно PowerShell должно иметь повышенные привилегии, чтобы сменить пароль на данном компьютере.

Параметр -AdminAccount указывает, что сценарий должен найти и сбросить пароль встроенной учетной записи Administrator. Обратите внимание, что этот параметр действует, даже если учетная запись Administrator имеет имя, отличное от Administrator. Сценарий отыскивает эту учетную запись по идентификатору безопасности (SID), а не по имени.

Параметр -AccountName указывает имя конкретной учетной записи. Вы можете указать одно имя учетной записи; подстановочные знаки не разрешены. Нельзя указать как -AdminAccount, так и -AccountName в одной команде, эти два параметры исключают друг друга.

Параметр -Password представляет собой объект SecureString, содержащий новый пароль, который предстоит использовать. Если опустить этот параметр, сценарий дважды запросит новый пароль (начальный ввод и подтверждение). Рекомендуется опустить параметр -Password, если вы не имеете опыта сохранения и восстановления объектов SecureString с использованием зашифрованных обычных строк. Дополнительные сведения по этой теме можно найти в упомянутой выше статье.

Параметр -Credential представляет собой объект PSCredential, содержащий административные учетные данные, который может сбрасывать пароли на любом компьютере. Подробнее этот параметр будет рассмотрен ниже.

Смена пароля влечет за собой серьезные последствия, поэтому сценарий запросит подтверждение перед сбросом пароля. Вы можете отключить подтверждение, указав параметр -Confirm:$false в командной строке сценария.

Использование параметров -Verbose и -WhatIf сходно с применением команд PowerShell. Если вы отключили подтверждения с использованием -Confirm:$false, параметр -Verbose заставляет сценарий вывести информацию о каждой выполненной смене пароля, а параметр -WhatIf предписывает сценарию сообщить, какие пароли будут сброшены, не предпринимая никаких действий.

Запрос пароля

В сценарии Reset-LocalAdminPassword.ps1 из статьи «Сброс пароля локального администратора» не предусмотрено способа дважды запросить новый пароль (сначала ввести его, а затем повторить для подтверждения). Вместо этого я составил отдельный тестовый сценарий для начального ввода пароля и подтверждения. Чтобы упростить использование Reset-LocalAccountPassword.ps1, я встроил в сценарий функцию запроса на подтверждение пароля. Таким образом, если вы опускаете параметр -Password (рекомендовано), сценарий попросит ввести новый пароль дважды.

Кроме того, тестовый сценарий из предыдущей статьи расшифровывал введенные пароли в памяти, чтобы сравнить их. Однако сценарий Reset-LocalAccountPassword.ps1 использует новую функцию, сравнивающую объекты SecureString в памяти без предварительной расшифровки, поэтому его безопасность выше, чем у тестового сценария из первой статьи.

Reset-LocalAccountPassword.ps1 все же должен временно расшифровать объекты SecureString перед внесением изменений. К сожалению, способа обойти это ограничение не существует, но сценарий лишь временно расшифровывает объекты SecureString и не пересылает новый пароль в виде простого текста по сети.

Параметр -Credential

Если опустить параметр -Credential, то при попытке смены пароля в Reset-LocalAdminPassword.ps1 используются данные текущей учетной записи. Другими словами, с текущими учетными данными должны быть связаны достаточные разрешения для сброса паролей локальной учетной записи. Если разрешения текущей локальной учетной записи недостаточны, можно воспользоваться командой Get-Credential, чтобы создать объект PSCredential для назначения учетных данных (пример будет показан в следующем разделе).

Важно отметить три особенности параметра -Credential:

  1. Как и в случае с параметром -Password, сценарий может временно расшифровать пароль объекта PSCredential в локальной памяти, но он не пересылает пароль в виде простого текста через сеть.
  2. Параметр -Credential бесполезен на текущем компьютере, так как остается необходимость в повышении полномочий. То есть вам по-прежнему нужно щелкнуть правой кнопкой мыши значок PowerShell и выбрать команду Run as administrator («Запуск от имени администратора»), чтобы сбросить пароль на текущем компьютере. Конечно, если ваши полномочия уже повышены, вам не нужны альтернативные учетные данные.
  3. Возможно, для смены паролей потребуется присвоить параметру реестра LocalAccountTokenFilterPolicy значение 1 на неприсоединенных к домену удаленных компьютерах с операционной системой Windows Vista и более поздних версий. Дополнительные сведения можно найти в статье базы знаний Microsoft 951016 (https://support.microsoft.com/en-us/help/951016/).

Практические примеры

Рассмотрим некоторые практические примеры использования сценария.

1. Сброс встроенной учетной записи Administrator на локальном компьютере:

Reset-LocalAccountPassword -AdminAccount

На экране 1 показана эта команда в действии.

 

Выполнение Reset-LocalAccountPassword.ps1
Экран 1. Выполнение Reset-LocalAccountPassword.ps1

Обратите внимание, что сценарий запрашивает новый пароль и подтверждает пароль перед продолжением работы (в данном примере пароли поначалу не совпадают). Кроме того, обратите внимание, что выполнение команды завершается с ошибкой Access denied («Отказано в доступе»). Как отмечалось выше, необходимо запустить окно PowerShell с повышенными правами (то есть щелкнуть правой кнопкой мыши значок PowerShell и выбрать команду Run as administrator), чтобы выполнить сценарий на локальном компьютере.

На экране 2 показана команда без подтверждения (-Confirm:$false) и с подробным выводом (-Verbose). Выполнение команды на экране 2 завершается успешно, так как окно PowerShell располагает повышенными правами (обратите внимание на его заголовок).

 

Выполнение Reset-LocalAdminPassword.ps1
Экран 2.  Выполнение Reset-LocalAdminPassword.ps1

2. Сброс встроенного пароля Admini­strator на удаленном компьютере:

Reset-LocalAccountPassword SALES1-
   AdminAccount

Эта команда сбрасывает пароль для встроенной учетной записи Administrator на компьютере с именем SALES1. Ваша текущая учетная запись, использованная при регистрации в системе, должна иметь разрешение для сброса пароля на удаленном компьютере. Сценарий сначала запрашивает новый пароль, а затем подтверждение перед изменением пароля.

3. Сброс пароля локальной учетной записи на нескольких компьютерах:

Get-Content Comp.txt |
   Reset-LocalAccountPassword
   -AccountName Supervisor -Confirm:$false

Эта команда сбрасывает пароль учетной записи Supervisor на всех компьютерах, перечисленных в текстовом файле Comp.txt (по одному имени компьютера на строке) без подтверждения изменения каждого пароля (-Confirm:$false).

4. Сброс встроенной учетной записи Administrator на удаленном компьютере с использованием альтернативных учетных данных:

Reset-LocalAccountPassword SALES2
   -AdminAccount -Credential (Get-Credential)

Эта команда сбрасывает пароль для встроенной учетной записи Administrator на компьютере SALES2. Она запрашивает учетные данные, имеющие разрешение выполнить сброс пароля с использованием команды Get-Credential. Скобки вокруг Get-Credential указывают PowerShell, что команду следует воспринимать как выражение. Без скобок PowerShell решит, что параметру -Credential передается строка.

Администрирование паролей для локальных учетных записей

В отличие от сценария Reset-LocalAdminPassword.ps1 в статье «Сброс пароля локального администратора», Reset-LocalAccountPassword.ps1 безопасным образом запрашивает подтверждение пароля, может изменять пароли для локальной учетной записи, отличной от встроенной учетной записи Administrator, и поддерживает альтернативные учетные данные. Дополните имеющийся набор инструментов сценарием Reset-LocalAccountPassword.ps1 и управляйте паролями локальных учетных записей на своих компьютерах.

Листинг. Reset-LocalAccountPassword.ps1
# Reset-LocalAccountPassword.ps1
# Written by Bill Stewart (bstewart@iname.com)
#requires -version 2
<#
.SYNOPSIS
Resets the built-in Administrator account or a named local account
   password on one or more computers.
.DESCRIPTION
Resets the built-in Administrator account or a named local account
   password on one or more computers. The 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. Wildcards are not permitted.
.PARAMETER AccountName
Specifies that you want to reset the password for the specified local
   account.
.PARAMETER AdminAccount
Specifies that you want to reset the password for the built-in
   Administrator account.
.PARAMETER Password
Specifes the new password for the account. If you omit this parameter
   (recommended), you will be prompted to enter the new password twice
   (for confirmation).
.PARAMETER Credential
Specifies credentials that have permission to reset the password
   on the computer(s). If you omit this parameter, the current logon
   account must have permission to reset the password.
.EXAMPLE
PS C:\> Reset-LocalAccountPassword -AdminAccount
This command will reset the built-in Administrator password
   on the current computer.
.EXAMPLE
PS C:\> Reset-LocalAccountPassword COMPUTER1,COMPUTER2
   -AdminAccount -Confirm:$false
This command will reset the built-in Administrator password on two
   different computers without prompting for confirmation.
.EXAMPLE
PS C:\> Reset-LocalAccountPassword -AccountName Manager
This command will reset the password for the local account named
   ‘Manager’ on the current computer.
.EXAMPLE
PS C:\> Reset-LocalAccountPassword PC3,PC4 -AccountName
   Supervisor -Confirm:$false
This command will reset the password of the local account named
   ‘Supervisor’ on two different computers without prompting
   for confirmation.
.EXAMPLE
PS C:\> Get-Content Computers.txt | Reset-LocalAccountPassword
   -AdminAccount -Confirm:$false
This command will reset the built-in Administrator password for the
   computers named in the file Computers.txt without prompting for
   confirmation.
.EXAMPLE
PS C:\> "WG1","WG2" | Reset-LocalAccountPassword -AdminAccount
   -Credential (Get-Credential)
This command will prompt to reset the built-in Administrator
   passwords on two computers. Before the command runs,
   it will prompt for credentials that have permission to reset the
   passwords.
#>
[CmdletBinding(DefaultParameterSetName=""NamedAccount",
   SupportsShouldProcess=$true,ConfirmImpact="High")]
param(
[Parameter(ParameterSetName="AdminAccount",Position=
   0,ValueFromPipeline=$true)]
[Parameter(ParameterSetName="NamedAccount",Position=
   0,ValueFromPipeline=$true)]
[String[]] $ComputerName=[Net.Dns]::GetHostName(),
[Parameter(ParameterSetName="NamedAccount",Mandatory=$true)]
[String] $AccountName,
[Parameter(ParameterSetName="AdminAccount",Mandatory=$true)]
[Switch] $AdminAccount,
[Parameter(ParameterSetName="AdminAccount")]
[Parameter(ParameterSetName="NamedAccount")]
[Security.SecureString] $Password,
[Parameter(ParameterSetName="AdminAccount")]
[Parameter(ParameterSetName="NamedAccount")]
[Management.Automation.PSCredential] $Credential
)
begin {
$ScriptName = $MyInvocation.MyCommand.Name
# Reset password for built-in Administrator account, not a named
   account.
$AdminAccount = $PSCmdlet.ParameterSetName -eq "AdminAccount"
# Safely compares two SecureString objects without decrypting
    them. Returns
# $true if they are equal, or $false otherwise.
function Compare-SecureString {
param(
[Security.SecureString] $secureString1,
[Security.SecureString] $secureString2
)
try {
$bstr1 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR
   ($secureString1)
$bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR
   ($secureString2)
$length1 = [Runtime.InteropServices.Marshal]::ReadInt32($bstr1, -4)
$length2 = [Runtime.InteropServices.Marshal]::ReadInt32($bstr2, -4)
if ( $length1 -ne $length2 ) {
return $false
}
for ( $i = 0; $i -lt $length1; ++$i ) {
$b1 = [Runtime.InteropServices.Marshal]::ReadByte($bstr1, $i)
$b2 = [Runtime.InteropServices.Marshal]::ReadByte($bstr2, $i)
if ( $b1 -ne $b2 ) {
return $false
}
}
return $true
}
finally {
if ( $bstr1 -ne [IntPtr]::Zero ) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr1)
}
if ( $bstr2 -ne [IntPtr]::Zero ) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
}
}
}
# Reads two SecureString objects (passwords), securely compares
   them, and
# returns the confirmed password.
function Read-PasswordWithConfirmation {
if ( $AdminAccount ) {
$name = "built-in Administrator account"
}
else {
$name = "local account ‘$AccountName’"
}
do {
$pass1 = Read-Host -Prompt ("Enter new password for {0}" -f $name)
   -AsSecureString
$pass2 = Read-Host -Prompt ("Confirm new password for {0}" -f $name)
   -AsSecureString
$equal = Compare-SecureString $pass1 $pass2
if ( -not $equal ) {
Write-Host "Passwords do not match"
}
} until ( $equal )
return $pass1
}
# Decrypts and returns a SecureString as a String. Required for the
# DirectoryEntry constructor and SetPassword methods because
   these API calls
# use clear-text passwords. Even though the clear-text passwords will be
# temporarily available in local memory, Windows does not send clear-text
# passwords over the network.
function ConvertTo-String {
param(
[Security.SecureString] $secureString
)
try {
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR
   ($secureString)
return [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
}
finally {
if ( $bstr -ne [IntPtr]::Zero ) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
# Writes a custom error to the error stream.
function Write-CustomError {
param(
[Exception] $exception,
$targetObject,
[String] $errorID,
[Management.Automation.ErrorCategory] $errorCategory="NotSpecified"
)
$errorRecord = New-Object Management.Automation.ErrorRecord
   ($exception,$errorID,$errorCategory,$targetObject)
$PSCmdlet.WriteError($errorRecord)
}
# If the -Password parameter is not present, read it interactively.
if ( -not $Password ) {
$Password = Read-PasswordWithConfirmation
}
# Set up the process action string.
if ( $AdminAccount ) {
$ProcessAction = "Reset password for built-in Administrator account"
}
else {
$ProcessAction = "Reset password for local account ‘$AccountName’"
}
# Called from script’s process block: Resets the requested local account
# password (either built-in Administrator account or a named account).
function Reset-LocalAccountPassword {
[CmdletBinding(ConfirmImpact="High")]
param(
[Parameter(ValueFromPipeline=$true)]
[String[]] $computerName
)
process {
$computerAdsPath = "WinNT://$computerName,Computer"
if ( -not $Credential ) {
$computer = [ADSI] $computerAdsPath
}
else {
$computer = New-Object DirectoryServices.DirectoryEntry
   ($computerAdsPath,$Credential.UserName,(ConvertTo-String
   $Credential.Password))
}
try {
# Piping to Format-List forces PowerShell to connect to the computer
# (we’re only interested in a possible exception).
[Void] ($computer | Format-List)
}

catch {
$message = "Unable to connect to computer ‘$computerName’ due
   to the following error: ‘{0}’" -f ($_.Exception.InnerException.Message
   -replace ‘\n+$’, ‘’)
$exception = New-Object ($_.Exception.GetType().FullName)
   ($message,$_.Exception)
Write-CustomError $exception $computerName $ScriptName
return
}
$localUser = $null
$localUserName = ""
$dirEntries = $computer.Children
if ( $AdminAccount ) {
try {
foreach ( $childObject in $dirEntries ) {
if ( $childObject.Class -eq "User" ) {
$childObjectSID = New-Object Security.Principal.SecurityIdentifier($childObject.objectSid[0],0)
if ( $childObjectSID.Value.EndsWith("-500") ) {
$localUser = $childObject
break
}
}
}
}
catch {
$message = "Unable to connect to computer ‘$computerName’ due
   to the following error: ‘{0}’" -f ($_.Exception.InnerException.Message
   -replace ‘\n+$’, ‘’)
$exception = New-Object ($_.Exception.GetType().FullName)
   ($message,$_.Exception)
Write-CustomError $exception $computerName $ScriptName
return
}
}
else {
try {
$localUser = $dirEntries.Find($AccountName, "User")
}
catch [Management.Automation.MethodInvocationException] {
$message = "Unable to perform operation `"$ProcessAction`" on target
   `"$computerName`" due to the following error: ‘{0}’" -f
   ($_.Exception.InnerException.Message -replace ‘\n+$’, ‘’)
$exception = New-Object ($_.Exception.GetType().FullName)
   ($message,$_.Exception)
Write-CustomError $exception $computerName $ScriptName
return
}
}
$localUserName = $localUser.Name[0]
try {
$localUser.SetPassword((ConvertTo-String $Password))
}
catch [Management.Automation.MethodInvocationException] {
$message = "Unable to perform operation `"$ProcessAction`"
   on target `"$computerName`" due to the following error: ‘{0}’" -f ($_.Exception.InnerException.Message -replace ‘\n+$’, ‘’)
$exception = New-Object ($_.Exception.GetType().FullName)($message,$_.Exception.InnerException)
Write-CustomError $exception $computerName $ScriptName
}
}
}
}
process {
foreach ( $computerNameItem in $ComputerName ) {
if ( $PSCmdlet.ShouldProcess($computerNameItem, $ProcessAction) ) {
Reset-LocalAccountPassword $computerNameItem
}
}
}