PowerShell 技能连载 - Adding New Incrementing Number Column in a Grid View Window

Maybe you’d like to add a column with incrementing indices to your objects. Try this:

$startcount = 0
Get-Service |
  Select-Object -Property @{N='ID#';E={$script:startcount++;$startcount}}, * |
  Out-GridView

When you run this chunk of code, you get a list of services in a grid view window, and the first column “ID#” is added with incrementing ID numbers.

The technique can be used to add arbitrary columns. Simply use a hash table with key N[ame] for the column name, and key E[xpression] with the script block that generates the column content.

PowerShell 技能连载 - 改进 Group-Object

在每一个技能中我们解释了 Group-Object 能为您做什么,以及它有多么好用。不幸的是,Group-Object 的性能不理想。如果您试图对大量对象分组,该 cmdlet 可能会消耗大量时间。

以下是一行按文件大小对您的用户文件夹中所有文件排序的代码。当您希望检测重复的文件时,这将是一个十分重要的先决条件。由于这行代码将在最终返回结果,所以将会消耗大量的时间,甚至数小时:

1
2
3
4
5
6
$start = Get-Date
$result = Get-ChildItem -Path $home -Recurse -ErrorAction SilentlyContinue -File |
Group-Object -Property Length
$stop = Get-Date

($stop - $start).TotalSeconds

由于这些限制,我们创建了一个基于 PowerShell 的 Group-Object 的实现,并称它为 Group-ObjectFast。它基本上做相同的事请,只是速度更快。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function Group-ObjectFast
{
param
(
[Parameter(Mandatory,Position=0)]
[Object]
$Property,

[Parameter(ParameterSetName='HashTable')]
[Alias('AHT')]
[switch]
$AsHashTable,

[Parameter(ValueFromPipeline)]
[psobject[]]
$InputObject,

[switch]
$NoElement,

[Parameter(ParameterSetName='HashTable')]
[switch]
$AsString,

[switch]
$CaseSensitive
)


begin
{
# if comparison needs to be case-sensitive, use a
# case-sensitive hash table,
if ($CaseSensitive)
{
$hash = [System.Collections.Hashtable]::new()
}
# else, use a default case-insensitive hash table
else
{
$hash = @{}
}
}

process
{
foreach ($element in $InputObject)
{
# take the key from the property that was requested
# via -Property

# if the user submitted a script block, evaluate it
if ($Property -is [ScriptBlock])
{
$key = & $Property
}
else
{
$key = $element.$Property
}
# convert the key into a string if requested
if ($AsString)
{
$key = "$key"
}

# make sure NULL values turn into empty string keys
# because NULL keys are illegal
if ($key -eq $null) { $key = '' }

# if there was already an element with this key previously,
# add this element to the collection
if ($hash.ContainsKey($key))
{
$null = $hash[$key].Add($element)
}
# if this was the first occurrence, add a key to the hash table
# and store the object inside an arraylist so that objects
# with the same key can be added later
else
{
$hash[$key] = [System.Collections.ArrayList]@($element)
}
}
}

end
{
# default output are objects with properties
# Count, Name, Group
if ($AsHashTable -eq $false)
{
foreach ($key in $hash.Keys)
{
$content = [Ordered]@{
Count = $hash[$key].Count
Name = $key
}
# include the group only if it was requested
if ($NoElement -eq $false)
{
$content["Group"] = $hash[$key]
}

# return the custom object
[PSCustomObject]$content
}
}
else
{
# if a hash table was requested, return the hash table as-is
$hash
}
}
}

只需要将上述例子中的 Group-Object 替换为 Group-ObjectFast,就可以体验它的速度:

1
2
3
4
5
6
$start = Get-Date
$result = Get-ChildItem -Path $home -Recurse -ErrorAction SilentlyContinue -File |
Group-ObjectFast -Property Length
$stop = Get-Date

($stop - $start).TotalSeconds

在我们的测试中,Group-ObjectFastGroup-Object 快了大约 10 倍。

PowerShell 技能连载 - 探索 Group-Object

Group-Object 是一个好用的 cmdlet:它可以方便地可视化分组。请查看以下示例:

1
2
3
Get-Process | Group-Object -Property Company
Get-Eventlog -LogName System -EntryType Error | Group-Object -Property Source
Get-ChildItem -Path c:\windows -File | Group-Object -Property Extension

Basically, the cmdlet builds groups based on the content of a given property. You can also omit the group, and just look at the count if all that matters to you are the number distributions:
基本上,该 cmdlet 基于指定的属性内容创建分组。当您只关注数量分布时,也可以忽略该分组,而只查看总数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\> Get-ChildItem -Path c:\windows -File | Group-Object -Property Extension -NoElement | Sort-Object -Property Count -Descending



Count Name
----- ----
10 .exe
10 .log
5 .ini
4 .xml
3 .dll
2 .txt
1 .dat
1 .bin
1 .tmp
1 .prx

如果您对实际对象的分组更感兴趣,可以通过 Group-Object 来返回一个哈希表。通过这种方式,您可以通过他们的键访问每个特定的分组:

1
2
3
4
$hash = Get-ChildItem -Path c:\windows -File |
Group-Object -Property Extension -AsHashTable

$hash.'.exe'

执行的结果将转储 Windows 目录下所有扩展名为 “.exe” 的文件。请注意键(查询的属性)不为字符串的情况。

类似地,如果您使用 PowerShell 远程操作并且扇出到多台计算机来平行地获取信息,当获取到结果时,Group-Object 将会把结果重新分组。这个示例同时从三台机器获取服务信息,而且结果将以随机顺序返回。Group-Object 将会对输入的数据分组,这样您可以操作计算机的结果。和哈希表的操作方法一样,您可以用方括号或点号来存取哈希表的键:

1
2
3
4
$services = Invoke-Command -ScriptBlock { Get-Service } -ComputerName server1, server2, server3 | Group-Object -Property PSComputerName -AsHashTable

$services["server1"]
$services."server2"

以下是一个类似的示例,但有一个 bug。您能指出错误吗?

1
2
3
4
$hash = Get-Service |
Group-Object -Property Status -AsHashTable

$hash.'Running'

当您查看哈希表时,您可能希望获取正在运行的服务:

1
2
3
4
5
6
PS C:\> $hash

Name Value
---- -----
Running {AdobeARMservice, AGMService, AGSService, App...
Stopped {AJRouter, ALG, AppIDSvc, AppMgmt...}

When you look at the hash table, you would expect to get back the running services:
然而,您并不会获得任何结果。那是因为哈希表中的那个键并不是字符串而是一个 “ServiceControllerStatus” 对象:

1
2
3
4
PS C:\> $hash.Keys | Get-Member


TypeName: System.ServiceProcess.ServiceControllerStatus

要确保获得到的是可存取的键,请总是将 -AsHashTable-AsString 合并使用。后者确保把键转换为字符串。现在示例代码可以按预期工作:

1
2
3
4
$hash = Get-Service |
Group-Object -Property Status -AsHashTable -AsString

$hash.'Running'

PowerShell 技能连载 - 自动化操作网站

有些时候,需要自动化操作某些已经人工打开的网站。也许您需要先用 WEB 表单登录到内部的网页。假设网站是通过 Internet Explorer 加载的(不是 Edge 或任何第三方浏览器),您可以使用 COM 接口来访问浏览器的实时内容。

当您访问动态网页时,纯 HTML 元素可能会更有用。一个纯 WebClient(或是 Invoke-WebRequest cmdlet)只会返回静态 HTML,并不是用户在浏览器中看到的内容。当使用一个真实的浏览器显示网页内容时,您的脚本需要访问驱动显示内容的完整 HTML。

要测试这一点,请打开 Internet Explorer 或者 Edge,并浏览到需要的网站。在我们的例子中,我们导航到 www.powershellmagazine.com

1
2
3
4
5
6
7
8
$obj = New-Object -ComObject Shell.Application
$browser = $obj.Windows() |
Where-Object FullName -like '*iexplore.exe' |
# adjust the below to match your URL
Where-Object LocationUrl -like '*powershellmagazine.com*' |
# take the first browser that matches in case there are
# more than one
Select-Object -First 1

$browser 中,您可以访问打开的浏览器中的对象模型。如果 $browser 为空,请确保您调整了 LocationUrl 的过滤条件。不要忘了两端的星号。

如果您希望挖掘网页中的所有图片,以下是获取所有图片列表的方法:

1
$browser.Document.images | Out-GridView

类似地,如果您希望挖掘网页的内容信息,以下代码返回页面的 HTML:

1
PS> $browser.Document.building.innerHTML

您可以使用正则表达式来挖掘内容。不过有一个限制:如果您需要以已登录的 WEB 用户的上下文来进行额外的操作,那么别指望了。例如,如果您需要下载一个需要登录才能获取的文件,那么您需要通过对象模型调用 Internet Explorer 的下载操作。

您可能无法通过 Invoke-WebRequest 或是其它简单的 WEB 客户端来下载文件,因为 PowerShell 运行在它自己的上下文中。而对于网站而言,看到的是一个匿名访问者。

使用 Internet Explorer 对象模型来进行更多高级操作,例如下载文件或视频,并不是完全不可行。但基本上是十分复杂的,您需要向用户界面发送点击和按键动作。

PowerShell 技能连载 - 安装打印机

从 Windows 8 和 Server 2012 R2 起,这些操作系统附带发行了一个名为 PrintManagement 的 PowerShell 模块。该模块中的 cmdlet 可以实现脚本化安装和配置打印机。以下是一段帮助您起步的代码:

1
2
3
4
5
6
7
8
9
10
11
$PrinterName = "MyPrint"
$ShareName = "MyShare"
$DriverName = 'HP Designjet Z Series PS Class Driver'
$portname = "${PrinterName}:"

Add-PrinterDriver -Name $DriverName
Add-PrinterPort -Name $portname
Add-Printer -Name $PrinterName -DriverName $DriverName -PortName $portname -ShareName $ShareName

# requires Admin privileges
# Set-Printer -Name $PrinterName -Shared $true

它从驱动库中安装了一个新的打印机。新的打印机默认没有在网络上共享,因为需要管理员权限。作为管理员,您可以运行 Set-Printer 来启用共享,也可以向 Add-Printer 命令添加 -Shared 开关参数。

要探索 PrintManagement 模块中的其它 cmdlet,请使用这行代码:

1
PS> Get-Command -Module PrintManagement

请注意 Windows 7 中没有包含该模块,而且无法在 Windows 7 中安装,因为 Windows 7 缺少运行该模块中 cmdlet 的某些依赖项。

PowerShell 技能连载 - 通过 CSV 创建对象

有些时候通过简单的基于文本的 CSV 格式来批量创建对象是一种聪明的方法,尤其是原始数据已是基于文本的而且只需要少量重格式化。

以下是一个简单的例子,以这种方式输入信息并创建一个自定义对象的列表:

1
2
3
4
5
6
7
8
9
$text = 'Name,FirstName,Location
Weltner,Tobias,Germany
Nikolic,Aleksandar,Serbia
Snover,Jeffrey,USA
Special,ÄÖÜß,Test'

$objects = $text | ConvertFrom-Csv

$objects | Out-GridView

结果看起来类似这样:

1
2
3
4
5
6
Name    FirstName Location
---- --------- --------
Weltner Tobias Germany
Nikolic Aleksandar Serbia
Snover Jeffrey USA
Special ÄÖÜß Test

PowerShell 技能连载 - 快速查找 Active Directory 组成员

经常地,AD 管理员需要查找某个 AD 组的所有成员,包括嵌套的成员。以下是一个常常出现在示例中的代码片段,用于解决这个问题:

1
2
3
4
5
$groupname = 'External_Consultants'
$group = Get-ADGroup -Identity $groupname
$dn = $group.DistinguishedName
$all = Get-ADUser -filter {memberof -recursivematch $dn}
$all | Out-GridView

(请注意您需要来自 Microsoft 免费的 RSAT 工具来使用这些示例中的 cmdlet。)

当您将 $groupname 中的组名改为您组织中存在的 AD 组名后,该代码不仅返回组中的直接用户,而且包含既在该组又在其它组中的直接用户。

然而,该代码执行起来非常慢。以下是一个更简单的实现,能达到多于五倍的速度:

1
2
3
$groupname = 'External_Consultants'
$all = Get-ADGroupMember -Identity $groupname -Recursive
$all | Out-GridView

它的内部使用合适的 LDAP 过滤器,和以上直接的方法类似:

1
2
3
4
5
6
$groupname = 'External_Consultants'
$group = Get-ADGroup -Identity $groupname
$dn = $group.DistinguishedName
$ldap = "(memberOf:1.2.840.113556.1.4.1941:=$dn)"
$all = Get-ADUser -LDAPFilter $ldap
$all | Out-GridView

PowerShell 技能连载 - 通过 Azure 认知服务使用人工智能

现在的云服务不仅提供虚拟机和存储,而且提供全新且令人兴奋的服务,例如认知服务。如果您需要直接访问这些服务,您需要一个 Azure 订阅密钥(可以在以下网站免费获得)。否则,您也可以使用这里提供的免费的互动 DEMO:
https://azure.microsoft.com/en-us/services/cognitive-services/computer-vision/#analyze

以下是一个发送图片文件到 Azure 图片分析的脚本,您将获得关于照片内容的详细描述,包括面部的坐标、性别,以及估计的年龄:

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
# MAKE SURE YOU SPECIFY YOUR FREE OR PAID AZURE SUBSCRIPTION ID HERE:
$subscription = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"


Add-Type -AssemblyName System.Net.Http
# MAKE SURE YOU SPECIFY A PATH TO A VALID IMAGE ON YOUR COMPUTER
$image = "C:\sadjoey.jpg"
$uriBase = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/analyze";
$requestParameters = "visualFeatures=Categories,Tags,Faces,ImageType,Description,Color,Adult"
$uri = $uriBase + "?" + $requestParameters

# get image file as a byte array
$imageData = Get-Content $image -Encoding Byte

# wrap image into byte array content
$content = [System.Net.Http.ByteArrayContent]::new($imageData)
$content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new("application/octet-stream")

# get a webclient ready
$webclient = [System.Net.Http.HttpClient]::new()
$webclient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key",$subscription)

# post the request to Azure Cognitive Services
$response = $webclient.PostAsync($uri, $content).Result
$result = $response.Content.ReadAsStringAsync().Result

# convert information from JSON into objects
$data = $result | ConvertFrom-Json

# get image detail information
$data.description.captions
$data.Faces | Out-String
$data.description.Tags

结果类似这样:

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
PS C:\> $data.description.captions

text confidence
---- ----------
a man and a woman standing in a room 0,94136500176759652



PS C:\> $data.faces

age gender faceRectangle
--- ------ -------------
23 Female @{top=114; left=229; width=47; height=47}



PS C:\> $data.description.tags
person
indoor
man
holding
woman
standing
window
table
room
front
living
young
video
computer
kitchen
playing
remote
wii
people
white
game

您也可以查看一下通过 Web Service 返回的 JSON 数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$result
{"categories":[{"name":"indoor_","score":0.42578125},{"name":"others_","score":0
.00390625}],"tags":[{"name":"person","confidence":0.999438464641571},{"name":"in
door","confidence":0.99413836002349854},{"name":"wall","confidence":0.9905883073
8067627}],"description":{"tags":["person","indoor","man","holding","woman","stan
ding","window","table","room","front","living","young","video","computer","kitch
en","playing","remote","wii","people","white","game"],"captions":[{"text":"a man
and a woman standing in a room","confidence":0.94136500176759652}]},"faces":[{"
age":23,"gender":"Female","faceRectangle":{"top":114,"left":229,"width":47,"heig
ht":47}}],"adult":{"isAdultContent":false,"adultScore":0.023264557123184204,"isR
acyContent":false,"racyScore":0.042826898396015167},"color":{"dominantColorForeg
round":"Brown","dominantColorBackground":"Black","dominantColors":["Brown","Blac
k","Grey"],"accentColor":"6E432C","isBwImg":false},"imageType":{"clipArtType":0,
"lineDrawingType":0},"requestId":"8aebed85-68eb-4b9f-b6f9-5243cd20e4d7","metadat
a":{"height":306,"width":408,"format":"Jpeg"}}

PowerShell 技能连载 - 将所有脚本备份到 ZIP 中

PowerShell 5 终于支持 ZIP 文件了,所以如果您希望备份所有 PowerShell 脚本到一个 ZIP 文件中,以下是一个单行代码:

1
2
Get-ChildItem -Path $Home -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue |
Compress-Archive -DestinationPath "$home\desktop\backupAllScripts.zip" -CompressionLevel Optimal

请注意在 Windows 10 上,所有文件写入 ZIP 之前都需要通过反病毒引擎。如果您的反病毒引擎检测到一段可疑的代码,可能会产生异常,并且不会生成 ZIP 文件。

PowerShell 技能连载 - 以其他用户身份运行 PowerShell 代码

本地管理员权限十分强大,您需要使用类似 JEA 等技术来尽可能减少本地管理员的数量。为什么?请看以下示例。如果您有某台机器上的本地管理员特权,而且启用了 PowerShell 远程操作,那么您可以发送任意的 PowerShell 代码到那台机器上,并且以登录到那台机器上的用户的上下文执行该代码。

如果一个企业管理员正坐在该机器前,您作为一个本地管理员也可以发送一行 PowerShell 代码,并以企业管理员的身份执行。

在操作之前:请知道自己在做什么。这个示例演示了计划任务和本地管理员权限的技术可能性。这些与 PowerShell 和 PowerShell 远程操作都没有关系。我们只是使用 PowerShell 作为工具。

您可以在没有 PowerShell 和 PowerShell 远程操作的情况下做相同的事情,只是使用纯 cmd 以及 psexec 等工具。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
function Invoke-PowerShellAsInteractiveUser
{
param(
[Parameter(Mandatory)]
[ScriptBlock]
$ScriptCode,

[Parameter(Mandatory)]
[String[]]
$Computername
)

# this runs on the target computer
$code = {

param($ScriptCode)

# turn PowerShell code into base64 stream
$bytes = [System.Text.Encoding]::Unicode.GetBytes($ScriptCode)
$encodedCommand = [Convert]::ToBase64String($bytes)

# find out who is physically logged on
$os = Get-WmiObject -Class Win32_ComputerSystem
$username = $os.UserName

# define a scheduled task in the interactive user context
# with highest privileges
$xml = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo />
<Triggers />
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings />
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-windowstyle minimized -encodedCommand $EncodedCommand</Arguments>
</Exec>
</Actions>
<Principals>
<Principal id="Author">
<UserId>$username</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
</Task>
"@

# define, run, then delete the scheduled job
$jobname = 'remotejob' + (Get-Random)
$xml | Out-File -FilePath "$env:temp\tj1.xml"
$null = schtasks.exe /CREATE /TN $jobname /XML $env:temp\tj1.xml
Start-Sleep -Seconds 1
$null = schtasks.exe /RUN /TN $jobname
$null = schtasks.exe /DELETE /TN $jobname /F
}

# run the code on the target machine, and submit the PowerShell code to execute
Invoke-Command -ScriptBlock $code -ComputerName $computername -ArgumentList $ScriptCode
}

要将恶意代码发送到另一台机器,例如打开一个可见的浏览器页面,或用这段代码通过文字转语音和用户“聊天”:

1
2
3
4
5
6
7
8
9
10
11
$ComputerName = 'ENTER THE COMPUTER NAME'

$pirateCode = {
Start-Process -FilePath www.microsoft.com

$sapi = New-Object -ComObject Sapi.SpVoice
$sapi.Speak("You are hacked...!")
Start-Sleep -Seconds 6
}

Invoke-PowerShellAsInteractiveUser -ScriptCode $pirateCode -Computername $ComputerName

显然,需要调整 $ComputerName 对应到您拥有本地管理员特权,并且启用了 PowerShell 远程操作系统上。并且,这段代码需要用户物理登录。如果没有物理登录的用户,那么将没有可以进入的用户回话,这段代码将会执行失败。