Posts on this page:
По просьбе читателей, а так же с учётом востребованности (судя по сообщениям форумов и ньюсгрупп) я нашёл время переписать скрипт ShareUtils.ps1 с поддержкой работы с удалёнными машинами и попутно пофиксив недочёты, которые были найдены за время эксплуатации предыдущей версии скрипта. Предыдущая версия опубликована здесь: Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 4)
Технический функционал изменился только возможностью работы с удалёнными компьютерами, но синтаксис был изменён (а так же удалены лишние функции) по аналогии с PrinterUtils и имеет примерно следующий вид:
Наткнулся на него когда готовил ответ для ньюсгрупп. Как известно, владелец объекта может спокойно изменять списки ACL объектов минуя все их ограничения пользуясь неоткланяемым правом владения объектом. Поэтому, если из ACL объекта удалить все ACE и сохранить, то мы из проводника можем в любой момент вызвать вкладку Security объекта и задать требуемые ACE. Однако, это возможно только из графического интерфейса проводника сделать. При использовании скрипта и командлета Set-Acl мы этого сделать не сможем. Продемонстрирую проблему:
[C:\] whoami
contoso\administrator
[C:\] Get-Acl C:\Test | fl
Path : Microsoft.PowerShell.Core\FileSystem::C:\Test
Owner : CONTOSO\administrator
Group : CONTOSO\Domain Users
Access :
Audit :
Sddl : O:LAG:DUD:PAI
[C:\] $acl = Get-Acl C:\Test
[C:\] $accessrule = New-Object System.Security.AccessControl.FileSystemAccessRule("Administrator","FullControl", "Allow"
)
[C:\] $acl.AddAccessRule($accessRule)
[C:\] $acl | Set-Acl C:\Test
Set-Acl : Attempted to perform an unauthorized operation.
At line:1 char:15
+ $acl | Set-Acl <<<< C:\Test
+ CategoryInfo : PermissionDenied: (C:\Test:String) [Set-Acl], UnauthorizedAccessException
+ FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.SetAclCommand
[C:\]
Как видите, текущий пользователь (администратор) является владельцем папки. Но все ACE в ACL пустые (секция Access). Пользуясь правами владельца, всё же, я не могу ничего сделать с этим списком. Как выяснилось в процессе исследования, пользователю-владельцу, который собирается менять ACL необходимо иметь явно назначенное или унаследованное разрешение TakeOwnership. Никаких Read/ChangePermissions и прочих не надо. Теперь я из проводника добавлю себе право TakeOwnership и попробую снова исполнить код:
[C:\] Get-Acl C:\Test | fl
Path : Microsoft.PowerShell.Core\FileSystem::C:\Test
Owner : CONTOSO\administrator
Group : CONTOSO\Domain Users
Access : CONTOSO\administrator Allow TakeOwnership, Synchronize
Audit :
Sddl : O:LAG:DUD:PAI(A;OICI;0x180000;;;LA)
[C:\] $acl = Get-Acl C:\Test
[C:\] $accessrule = New-Object System.Security.AccessControl.FileSystemAccessRule("Administrator","FullControl", "Allow"
)
[C:\] $acl.AddAccessRule($accessRule)
[C:\] $acl | Set-Acl C:\Test
[C:\] Get-Acl C:\Test | fl
Path : Microsoft.PowerShell.Core\FileSystem::C:\Test
Owner : CONTOSO\administrator
Group : CONTOSO\Domain Users
Access : CONTOSO\Administrator Allow TakeOwnership, Synchronize
CONTOSO\Administrator Allow FullControl
Audit :
Sddl : O:LAG:DUD:PAI(A;OICI;0x180000;;;LA)(A;;FA;;;LA)
[C:\]
Как видите, теперь всё получилось. Я не понимаю, зачем мне нужно иметь право TakeOwnership, чтобы изменить ACL, если я являюсь владельцем. В реальной среде это может вызвать определённые трудности, как неадекватное поведение скрипта, который будет сыпать ошибками (в связи с чем появился вопрос на ньюсгруппах).
Зато мною не очень любимый WMI справился с задачей на ура, израсходовав кода при этом во много раз больше, чем с использованием командлетов. И в итоге получилось вот:
$path = "C:\Test" $user = "Administrator" $path = $path.replace("\", "\\") $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance() $ace = ([WMIClass] "Win32_ace").CreateInstance() $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance() $SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier]) [byte[]] $SIDArray = ,0 * $SID.BinaryLength $SID.GetBinaryForm($SIDArray,0) $Trustee.Name = $user $Trustee.SID = $SIDArray $ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"FullControl" $ace.AceFlags = "0x3" $ace.AceType = 0 $ace.Trustee = $trustee # читаем текущий ACL с объекта $oldDACL = (gwmi Win32_LogicalFileSecuritySetting -filter "path='$path'").GetSecurityDescriptor().Descriptor.DACL # добавляем его к пустому объекту DACL $SD.DACL = $oldDACL # и добавляем новый ACE $SD.DACL += @($ace.psobject.baseobject) # устанавливаем флаг SE_DACL_PRESENT $SD.ControlFlags = "0x4" $folder = gwmi Win32_LogicalFileSecuritySetting -filter "path='$path'" $folder.setsecuritydescriptor($SD)
Я больше склонен доверять WMI и констатировать факт, что он работает честно – даёт мне делать с объектом что угодно и как угодно признавая мои права владения.
Я считаю, что Set-Acl был сильно не прав, отказав мне в изменении пустых ACE, поэтому отрепортил это дело на connect:
https://connect.microsoft.com/feedback/ViewFeedback.aspx?FeedbackID=418906&SiteID=99
В предыдущем посте я показал, как можно легко и удобно подписывать скрипты и как усилить безопасность их исполнения, а так же предупредить несанкционированное изменение кода. Но, как я уже отметил, подписывать каждый раз скрипт из консоли не особо удобно. Например, если вы занимаетесь финальной отладкой скрипта в редакторе, как PowerGUI или PowerShell ISE, то в конце отладки вы просто сохраняете скрипт и закрываете редактор. Понятно, что ещё отдельно запускать консоль PowerShell будет неудобно. Поэтому я написал небольшой скрипт с установщиком, который позволит из проводника по правому нажатию на PS1 файл вы сможете подписывать скрипты.
Чтобы поместить свой элемент в контекстное меню при нажатии на PS1 файлы (на других типах файлов его не будет) нам нужно добавить некоторые значения в реестр по пути:
HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell
подключ с названием нашего элемента – Sign It и в нём уже команду, которая будет отрабатываться при нажатии на него. Я решил не изобретать велосипед и воспользовался приёмами, которые использовал ещё в старом блоге: Полезная безделушка Hash SHA1 на PowerShell. Шапка по сути осталась та же, только тело (исполняемый модуль) изменился. И вот, что получилось для PowerShell 1.0 (для V2 опубликую ниже):
##################################################################### # Sign PS1 Script.ps1 # Version 1.5 # # Automatically sign PowerShell script from .PS1 file context menu # Fully compatible with PowerShell 1.0 # # Vadims Podans (c) 2009 # http://www.sysadmins.lv/ ##################################################################### $FilePath = Read-Host "Specify path to hold SignIt.PS1" if (Test-Path $FilePath) { $FilePath = $FilePath + "\" + "SignIt.ps1" $RegPath = "Registry::HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell\Sign It\command" $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -noninteractive -noprofile -command $FilePath '%1'" New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue if (Test-Path $RegPath) { New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue New-Item -ItemType file -Path $FilePath -Force if (Test-Path $FilePath) { $exefile = 'param ($file) $cp = new-object Microsoft.CSharp.CSharpCodeProvider $cpar = New-Object System.CodeDom.Compiler.CompilerParameters $HideWindow = 0x0080 $ShowWindow = 0x0040 $Code = @" using System; using System.Runtime.InteropServices; namespace Win32API { public class Window { [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); } } "@ $cp.CompileAssemblyFromSource($cpar, $code) $PSHandle = (Get-Process –id $pid).MainWindowHandle [Win32API.Window]::SetWindowPos($PSHandle, 0, 0, 0, 0, 0, $HideWindow) function _msgbox_ ($title, $text, $type = "None") { [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms") $msg = [Windows.Forms.MessageBox]::Show($text, $title, [Windows.Forms.MessageBoxButtons]::ok, [Windows.Forms.MessageBoxIcon]::$type) } $cert = @(dir cert:\currentuser\my -codesigning)[0] if (!$cert) { _msgbox_ -title "Error" -text "Here is no valid signing certificate!" -type "Error" exit } $status = Set-AuthenticodeSignature "$file" $cert -TimestampServer "http://timestamp.verisign.com/scripts/timstamp.dll" if ($status.status -eq "Valid") { _msgbox_ -title "Success" -text "Script is now signed! Enjoy!" } else { _msgbox_ -title "Error" -text $status.StatusMessage -type "Error" } exit' Set-Content -Path $FilePath -Value $exefile &$filepath $filepath } else { Write-Warning "Unable to create file in: $FilePath. Path is incorrect or you haven't sufficient permissions" } } else { Write-Warning "Unable to create registry key. May be you haven't sufficient permissions to HKLM hive" } }
Данный скрипт является установщиком. Для реализации всех функций достаточно запустить этот скрипт и по требованию указать путь, где будет находиться рабочий скрипт (содержимое переменной $exefile).
Примечание: такой метод установки справедлив только для V2. При использовании PowerShell 1.0 установщик следует запускать непосредственно из консоли PowerShell.
Заметка: Здесь следует обратить внимание на блок кода, который идёт после PARAM в переменной $exefile. Данный блок кода скрывает саму консоль PowerShell и показывает только графическую часть, которая исполняется из скрипта. Функция скрытия консоли честно взята из блога Васи Гусева: Win32 API из PowerShell 1.0. Данный метод работает как в PowerShell V2 CTP3, так и в PowerShell 1.0.
И вариант скрипта для PowerShell V2:
##################################################################### # Sign PS1 Script.ps1 # Version 1.5 # # Automatically sign PowerShell script from .PS1 file context menu # # Require PowerShell V2, not compatible with PowerShell 1.0 # # Vadims Podans (c) 2009 # http://www.sysadmins.lv/ ##################################################################### #requires -Version 2.0 $FilePath = Read-Host "Specify path to hold SignIt.PS1" if (Test-Path $FilePath) { $FilePath = $FilePath + "\" + "SignIt.ps1" $RegPath = "Registry::HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell\Sign It\command" $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle Hidden -noprofile -command $FilePath '%1'" New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue if (Test-Path $RegPath) { New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue New-Item -ItemType file -Path $FilePath -Force if (Test-Path $FilePath) { $exefile = 'param ($file) function _msgbox_ ($title, $text, $type = "None") { [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms") $msg = [Windows.Forms.MessageBox]::Show($text, $title, [Windows.Forms.MessageBoxButtons]::ok, [Windows.Forms.MessageBoxIcon]::$type) } $cert = @(dir cert:\currentuser\my -codesigning)[0] if (!$cert) { _msgbox_ -title "Error" -text "Here is no valid signing certificate!" -type "Error" exit } $status = Set-AuthenticodeSignature "$file" $cert -TimestampServer "http://timestamp.verisign.com/scripts/timstamp.dll" if ($status.status -eq "Valid") { _msgbox_ -title "Success" -text "Script is now signed! Enjoy!" } else { _msgbox_ -title "Error" -text $status.StatusMessage -type "Error" } exit' Set-Content -Path $FilePath -Value $exefile &$filepath $filepath } else { Write-Warning "Unable to create file in: $FilePath. Path is incorrect or you haven't sufficient permissions" } } else { Write-Warning "Unable to create registry key. May be you haven't sufficient permissions to HKLM hive" } }
Так же, после копирования файла установщик попытается сразу подписать рабочий скрипт, чтобы этого не пришлось делать потом вручную. После этого в контекстном меню PS1 файлов будет элемент Sign It:
И если у пользователя есть сертификат для подписи скриптов, то при нажатии на этот элемент получите сообщение:
В противном случае получите ошибку:
Вот так можно значимо упростить процесс подписывания скриптов не открывая консоль PowerShell. Но если файлов будет много, то ничего не мешает основную часть кода засунуть в цикл Foreach. Например:
$cert = @(dir cert:\currentuser\my -codesigning)[0] dir -Include *.ps1 -Recurse | %{Set-AuthenticodeSignature "$_" $cert}
На этом пока о подписывании скриптов всё, что я хотел рассказать.
Update 30.08.2009: немного переделал скрипты с учётом следующих поправок:
И, собственно, кнопки для скачивания скриптов:
Как известно практически всем пользователям PowerShell, в целях безопасности была введена политика запуска скриптов. Которая имеет 4 режима:
В версии V2 (пока ещё CTP3) скрипты PowerShell приравняли к исполняемым файлам и эти файлы стали неотключаемо мониториться политикой Software Restriction Policies. Я об этом уже писал ранее: PowerShell V2 и Software Restriction Policies. Поначалу меня это сильно напрягало, но после пришёл к мнению, что это правильно. Неправильно только то, что мы этого не видим (ps1 расширение нигде не фигурирует). С этим бороться можно двумя методами – явно указывать пути, откуда разрешён запуск .ps1 файлов или подписать их все.
Если компьютеров в сети больше одного, то самое идеальное для решения задачи будет наличие домена Active Directory и, по возможности, Enterprise Certification Authity (CA). Наличие домена решит массу задач, как распространение политики SRP в пределах домена, распространение сертификата в пределах домена, контроль версии сертификата, которым подписаны скрипты.
Итак, для начала нам нужно получить сертификат, которым будут подписываться скрипты. В целях безопасности следует создать ограниченную учётную запись пользователя из под которой администратор (в большинстве случаев) будет подписывать скрипты. В книге PowerShell In Action для генерации сертификата предлагается использовать makecert.exe, который входит в состав Visual Studio SDK, но как мне кажется более правильным будет использование CA. В Windows Server CA для этих целей есть уже готовый шаблон, который называется Code Signing. Но принципиальной разницы нету, каким инструментом вы будете генерировать сертификат и я опишу процесс получения сертификата с использованием доменного CA.
Если CA у нас уже установлен, то открываем оснастку Certification Authority и переходим в раздел Certificate Templates. Нажимаем правой кнопкой и выбираем Manage. Откроется редактор шаблонов. Если у вас Enterprise или Datacenter редакции Windows Server, то вы можете создать свой настроенный шаблон. Но я не вижу в этом необходимости. На данном этапе нам необходимо разрешить ограниченному пользователю запрашивать сертификаты этого шаблона. Для этого в закладке Security шаблона Code Signing нужно разрешить чтение и запрос сертификата ограниченному пользователю. Когда эта процедура проделана, редактор шаблонов можно закрыть. После чего в оснастке Certification Authority снова нажать правой кнопкой на разделе Certificate Templates –> New –> Certificate Template to Issue и в списке выбрать шаблон Code Signing.
После чего нужно залогиниться этим пользователем, запустить оснастку Certificate Manager (Start –> Run… –> certmgr.msc) и выполнить запрос сертификата. В списке шаблонов должен быть и добавленный нами Code Signing. Когда сертификат будет запрошен не надо закрывать оснастку сертификатов. Далее нам потребуется экспортировать открытую часть сертификата в x509 файл (с расширением .cer). Экспорт открытой части нам потребуется для проверки подписи и организации доверия подписи в пределах домена. Экспортированный сертификат необходимо теперь доставить администратору(-ам), который отвечает за групповую политику.
В групповых политиках (чаще всего в доменной политике) необходимо создать новую политику Software Restriction Policies и в Additiona Rules добавить правило сертификата (Certificate Rule) и указать экспортированный сертификат. Это необходимо затем, что PowerShell для проверки доверия сертификата ищет его в контейнере Trusted Publishers и только Software Restriction Policies позволяет централизовано распространять сертификаты в этот контейнер.
Теперь можно приступать к подписыванию скриптов. Для этих целей используется командлет Set-AuthenticodeSignature и синтаксис его такой:
Set-AuthenticodeSignature $file $cert
Где $file – путь к скрипту и $cert – объект сертификата, который получается следующим образом:
$cert = @(dir cert:\CurrentUser\My -codesigning)[0]
здесь мы явно указываем, что нам нужен сертификат, у которого в EKU (Enchanced Key Usage) указан Code Sgining. В нашем случае он будет всего 1. Но если их окажется несколько, то мы выберем самый первый. Вот как это будет выглядеть на практике:
[Desktop] Set-ExecutionPolicy allsigned
[Desktop] Get-ExecutionPolicy
AllSigned
[Desktop] .\uptime.ps1
File C:\Documents and Settings\Signer\Desktop\uptime.ps1 cannot be loaded. The file C:\Documents and Settings\Signer
\Desktop\uptime.ps1 is not digitally signed. The script will not execute on the system. Please see "get-help
about_signing" for more details..
At line:1 char:13
+ .\uptime.ps1 < +="" categoryinfo="" :="" notspecified:="" (:)="" [],="" pssecurityexception="" +="" fullyqualifiederrorid="" :="">
[Desktop] $cert = @(dir cert:\currentuser\my -codesigning)[0]
[Desktop] $cert
Directory: Microsoft.PowerShell.Security\Certificate::currentuser\my
Thumbprint Subject
---------- -------
42E5B32A19885C6ADCF9683BDA1C871E0FE5E0DB CN=Signer, CN=Users, DC=contoso, DC=com
[Desktop] Set-AuthenticodeSignature uptime.ps1 $cert
Directory: C:\Documents and Settings\Signer\Desktop
SignerCertificate Status Path
----------------- ------ ----
42E5B32A19885C6ADCF9683BDA1C871E0FE5E0DB Valid uptime.ps1
[Desktop] .\uptime.ps1
System Uptime for DC1 is: 1 days 4 hours 19 minutes 20 seconds
[Desktop]
Я наглядно показал, как это работает. Мы сначала перевели политику исполнения скриптов в AllSigned и убедились, что неподписанный скрипт не исполняется. После чего я подписал этот скрипт и попробовал снова. Как видите, скрипт теперь исполнился.
Если не будет выполнено условие распространения сертификата посредством политики SRP в контейнер Trusted Publishers, то вы получите вот такое сообщение:
[Desktop] .\uptime.ps1
Do you want to run software from this untrusted publisher?
File C:\Documents and Settings\Signer\Desktop\uptime.ps1 is
published by CN=Signer, CN=Users, DC=contoso, DC=com and is not trusted
on your system. Only run scripts from trusted publishers.
[V] Never run [D] Do not run [R] Run once [A] Always run [?] Help
(default is "D"):d
File C:\Documents and Settings\Signer\Desktop\uptime.ps1 cannot be loaded because you have elected to no
t run this software now.
At line:1 char:12
+ .\uptime.ps1
[Desktop]
Вот таким образом мы решаем задачу исполнения только проверенного набора скриптов. В этом смысле PowerShell 1.0 менее безопасный и удобный, поскольку мы не можем политикой SRP блокировать исполнение PS1 файлов как класс и имеем только один выход – принудительное подписывание скриптов. В версии V2 политика исполнения скриптов удобно интегрируется с SRP. Удобство интегрирования в том, что SRP помимо распространения сертификата в пределах домена так же на основе этого правила разрешает исполнять эти скрипты в обход общего ограничения на PS1 файлы.
В следующем посте я расскажу, как можно упростить процесс подписывания скриптов. Так что не отключаемся :-)
Этот скрипт написал скорее для себя, но весьма полезный. В Windows XP/Windows Server 2003 аптайм можно было легко посмотреть в свойствах сетевого подключения, которое как правило работает постоянно. Но в Windows Vista/Windows Server 2008 до него добираться далеко. Смотреть в Task Manager не удобно, поскольку он показывает аптайм в часах, а в уме высчитывать дни как-то неудобно. Вот и набросал функцию, которую положил себе в профиль: