PowerShell 技能连载 - PowerShell 7

今天我们不讨论代码,而是讨论 PowerShell 的总体情况。Microsoft 宣布了下一代 PowerShell 叫做 “PowerShell 7”,并且是基于 .NET Core 3.0 的。这是非常重大的变化,因为 .NET Core 3.0 重新引入了 WPF (Windows Presentation Foundation, GUI),至少在 Windows 平台上。基于这个变化,PowerShell 可以重新引入 GUI 相关的 cmdlet 例如 Out-GridView,并且可以期待 PowerShell 7 能够弥补大多数使用 PowerShell 6 的 Windows 管理员所缺少的功能。

虽然在 PowerShell 7 之前,使用目前的版本也是可行的,但我们建议您关注新闻并阅读以下链接:

https://devblogs.microsoft.com/powershell/the-next-release-of-powershell-powershell-7/

PowerShell 技能连载 - 内置的 RSAT 工具

远程服务器管理工具 (RSAT) 过去是一个外部下载,添加了两个重要的 PowerShell 模块:ActiveDirectoryGroupPolicy。不幸的是,主要的 Windows 更新移除了已安装的 RSAT 工具,所以如果您的脚本需要客户端的 Active Dicrectory 命令,那么需要人工确定并且下载合适新版 Windows 10 的 RSAT 包并且手工安装它。

在 Windows 10 Build 1809 和以后的版本中,这要更容易一些。您可以通过 PowerShell 以类似这样的方式控制 RSAT 状态(假设您有管理员特权):

1
2
3
4
5
6
7
8
9
PS> Get-WindowsCapability -Online -Name *RSAT.ActiveDirectory*


Name : Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
State : NotPresent
DisplayName : RSAT: Active Directory Domain Services and Lightweight Directory Services Tools
Description : Active Directory Domain Services (AD DS) and Active Directory Lightweight Directory Services (AD LDS) Tools include snap-ins and command-line tools for remotely managing AD DS and AD LDS on Windows Server.
DownloadSize : 5230337
InstallSize : 17043386

要安装 RSAT,请运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PS> Get-WindowsCapability -Online -Name *RSAT.ActiveDirectory* |
Add-WindowsCapability -Online


Path :
Online : True
RestartNeeded : False




PS> Get-Module -Name ActiveDirectory -ListAvailable


Directory: C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules


ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Manifest 1.0.1.0 ActiveDirectory {Add-...

虽然在某些情况下仍然需要下载 RSAT 包,但您不再需要搜索正确的版本并手动构建。

PowerShell 技能连载 - 通过 Index Search 搜索文件

Windows 索引服务能够对您的用户数据文件进行索引,并且在文件资源管理器中快速搜索。以下是一个基于内容返回文件的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Search-FileContent ([String][Parameter(Mandatory)]$FilterText, $Path = $home )
{
$objConnection = New-Object -COM ADODB.Connection
$objRecordset = New-Object -COM ADODB.Recordset

$objConnection.Open("Provider=Search.CollatorDSO;Extended properties='Application=Windows';")

$objRecordset.Open("SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE Contains('""$FilterText""') AND SCOPE='$Path'", $objConnection)
While (!$objRecordset.EOF )
{
$objRecordset.Fields.Item("System.ItemPathDisplay").Value
$null = $objRecordset.MoveNext()
}
}

要使用它,请指定一个关键字。以下代码返回所有包含该关键字的文件:

1
2
PS> Search-FileContent -FilterText testcase -Path C:\Users\tobwe\Documents\
C:\Users\tobwe\Documents\Development\experiment1.zip

如你快速发现的,Index Search 并不会返回 PowerShell 脚本(*.ps1 文件)。缺省情况下,PowerShell 脚本并没有被索引。如果您希望通过内容搜索到这些文件,请到 Index Service 设置并且包含 PowerShell 脚本。点击这里了解更多:https://devblogs.microsoft.com/scripting/use-windows-search-to-find-your-powershell-scripts/

PowerShell 技能连载 - 对循环启用流操作

PowerShell 包含许多循环构造。这些循环构造不能变成流,所以您无法将结果通过管道传递给其它 cmdlet 并且享受管道的实时性优势。相反,您必须先将所有数据存储在变量中,并且只有当循环结束之后才可以将变量通过管道传递给其它命令。

虽然您可以用 ForEach-Object 来代替传统的 foreachfor 循环,但它会减慢代码的执行速度,并且不是代替 whiledo 循环的方案。

以下是一个对所有循环启用快速流的简单技巧。让我们从这个非常简单的循环开始:

1
2
3
4
5
6
7
8
9
# stupid sample using a do loop
# a more realistic use case could be a database query

do
{
$val = Get-Random -Minimum 0 -Maximum 10
$val

} while ($val -ne 6)

它会一直循环直到达到随机数 6。您会发现无法实时将结果传输到管道,所以你必须使用类似这样的方法来对结果进行排序或者用它们来做其他事情:

1
2
3
4
5
6
7
8
$all = do
{
$val = Get-Random -Minimum 0 -Maximum 10
$val

} while ($val -ne 6)

$all | Out-GridView

通过将循环封装到一个脚本块中,您可以获得实时流:更少的内存消耗,立即得到结果:

1
2
3
4
5
6
& { do
{
$val = Get-Random -Minimum 0 -Maximum 10
$val

} while ($val -ne 6) } | Out-GridView

PowerShell 技能连载 - 开发 PowerShell Core 还是 Windows PowerShell 脚本

您可能知道,有两种类型的 PowerShell:随着 Windows 操作系统分发的 Windows PowerShell 是基于完整的 .NET Framework而 PowerShell 6 以及更高版本是开源、跨平台,并且基于(有限的).NET Core 和 Standard。

如果您开发能够在两类系统上执行的脚本,那非常棒!不过如果如果知道您的代码是基于其中的一个系统,请确保在脚本的顶部添加合适的 #requires 语句。

这段代码只能在 PowerShell 6 及更高版本运行(假设您已事先将它保存为文件):

1
2
#requires -PSEdition Core
"This runs in PowerShell 6 and better only..."

类似地,以下代码只能在 Windows PowerShell 中运行:

1
2
#requires -PSEdition Desktop
"This runs in Windows PowerShell only..."

PowerShell 技能连载 - 控制音量和静音状态

要调节音量以及静音、取消扬声器的静音,PowerShell 可以像这样用 C# 代码来操作 API:

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
Add-Type -TypeDefinition @'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'@

# query audio volume and mute status
[Audio]::Volume
[Audio]::Mute

# unmute
[Audio]::Mute = $false

# set volume to 50% (range 0.0 - 1.0)
[Audio]::Volume = 0.5]

PowerShell 技能连载 - 检查坏(不安全的)密码(第 2 部分)

在前一个技能中我们解释了如何用 Web Service 来安全地检测密码并且查明它们是否已被泄漏。

信息安全有关的代码有时经过压缩后看起来是否“有趣”,所以在第一步分钟我们分享了优美的而且可读的代码。而以下是考虑“信息安全”的变体,它展示 PowerShell 代码可以被压缩到什么程度并且可以自动混淆。这段代码返回一个指定的密码被暴露了多少次(如果未曾发现被暴露过,返回 null)。

1
$p = 'P@ssw0rd'[Net.ServicePointManager]::SecurityProtocol = 'Tls12'$a,$b = (Get-FileHash -A 'SHA1' -I ([IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($p)))).Hash -split '(?<=^.{5})'(((irm "https://api.pwnedpasswords.com/range/$a" -UseB) -split '\r\n' -like "$b*") -split ':')[-1

PowerShell 技能连载 - 检查坏(不安全的)密码(第 1 部分)

复杂的密码不一定安全。例如,”P@ssw0rd” 是一个非常复杂的密码,但是非常不安全。这是为什么安全社区开始建议用更相关的测试取代复杂性标准,并防止使用以前黑客入侵中使用过的密码。这些密码——虽然它们可能很复杂——是字典攻击的一个常规部分并且非常不安全。

如何知道某个密码是否已被泄露?您可以使用类似 haveibeenpwnd.com 的网站或者它们的 API。这是它的工作原理:

  1. 您根据密码创建哈希值,这样就不会泄漏您的密码。
  2. 将哈希的头五个字节发送到 API,这样就不会泄漏哈希值。
  3. 您可以获得所有以这五个字节开头的哈希值。
  4. 检查返回的哈希中是否有您密码的哈希值。

以下是用 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
# enable all SSL protocols
[Net.ServicePointManager]::SecurityProtocol = 'Ssl3,Tls, Tls11, Tls12'

# get password hash
$stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($Password))
$hash = Get-FileHash -InputStream $stream -Algorithm SHA1
$stream.Close()
$stream.Dispose()

# find first five and subsequent hash characters
$prefix, $suffix = $hash.Hash -split '(?<=^.{5})'

# ask for matching passwords with the same first 5 hash digits
$url = "https://api.pwnedpasswords.com/range/$prefix"
$response = Invoke-RestMethod -Uri $url -UseBasicParsing

# find the exact match
$lines = $response -split '\r\n'
$seen = foreach ($line in $lines)
{
if ($line.StartsWith($suffix)) {
[int]($line -split ':')[-1]
break
}
}

"$Password has been seen {0:n0} times." -f $seen

试着改变 $Password 中的密码来测试这段代码。您会很惊讶地发现许多密码已经泄漏:

Sunshine has been seen 13.524 times.

PowerShell 技能连载 - 用聪明的方法指定位标志

在前一个技能中我们学习了如何在 PowerShell 中启用所有的 SSL 安全协议来连接到 Web Service 和 网站:

1
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12

有趣的是,可以用一行更短的代码来代替:

1
[Net.ServicePointManager]::SecurityProtocol = 'Ssl3, Tls, Tls11, Tls12'

以下是它的原因:

由于 SecurityProtocolNet.SecurityProtocolType 类型的,所以当您传入字符串数据,它可以自动转换:

1
2
PS> [Net.ServicePointManager]::SecurityProtocol.GetType().FullName
System.Net.SecurityProtocolType

与其用 SecurityProtocolType 枚举并且用 -bor 操作符来连接,您还可以用比特标志位的名称组成的逗号分隔的字符串。两者是相同的:

1
2
3
4
5
6
7
$a = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
$b = [Net.SecurityProtocolType]'Ssl3, Tls, Tls11, Tls12'



PS> $a -eq $b
True

PowerShell 技能连载 - 在 PowerShell 中使用 SSL/HTTPS

根据您的 PowerShell、.NET Framework 的版本和升级,WEB 连接的缺省安全协议可能仍然是 SSL3。您可以方便地查明它:

1
[Net.ServicePointManager]::SecurityProtocol

如果返回的协议不包含 Tls12,那么您可能无法用 PowerShell 连接到安全的 Web Service 和网站。我们只需要这样操作就可以启用更多的服务:

1
2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
[Net.ServicePointManager]::SecurityProtocol