PowerShell 技能连载 - 记录脚本输出

有一系列办法能记录脚本的输出结果,但是一个非常偷懒的办法是使用 Start-Transcript。在 PowerShell 5 中,这个 cmdlet 不仅在 powershell.exe 中支持,而且在所有宿主中都支持。所以您在 PowerShell ISE 或其它编辑器中都可以使用它。另外,transcript 支持嵌套,所以如果您写一个脚本,您可以安全地在起始处加上 Start-Transcript 并且在结尾处加上 Stop-Transcript

Start-Transcript 将所有输出输出写入一个文本文件。如果您没有指定路径,那么该 cmdlet 将会使用默认路径。您只需要确保脚本确实产生了可记录的输出。

要使脚本更详细,请结合这个技巧一起使用:当您将赋值语句放入一对括号 (),该赋值语句也会输出赋值的数据。这个输出结果也会被 transcript 接收到。请试试一下代码:

1
2
3
4
5
6
7
# default assignment, no output
$a = Get-Service
$a.Count

# assignment in parentheses, outputs the assignment
($b = Get-Service)
$b.Count

PowerShell 技能连载 - 在 Linux 的 PowerShell Core 中安装模块

当您想通过 PowerShellGet 库为所有用户安装模块,您需要管理员权限。在 Linux 的 PowerShell Core 中,您可以用 sudo 命令来启用管理员权限,并且运行 PowerShell。只需要把命令写在在大括号中即可。

在 Linux 的 PowerShell Core 上,以下命令将为所有用户从 PowerShell Gallery 中安装 AzureRM.NetCore:

1
sudo powershell -Command {Install-Module -Name AzureRM.Netcore}

PowerShell 技能连载 - PowerShell.exe 的“大括号秘密”

当从 PowerShell 或 PowerShell Core 中调用 powershell.exe 时有一些小秘密:当您执行 powershell.exe 并通过 -Command 传递一些命令时,PowerShell 将运行这个命令并返回纯文本:

1
2
3
$a = powershell -noprofile -Command Get-Service
$a[0].GetType().FullName
System.String

所以当您将代码放在大括号中执行时,PowerShell 会将强类型的结果序列化后返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$a = powershell -noprofile -Command { Get-Service }
$a[0].GetType().FullName
System.Management.Automation.PSObject

$a[0] | Select-Object -Property *

Name : AdobeARMservice
RequiredServices : {}
CanPauseAndContinue : False
CanShutdown : False
CanStop : True
DisplayName : Adobe Acrobat Update Service
DependentServices : {}
MachineName : .
ServiceName : AdobeARMservice
ServicesDependedOn : {}
Status : Running
ServiceType : Win32OwnProcess
StartType : Automatic
Site :
Container :

PowerShell 技能连载 - 解析完全限定名

完全限定名是字符串的格式,而字符串包含丰富的处理数据方法。最强大而又十分简单的是 Split() 方法。

请看使用 Split() 解析完全限定名,获取最后一个元素的名称,是多么简单:

1
2
3
$dn = 'CN=pshero010,CN=Users,DC=powershell,DC=local'
$lastElement = $dn.Split(',')[0].Split('=')[-1]
$lastElement

Split() 总是返回一个字符串数组。通过大括号,您可以操作每一个独立的数组元素。所以这段代码首先用逗号分割,然后取出第一个元素,即“CN=pshero010”。然后,再次使用相同的技术,用“=”分割。在这里,我们关心的是最后一个数组元素。PowerShell 支持负的数组下标,它从数组的尾部开始计算,所以下标为 -1 获取的是最后一个数组元素。任务完成!

PowerShell 技能连载 - 审计登录事件

您是否想知道当您不在的时候是否有人登录过您的 PC?在前一个技能中我们解释了如何从 Windows 安全日志中解析详细的审计信息,假设您拥有管理员权限。

To find out who logged into your PC, try the code below! The function Get-LogonInfo searches for security events with ID 4624. Security information is protected, so you need to be an Administrator to run this code. This is why the code uses a #requires statement that prevents non-Admins from running the code.
要查看谁登录到了您的 PC,请试试以下代码!Get-LogonInfo 函数搜索 ID 为 4624 的安全事件。安全信息是受保护的,所以只有管理员账户才能执行这段代码。这是为什么这段代码使用 #requires 来防止非管理员执行这段代码的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#requires -RunAsAdministrator

function Get-LogonInfo
{
param
(
[Int]$Newest = [Int]::MaxValue,
[DateTime]$Before,
[DateTime]$After,
[string[]]$ComputerName,
$Authentication = '*',
$User = '*',
$Path = '*'
)

$null = $PSBoundParameters.Remove('Authentication')
$null = $PSBoundParameters.Remove('User')
$null = $PSBoundParameters.Remove('Path')
$null = $PSBoundParameters.Remove('Newest')


Get-EventLog -LogName Security -InstanceId 4624 @PSBoundParameters |
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeGenerated
User = $_.ReplacementStrings[5]
Domain = $_.ReplacementStrings[6]
Path = $_.ReplacementStrings[17]
Authentication = $_.ReplacementStrings[10]

}
} |
Where-Object Path -like $Path |
Where-Object User -like $User |
Where-Object Authentication -like $Authentication |
Select-Object -First $Newest
}


$yesterday = (Get-Date).AddDays(-1)
Get-LogonInfo -After $yesterday |
Out-GridView

这个函数也利用了 $PSBoundParameters 哈希表。这个哈希表包含了用户传入的所有参数。只有一部分信息需要传递给 Get-EventLog 命令,所以用于其他参数需要从哈希表中移除。这样,用户可以只传递 BeforeAfterComputerNameGet-EventLog 命令。

接下来,处理事件信息。所有相关的信息都可以在 ReplacementStrings 属性中找到。这个属性是一个数组。正如结果所展示的那样,ID 为 4624 的事件,第六个(下标为 5)元素为用户名,第七个(下标为 6)元素为域名,第十八个(下标为 17)列出执行登录操作的可执行程序路径。

物理上的登陆通常是由 lass ,即本地安全授权执行的。所以要只查看由人类执行的登录操作,请使用以下代码:

1
2
3
$yesterday = (Get-Date).AddDays(-1)
Get-LogonInfo -After $yesterday -Path *\lsass.exe |
Out-GridView

PowerShell 技能连载 - 查找所有 UAC 提权记录

Windows 的“安全”日志包含了丰富的审计信息。默认情况下,它记录了所有的提权请求。当您以管理员身份运行一个应用程序的时候,就会产生一条记录。

要获取您机器上提权的记录列表,请试试以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#requires -RunAsAdministrator

function Get-ElevationInfo
{
param
(
[DateTime]$Before,
[DateTime]$After,
[string[]]$ComputerName,
$User = '*',
$Privileges = '*',
$Newest = [Int]::MaxValue
)

$null = $PSBoundParameters.Remove('Privileges')
$null = $PSBoundParameters.Remove('User')
$null = $PSBoundParameters.Remove('Newest')



Get-EventLog -LogName Security -InstanceId 4672 @PSBoundParameters |
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeGenerated
User = $_.ReplacementStrings[1]
Domain = $_.ReplacementStrings[2]
Privileges = $_.ReplacementStrings[4]
}
} |
Where-Object Path -like $Privileges |
Where-Object User -like $User |
Select-Object -First $Newest
}

Get-ElevationInfo -User pshero* -Newest 2 |
Out-GridView

Get-ElevationInfo 查询 ID 为 4672 的系统日志。安全信息是受保护的,所以只有管理员账户才能执行这段代码。这是为什么这段代码使用 #requires 来防止非管理员执行这段代码的原因。

这个函数也利用了 $PSBoundParameters 哈希表。这个哈希表包含了用户传入的所有参数。只有一部分信息需要传递给 Get-EventLog 命令,所以用于其他参数需要从哈希表中移除。这样,用户可以只传递 BeforeAfterComputerNameGet-EventLog 命令。

接下来,处理事件信息。所有相关的信息都可以在 ReplacementStrings 属性中找到。这个属性是一个数组。正如结果所展示的那样,ID 为 4672 的事件,第二个(下标为 1)元素为用户名,第三个(下标为 2)元素为域名,第五个(下标为 4)列出获取到的安全特权。

PowerShell 技能连载 - 如何正确地封装多个结果

当一个 PowerShell 函数需要返回多用于一种信息时,一定要将它们打包成一个对象。只有通过这种方法,调用者才能够发现和独立存取该信息。以下是一个快速的例子。

这个函数只是输出三段数据。它以一个包含不同对象的数组形式返回:

1
2
3
4
5
6
7
8
9
10
11
function test
{
33.9
"Hallo"
Get-Date
}

$result = test

$result.Count
$result

以下是一个更好的函数,能返回相同的信息,但是这些信息被封装为一个结构化的对象。通过这种方法,用户可以容易地读取函数返回的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test
{
[PSCustomObject]@{
Number = 33.9
Text = "Hallo"
Date = Get-Date
}

}


$result = test
$result.Count

$result

$result.Number

PowerShell 技能连载 - 探索 WMI

如果您知道 WMI 类查询的名字,Get-WmiObjectGet-CimInstance 两个命令都可以提供丰富的信息。

以下是一个名为 Explore-WMI 的快速的 PowerShell 函数,它可以帮您查找有用的 WMI 类名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Explore-WMI
{
# find all WMI classes that start with "Win32_"...
$class = Get-WmiObject -Class Win32_* -List |
# exclude performance counter classes...
Where-Object { $_.Name -notlike 'Win32_Perf*' } |
# exclude classes with less than 6 properties...
Where-Object { $_.Properties.Count -gt 5 } |
# let the user select one of the found classes
Out-GridView -Title 'Select one' -OutputMode Single

# display selected class name
Write-Warning "Klassenname: $($class.Name)"

# query class...
Get-WmiObject -Class $class.Name |
# and show all of its properties
Select-Object -Property *


# output code
$name = $class.name
" Get-WmiObject -Class $name | Select-Object -property *" | clip.exe

Write-Warning 'Code copied to clipboard. Paste code to try'
}

当运行完这段代码后,调用 Explore-WMI 命令,它将会打开一个 grid view 窗口,显示所有以 “Win32_” 开头的 WMI 类,并且不包括性能计数器类,并且暴露至少 6 个属性。您接下来可以选择其中一个。PowerShell 将会显示这个类的实例和它的所有数据,然后将生成这些结果的命令复制到剪贴板。

通过这种方式可以方便有趣地在 WMI 中搜索有用的信息,并且获取得到这些信息的代码。

PowerShell 技能连载 - 注册缺省的 PowerShell 源

如果您使用 PowerShellGet 模块(默认随着 Windows 10 和 Server 2016 分发),您可以方便地下载和安装共享的 PowerShell 脚本和模块:

1
2
3
4
5
6
7
8
PS> Find-Module -Tag Security

Version Name Repository Description
------- ---- ---------- -----------
2.5.0 Carbon PSGallery Carbon is a PowerShell module for automating t...
0.8.1 ACMESharp PSGallery Client library for the ACME protocol, which is...
2.22 DSInternals PSGallery The DSInternals PowerShell Module exposes seve...
1.2.0.0 DSCEA PSGallery DSCEA is a scanning engine for processing Test...

不过有些时候,机器上缺失了缺省的 PSGallery 源,要还原缺省设置,请使用以下代码:

1
PS> Register-PSRepository -Default

PowerShell 技能连载 - 查找安装的软件

大多数已安装的文件将自己注册在 Windows 注册表中的四个位置。以下是一个名为 Get-InstalledSoftware 的快速 PowerShell 函数,它能够查询所有这些键名,然后输出找到的软件的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Get-InstalledSoftware
{
param
(
$DisplayName='*',

$DisplayVersion='*',

$UninstallString='*',

$InstallDate='*'

)

# registry locations where installed software is logged
$pathAllUser = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
$pathCurrentUser = "Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
$pathAllUser32 = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
$pathCurrentUser32 = "Registry::HKEY_CURRENT_USER\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"


# get all values
Get-ItemProperty -Path $pathAllUser, $pathCurrentUser, $pathAllUser32, $pathCurrentUser32 |
# choose the values to use
Select-Object -Property DisplayVersion, DisplayName, UninstallString, InstallDate |
# skip all values w/o displayname
Where-Object DisplayName -ne $null |
# apply user filters submitted via parameter:
Where-Object DisplayName -like $DisplayName |
Where-Object DisplayVersion -like $DisplayVersion |
Where-Object UninstallString -like $UninstallString |
Where-Object InstallDate -like $InstallDate |

# sort by displayname
Sort-Object -Property DisplayName
}

这个函数也演示了如何不使用 PowerShell 驱动器而直接使用原生的注册表路径。方法是在路径前面添加 provider 的名称。在这个例子中是 Registry::

这个函数将所有输出的列(属性)也暴露为参数,所以可以方便地过滤查询结果。以下例子演示了如何查找所有名字包含“Microsoft”的软件:

1
2
3
4
5
6
7
8
9
10
11
PS C:\> Get-InstalledSoftware -DisplayName *Microsoft*

DisplayVersion DisplayName
-------------- -----------
Definition Update for Microsoft Office 2013 (KB3115404) 32-Bit...
15.0.4569.1506 Microsoft Access MUI (English) 2013
15.0.4569.1506 Microsoft Access Setup Metadata MUI (English) 2013
15.0.4569.1506 Microsoft DCF MUI (English) 2013
15.0.4569.1506 Microsoft Excel MUI (English) 2013
15.0.4569.1506 Microsoft Groove MUI (English) 2013
(...)