PowerShell 技能连载 - 更好的 NetStat(第 3 部分)

在上一个技巧中,我们介绍了 Get-NetTCPConnection cmdlet,它是 Windows 系统上网络实用程序 netstat.exe 的更好替代方法,并且通过一些技巧,您甚至可以解析 IP 地址和进程 ID。但是,这会大大降低命令的速度,因为 DNS 查找可能会花费一些时间,尤其是在网络超时的情况下。

让我们研究一下新的 PowerShell 7 并行处理功能如何加快处理速度。

以下是具有传统顺序处理的原始代码,该代码转储所有连接到端口 443 并解析主机名和进程(缓慢):

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
$Process = @{
Name='Process'
Expression={
# return process path
(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path

}
}

$HostName = @{
Name='Host'
Expression={
$remoteHost = $_.RemoteAddress
try {
# try to resolve IP address
[Net.Dns]::GetHostEntry($remoteHost).HostName
} catch {
# if that fails, return IP anyway
$remoteHost
}
}
}

# get all connections to port 443 (HTTPS)
Get-NetTCPConnection -RemotePort 443 -State Established |
# where there is a remote address
Where-Object RemoteAddress |
# and resolve IP and Process ID
Select-Object -Property $HostName, OwningProcess, $Process

这是一种利用 PowerShell 7 中新的“并行循环”功能的方法(快速):

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
# get all connections to port 443 (HTTPS)
Get-NetTCPConnection -RemotePort 443 -State Established |
# where there is a remote address
Where-Object RemoteAddress |
# start parallel processing here
# create a loop that runs with 80 consecutive threads
ForEach-Object -ThrottleLimit 80 -Parallel {
# $_ now represents one of the results emitted
# by Get-NetTCPConnection
$remoteHost = $_.RemoteAddress
# DNS resolution occurs now in separate threads
# at the same time
$hostname = try {
# try to resolve IP address
[Net.Dns]::GetHostEntry($remoteHost).HostName
} catch {
# if that fails, return IP anyway
$remoteHost
}
# compose the calculated information into one object
[PSCustomObject]@{
HostName = $hostname
OwningProcess = $_.OwningProcess
Process = (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path
}
}

如您所见,第二种方法比以前快得多,并且是 PowerShell 7 中新的“并行循环”的好用例。

但是,不利的一面是,该代码现在的兼容性更低,只能在 Windows 系统上运行,并且只能在 PowerShell 7 中运行。在本系列的最后一部分中,我们将展示一个更简单的解决方案,该解决方案可以在所有版本的 PowerShell 上运行。

PowerShell 技能连载 - 更好的 NetStat(第 2 部分)

在上一个技巧中,我们介绍了 Get-NetTCPConnection cmdlet,它是 Windows 系统上 netstat.exe 网络实用程序的更好替代方案。它可以列出打开的端口和连接,我们以列出所有与 HTTPS(端口443)的连接的示例作为总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> Get-NetTCPConnection -RemotePort 443 -State Established

LocalAddress LocalPort RemoteAddress RemotePort State AppliedSetting OwningProcess
------------ --------- ------------- ---------- ----- -------------- -------------
192.168.2.105 58640 52.114.74.221 443 Established Internet 14204
192.168.2.105 56201 52.114.75.149 443 Established Internet 9432
192.168.2.105 56200 52.114.142.145 443 Established Internet 13736
192.168.2.105 56199 13.107.42.12 443 Established Internet 12752
192.168.2.105 56198 13.107.42.12 443 Established Internet 9432
192.168.2.105 56192 40.101.81.162 443 Established Internet 9432
192.168.2.105 56188 168.62.58.130 443 Established Internet 10276
192.168.2.105 56181 168.62.58.130 443 Established Internet 10276
192.168.2.105 56103 13.107.6.171 443 Established Internet 9432
192.168.2.105 56095 13.107.42.12 443 Established Internet 9432
192.168.2.105 56094 13.107.43.12 443 Established Internet 9432
192.168.2.105 55959 140.82.112.26 443 Established Internet 21588
192.168.2.105 55568 52.113.206.137 443 Established Internet 13736
192.168.2.105 55555 51.103.5.186 443 Established Internet 12752
192.168.2.105 49638 51.103.5.186 443 Established Internet 5464

该列表本身并不是很有用,因为它不能解析 IP 地址,也不会告诉您哪些程序保持着连接。但是,借助一点 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
$Process = @{
Name='Process'
Expression={
# return process path
(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path

}
}

$HostName = @{
Name='Host'
Expression={
$remoteHost = $_.RemoteAddress
try {
# try to resolve IP address
[Net.Dns]::GetHostEntry($remoteHost).HostName
} catch {
# if that fails, return IP anyway
$remoteHost
}
}
}

# get all connections to port 443 (HTTPS)
Get-NetTCPConnection -RemotePort 443 -State Established |
# where there is a remote address
Where-Object RemoteAddress |
# and resolve IP and process ID
Select-Object -Property $HostName, OwningProcess, $Process

Select-Object 选择要显示的对象。它支持“计算属性”。 $Process 定义了一个名为 “Process“ 的计算属性:它采用原始的 OwningProcess 属性,并通过 Get-Process 处理它的进程 ID,以获取应用程序的路径。

$HostName 中也会发生同样的情况:在此,用 .NET 的 GetHostEntry() 方法解析 IP 并返回解析的主机名。现在的结果如下所示:

Host                            OwningProcess Process
----                            ------------- -------
13.107.6.171                             9432 C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE
1drv.ms                                  9432 C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE
lb-140-82-113-26-iad.github.com         21588 C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
1drv.ms                                  9432 C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE
52.113.206.137                          13736 C:\Users\tobia\AppData\Local\Microsoft\Teams\current\Teams.exe
51.103.5.186                            12752 C:\Users\tobia\AppData\Local\Microsoft\OneDrive\OneDrive.exe

不过这样做的成本可能很高,因为解析 IP 地址可能会花费很长时间,尤其是在查询超时时。在下一部分中,我们将介绍并行处理以加快处理速度。

PowerShell 技能连载 - 更好的 NetStat(第 1 部分)

在 Windows 系统上,netstat.exe 是一个有用的实用程序,用于检查打开的端口和侦听器。但是,该工具仅返回文本,具有隐式参数,并且无法跨平台使用。

在 Windows 系统上,可以使用名为 Get-NetTCPConnection 的新 PowerShell cmdlet,该 cmdlet 模仿 netstat.exe 中的许多功能。例如,您可以列出任何软件(浏览器)当前打开的所有 HTTPS 连接(端口443):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> Get-NetTCPConnection -RemotePort 443 -State Established

LocalAddress LocalPort RemoteAddress RemotePort State AppliedSetting OwningProcess
------------ --------- ------------- ---------- ----- -------------- -------------
192.168.2.105 58640 52.114.74.221 443 Established Internet 14204
192.168.2.105 56201 52.114.75.149 443 Established Internet 9432
192.168.2.105 56200 52.114.142.145 443 Established Internet 13736
192.168.2.105 56199 13.107.42.12 443 Established Internet 12752
192.168.2.105 56198 13.107.42.12 443 Established Internet 9432
192.168.2.105 56192 40.101.81.162 443 Established Internet 9432
192.168.2.105 56188 168.62.58.130 443 Established Internet 10276
192.168.2.105 56181 168.62.58.130 443 Established Internet 10276
192.168.2.105 56103 13.107.6.171 443 Established Internet 9432
192.168.2.105 56095 13.107.42.12 443 Established Internet 9432
192.168.2.105 56094 13.107.43.12 443 Established Internet 9432
192.168.2.105 55959 140.82.112.26 443 Established Internet 21588
192.168.2.105 55568 52.113.206.137 443 Established Internet 13736
192.168.2.105 55555 51.103.5.186 443 Established Internet 12752
192.168.2.105 49638 51.103.5.186 443 Established Internet 5464

不幸的是,Get-NetTCPConnection 有严格的限制。例如,它无法解析 IP 地址或进程 ID,因此您无法轻松发现所连接的服务器名称以及维护连接的程序。并且仅在 Windows 系统上可用。

在接下来的部分中,让我们一一消除这些限制。

PowerShell 技能连载 - 获取开机以来经历的时间

PowerShell 7 附带了一个名为 Get-Uptime 的新 cmdlet。它返回一个 timepan 对象,该对象具有自上次重新引导以来已过去的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> Get-Uptime


Days : 9
Hours : 23
Minutes : 21
Seconds : 14
Milliseconds : 0
Ticks : 8616740000000
TotalDays : 9,9730787037037
TotalHours : 239,353888888889
TotalMinutes : 14361,2333333333
TotalSeconds : 861674
TotalMilliseconds : 861674000

提交 -Since 参数时,它将返回上次重新启动的日期。

Get-Uptime 在 Windows PowerShell 中不可用,但是自行创建此命令很简单。运行以下代码在 Windows PowerShell 中创建自己的 Get-Uptime 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Get-Uptime
{
param([Switch]$Since)

$date = (Get-CimInstance -Class Win32_OperatingSystem).LastBootUpTime
if ($Since)
{
return $date
}
else
{
New-Timespan -Start $date
}
}

PowerShell 技能连载 - 查找未使用(或使用过的)驱动器号

通过连接类型转换的结果,您可以轻松创建字母列表:

1
2
3
4
5
6
PS> [Char[]](65..90)
A
B
C
D
...

从此列表中,您可以生成正在使用的驱动器号的列表:

1
2
3
PS> [Char[]](65..90) | Where-Object { Test-Path "${_}:\" }
C
D

同样,要查找空闲的(未使用的)驱动器号,请尝试以下操作:

1
2
3
4
5
6
PS> [Char[]](65..90) | Where-Object { (Test-Path "${_}:\") -eq $false }
A
B
E
F
...

PowerShell 技能连载 - 了解 PowerShell 中的 REST Web 服务

Web 服务类型很多,PowerShell 可以使用 Invoke-RestMethod 访问其中的许多服务。这是一个快速入门,可帮助您起步。

这是最简单的形式,Web 服务就是上面带有结构化数据的网页。您可以在浏览器中使用相同的 URL 来查看数据。PowerShell 使用 Invoke-RestMethod 检索数据。这为您提供了最新的 PowerShell 团队博客:

1
2
Invoke-RestMethod -Uri https://blogs.msdn.microsoft.com/powershell/feed/ |
Select-Object -Property Title, pubDate

像上面的那种简单的 REST Web 服务可以接受参数。这些参数可以作为 URL 的一部分提交。这是一个可以接受任何数字并返回有关该数字的部分意义信息的 Web 服务。下次您被邀请参加 25 周年纪念日时,您可能想查询一些有关数字 25 的有趣信息:

1
2
3
4
5
6
7
8
PS> Invoke-RestMethod http://numbersapi.com/25
25 is the number of cents in a quarter.

PS> Invoke-RestMethod http://numbersapi.com/25
25 is the (critical) number of Florida electoral votes for the 2000 U.S. presidential election.

PS> Invoke-RestMethod http://numbersapi.com/25
25 is the minimum age of candidates for election to the United States House of Representatives.

其他 Web 服务通过不可见的 POST 数据传入用户数据(类似于网站上的表单数据)。它们可能还需要会话状态、cookie 和/或登录。

这是最后一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# https://4bes.nl/2020/08/23/calling-a-rest-api-from-powershell/

$Body = @{
Cook = "Freddy"
Meal = "PastaConCarne"
}

$Parameters = @{
Method = "POST"
Uri = "https://4besday4.azurewebsites.net/api/AddMeal"
Body = ($Body | ConvertTo-Json)
ContentType = "application/json"
}
Invoke-RestMethod @Parameters -SessionVariable cookie
Invoke-RestMethod "https://4besday4.azurewebsites.net/api/AddMeal" -WebSession $cookie

哈希表类似于您要发送到 Web 服务的参数。它们将转换为 JSON 格式。由 Web 服务确定接受用户输入的格式。然后,使用 POST方法将数据传输到 Web 服务。

如果您为向指定名字的厨师发送一个请求,则会从 Web 服务中获取一条通知,告知您正在准备食品。确保更改 Cook 和 Meal 的数据。

如您所见,Invoke-RestMethod 使用了两次。第一次调用获取会话状态和 cookie,并将其存储在使用 -SessionVariable 参数定义的 $cookie 变量中。

第二个调用通过 -WebSession 参数提交会话状态。这样,Web 服务可以保留每次调用之间的状态并清楚地标识您。

PowerShell 技能连载 - 更好的递归

当函数调用自身时,称为“递归”。当脚本想要遍历文件系统的一部分时,您会经常看到这种技术:一个函数处理文件夹内容,当它遇到子文件夹时,它会调用自身。

递归的功能很强大,但是却很难调试,并且有潜在的危险。因为当您犯错时,您将陷入无休止的循环。此外,递归深度过高时,始终存在堆栈溢出的风险。

许多通常需要递归的任务也可以通过使用“队列”来设计:当您的代码遇到新任务时,无需再次调用自身,而是将新任务放在队列中,一旦完成初始任务,开始解决队列中的任务。

感谢 Lee Holmes,这是一个遍历整个驱动器 C:\ 但不使用递归的简单示例。相反,您可以看到正在使用的队列:

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
# get a new queue
[System.Collections.Queue]$queue = [System.Collections.Queue]::new()
# place the initial search path(s) into the queue
$queue.Enqueue('c:\')
# add as many more search paths as you need
# they will eventually all be traversed
#$queue.Enqueue('D:\')

# while there are still elements in the queue...
while ($queue.Count -gt 0)
{
# get one item off the queue
$currentDirectory = $queue.Dequeue()
try
{
# find all subfolders and add them to the queue
# a classic recurse approach would have called itself right here
# this approach instead pushes the future tasks just onto
# the queue for later use
[IO.Directory]::GetDirectories($currentDirectory) | ForEach-Object {
$queue.Enqueue($_)
}
}
catch {}

try
{
# find all files in this folder with the given extensions
[IO.Directory]::GetFiles($currentDirectory, '*.psm1')
[IO.Directory]::GetFiles($currentDirectory, '*.ps1')
}
catch{}
}

以下是 UMU 提供的,不用递归实现本任务的方法:

1
[IO.Directory]::EnumerateFiles('path', '*', 1)

PowerShell 技能连载 - 管理本地组成员(第 2 部分)

在上一个技巧中,我们解释了为什么访问本地组成员并不总是与内置的 cmdlet(如 Get-LocalGroupMember)一起使用,以及使用旧的(但仍可正常使用)ADSI 接口解决该问题的方法。

如果您想以此为基础构建解决方案,您可能想知道如何将本地帐户添加到组或从组中删除,以及如何启用和禁用本地管理员帐户。

这是说明这些方法的几行有用的代码。您可以独立使用这些代码,也可以将它们集成到自己的脚本逻辑中。

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
# these examples use the data below - adjust to your needs
# DO NOT RUN THESE LINES UNLESS YOU CAREFULLY
# REVIEWED AND YOU KNOW WHAT YOU ARE DOING!

# use local machine
$ComputerName = $env:computername
# find name of local Administrators group
$Group = ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
# find name of local Administrator user
$Admin = Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"
$UserName = $Admin.Name
# examples

# find all local groups
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$computerObj.psbase.children |
Where-Object { $_.psbase.schemaClassName -eq 'group' } |
Select-Object -Property @{N='Name';E={$_.Name[0]}},
Path,
@{N='Sid';E={[Security.Principal.SecurityIdentifier]::new($_.objectSid.value,0).Value}}



# find members of local admin group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
$groupObj.psbase.Invoke('Members') |
ForEach-Object { $_.GetType().InvokeMember('ADspath','GetProperty',$null,$_,$null) }

# add user to group/remove from group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
# specify the user or group to add or remove
$groupObj.Add('WinNT://DOMAIN/USER,user')
$groupObj.Remove('WinNT://DOMAIN/USER,user')

# enabling/disabling accounts
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$userObj = $computerObj.psbase.children.find($UserName, 'User')
#enable
$userObj.UserFlags=$userObj.UserFlags.Value -band -bnot 512
$userObj.CommitChanges()

#disable
$userObj.UserFlags=$userObj.UserFlags.Value -bor 512
$userObj.CommitChanges()

PowerShell 技能连载 - 管理本地组成员(第 1 部分)

幸运的是,PowerShell 5 及更高版本附带了诸如 Get-LocalGroupMember 之类的 cmdlet,该 cmdlet 列出了本地组的成员。不幸的是,这些 cmdlet 有一个缺陷:如果组包含一个或多个孤立成员,则该 cmdlet 将无法列出任何组成员。

孤立的组成员可以是已添加到组中但后来在 Active Directory 中删除的用户或组。这样的孤儿显示为 SID 号,而不是对话框中的名称。

若要解决此问题并列出组成员而不考虑孤立帐户,请尝试使用 Get-GroupMemberLocal 函数,该函数使用 PowerShell 自带 cmdlet 之前前通常使用的,旧的 ADSI 方法:

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
function Get-GroupMemberLocal
{
[CmdletBinding(DefaultParameterSetName='Name')]
param
(
[Parameter(Mandatory,Position=0,ParameterSetName='Name')]
[string]
$Name,

[Parameter(Mandatory,Position=0,ParameterSetName='Sid')]
[System.Security.Principal.SecurityIdentifier]
$Sid,

[string]
$Computer = $env:COMPUTERNAME
)

if ($PSCmdlet.ParameterSetName -eq 'Sid')
{
$Name = $sid.Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
}

$ADSIComputer = [ADSI]("WinNT://$Computer,computer")
$group = $ADSIComputer.psbase.children.find($Name, 'Group')
$group.psbase.invoke("members") |
ForEach-Object {
try
{
$disabled = '-'
$disabled = $_.GetType().InvokeMember("AccountDisabled", 'GetProperty', $null, $_, $null)
} catch {}
[PSCustomObject]@{
Name = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
SID = [Security.Principal.SecurityIdentifier]::new($_.GetType().InvokeMember("objectSid", 'GetProperty', $null, $_, $null),0)
Path = $_.GetType().InvokeMember("AdsPath", 'GetProperty', $null, $_, $null)
Type = $_.GetType().InvokeMember("Class", 'GetProperty', $null, $_, $null)
Disabled = $disabled
}
}
}

下面是一个实际示例:让我们转储本地管理员组。您可以按名称或不区分文化的 SID 来访问组:

1
2
3
4
5
6
7
8
PS> Get-GroupMemberLocal -Sid S-1-5-32-544 | Format-Table

Name SID Path Type Disabled
---- --- ---- ---- --------
Administrator S-1-5-21-2770831484-2260150476-2133527644-500 WinNT://WORKGROUP/DELL7390/Administrator User True
Presentation S-1-5-21-2770831484-2260150476-2133527644-1007 WinNT://WORKGROUP/DELL7390/Presentation User False
RemoteAdmin S-1-5-21-2770831484-2260150476-2133527644-1013 WinNT://WORKGROUP/DELL7390/RemoteAdmin User False
Management S-1-5-21-2770831484-2260150476-2133527644-1098 WinNT://WORKGROUP/DELL7390/Management Group -

PowerShell 技能连载 - 标识本地管理员帐户的名称

有时,PowerShell 脚本需要访问或使用内置的 Administrator 帐户或内置的 Administrators组。不幸的是,它们的名称已本地化,因此它们的名称可以根据 Windows 操作系统的语言进行更改。

但是,它们确实使用了恒定的(众所周知的)SID(安全标识符)。通过使用 SID,您可以获得名称。对于本地 Administrator组,这很简单,因为 SID 始终是已知的:S-1-5-32-544。使用一行代码,可以翻译 SID。这是从德文系统获得的结果:

1
2
PS> ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value
VORDEFINIERT\Administratoren

使用内置管理员等帐户,就不那么简单了。在此,只有 RID(相对标识符)是已知的:-500。

通过简单的WMI查询,您将获得与过滤器匹配的帐户:

1
2
3
4
5
PS> Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"

Name Caption AccountType SID Domain
---- ------- ----------- --- ------
Administrator DELL7390\Administrator 512 S-1-5-21-2770831484-2260150476-2133527644-500 DELL7390