PowerShell 技能连载 - 将对象转换为哈希表

我们经常需要检视一个对象,例如一个进程或是一个 Active Directory 用户。当您在一个网格视图窗口,例如 Out-GridView 中显示一个对象时,整个对象显示为一个长行。

一个好得多的方法是将一个对象转换为哈希表。通过这种方式,每个属性都在独立的一行中显示,这样您就可以用网格视图窗口顶部的文本过滤器搜索每个属性。另外,您可以完全控制转换的过程,这样您可以对对象的属性排序,并且确保它们按字母顺序排序,甚至排除空属性。

以下是一个将对象转换为哈希表并且可以根据需要排除空属性的函数:

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 Convert-ObjectToHashtable
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
$object,

[Switch]
$ExcludeEmpty
)

process
{
$object.PSObject.Properties |
# sort property names
Sort-Object -Property Name |
# exclude empty properties if requested
Where-Object { $ExcludeEmpty.IsPresent -eq $false -or $_.Value -ne $null } |
ForEach-Object {
$hashtable = [Ordered]@{}} {
$hashtable[$_.Name] = $_.Value
} {
$hashtable
}
}
}

让我们来看看一个对象,例如当前进程,缺省情况下在 Out-GridView 是如何显示的:

1
$process = Get-Process -Id $pid | Out-GridView

和以下这个作对比:

1
$process = Get-Process -Id $pid | Convert-ObjectToHashtable -ExcludeEmpty | Out-GridView

现在分析起来好多了。

PowerShell 技能连载 - 对象的魔法(第 4 部分)

将一个对象转换为一个哈希表如何?通过这种方式,当将对象输出到一个网格视图窗口时,可以每行显示一个对象属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$hash = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name |
Sort-Object |
ForEach-Object { $hash = [Ordered]@{} } { $hash[$_] = $object.$_ } { $hash }

# output regularly
$object | Out-GridView -Title Regular

# output as a hash table, only non-empty properties, sorted
$hash | Out-GridView -Title Hash

PowerShell 技能连载 - 对象的魔法(第 3 部分)

假设您希望隐藏对象所有没有值(为空)的属性。以下是一个简单的实现:

1
2
3
4
5
6
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$propNames = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name
$object | Select-Object -Property $propNames

这段代码将只输出包含值的属性。您甚至可以对属性排序:

1
2
3
4
5
6
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$propNames = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name | Sort-Object
$object | Select-Object -Property $propNames

PowerShell 技能连载 - 对象的魔法(第 2 部分)

通过隐藏的 “PSObject“ 属性,您可以获取对象成员的详细信息。例如,如果您希望知道哪个属性可以被改变,请试试这段代码:

1
2
3
4
5
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject.Properties.Where{$_.IsSettable}.Name

结果是进程对象的可以被赋值的属性列表:

MaxWorkingSet
MinWorkingSet
PriorityBoostEnabled
PriorityClass
ProcessorAffinity
StartInfo
SynchronizingObject
EnableRaisingEvents
Site

类似地,您可以轻松地找出所有当前没有值(为空)的属性:

1
2
3
4
5
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject.Properties.Where{$null -eq $_.Value}.Name

PowerShell 技能连载 - 对象的魔法(第 1 部分)

在 PowerShell 中,大多数据是以 PSObject 来表示的,它是一个由 PowerShell 添加的特殊的对象“包装器”。要获取这个特殊的包装器,可以通过对象名为 “PSObject“ 的隐藏属性。让我们来看看:

1
2
3
4
5
6
7
8
9
10
11
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# get another object
$object = "Hello"

# try again
$object.PSObject

如您所见,该 “PSObject“ 基本上是一个对象的描述。并且它包含了许多有用的信息。以下是一部分:

1
2
3
4
5
6
7
8
9
10
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# find useful information
$object.PSObject.TypeNames | Out-GridView -Title Type
$object.PSObject.Properties | Out-GridView -Title Properties
$object.PSObject.Methods | Out-GridView -Title Methods

PowerShell 技能连载 - 加密文本(第 2 部分)

这是我们的加密解密系列的第二部分。在第一部分中您学到了如何在一台计算机中安全地加密文本。现在我们来关注解密部分。

要正确地解密文本,必须指定加密时使用的相同编码。基于您的加密参数,您必须指定相同的密码。并且基于 -Scope 设置,解密将只能针对您和/或仅在您加密文本的同一机器上工作。

以下是 Unprotect-Text 函数。我们也从上一个技能中复制了 Protect-Text 函数,这样您可以方便地使用这两种功能:

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
116
117
118
119
function Protect-Text
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[String]
$SecretText,

[string]
$Password='',

[string]
[ValidateSet('CurrentUser','LocalMachine')]
$scope = 'CurrentUser',

[string]
[ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
$Encoding = 'Default',

[Switch]
$ReturnByteArray

)
begin
{
Add-Type -AssemblyName System.Security
if ([string]::IsNullOrEmpty($Password))
{
$optionalEntropy = $null
}
else
{
$optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
}
}
process
{
try
{
$userData = [System.Text.Encoding]::$Encoding.GetBytes($SecretText)
$bytes = [System.Security.Cryptography.ProtectedData]::Protect($userData, $optionalEntropy, $scope)
if ($ReturnByteArray)
{
$bytes
}
else
{
[Convert]::ToBase64String($bytes)
}
}
catch
{
throw "Protect-Text: Unable to protect text. $_"
}
}
}



function Unprotect-Text
{
[CmdletBinding(DefaultParameterSetName='Byte')]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="Text", Position=0)]
[string]
$EncryptedString,

[Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="Byte", Position=0)]
[Byte[]]
$EncryptedBytes,

[string]
$Password='',

[string]
[ValidateSet('CurrentUser','LocalMachine')]
$scope = 'CurrentUser',

[string]
[ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
$Encoding = 'Default'

)
begin
{
Add-Type -AssemblyName System.Security

if ([string]::IsNullOrEmpty($Password))
{
$optionalEntropy = $null
}
else
{
$optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
}
}
process
{
try
{
if ($PSCmdlet.ParameterSetName -eq 'Text')
{
$inBytes = [Convert]::FromBase64String($EncryptedString)
}
else
{
$inBytes = $EncryptedBytes
}
$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($inBytes, $optionalEntropy, $scope)
[System.Text.Encoding]::$Encoding.GetString($bytes)
}
catch
{
throw "Unprotect-Text: Unable to unprotect your text. Check optional password, and make sure you are using the same encoding that was used during protection."
}
}
}

以下是如何使用它的示例:

1
2
3
4
5
$text = "This is my secret"

$a = Protect-Text -SecretText $text -scope CurrentUser -Password zumsel

Unprotect-Text -EncryptedString $a -scope CurrentUser -Password zumsel

Protect-Text 创建了一个 Base64 编码的字符串,它可以用 Unprotect-Text 函数来解密。如果您不希望额外的密码,那么只能使用基于 -Scope 的缺省的保护。

要节省空间,您可以使用字节数组来代替 Base64 编码的字符串:

1
2
$b = Protect-Text -SecretText $text -scope CurrentUser -ReturnByteArray
Unprotect-Text -EncryptedBytes $b -scope CurrentUser

PowerShell 技能连载 - 加密文本(第 1 部分)

让我们来看看一种在计算机上加密文本的安全方法。以下的 Protect-Text 函数接受任意文本,并自动加密,不需要密码。它不使用密码,而是使用您的用户帐户和机器,或者只使用您的机器作为密钥。

如果使用 -Scope LocalMachine,任何使用该机器的人都可以解密该文本,但如果该文本泄露给其他人,它无法在其它机器上解密。如果您使用 -Scope CurrentUser,只有加密者可以解密,并且只能在加密的机器上解密。这个加密方案很适合保存您的私人密码。

此外,为了提高安全性,可以在顶部添加(可选)密码。当指定了密码时,将应用上述相同的限制,但此外还需要知道密码才能解密文本。

请注意您也可以控制文本解码。请确保加密和解密使用相同的编码。

以下是加密函数:

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
function Protect-Text
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[String]
$SecretText,

[string]
$Password='',

[string]
[ValidateSet('CurrentUser','LocalMachine')]
$scope = 'CurrentUser',

[string]
[ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
$Encoding = 'Default',

[Switch]
$ReturnByteArray

)
begin
{
Add-Type -AssemblyName System.Security
if ([string]::IsNullOrEmpty($Password))
{
$optionalEntropy = $null
}
else
{
$optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
}
}
process
{
try
{
$userData = [System.Text.Encoding]::$Encoding.GetBytes($SecretText)
$bytes = [System.Security.Cryptography.ProtectedData]::Protect($userData, $optionalEntropy, $scope)
if ($ReturnByteArray)
{
$bytes
}
else
{
[Convert]::ToBase64String($bytes)
}
}
catch
{
throw "Protect-Text: Unable to protect text. $_"
}
}

结果类似这样:

1
2
3
4
PS> Protect-Text -SecretText 'I am encrypted' -scope LocalMachine
AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAF8Hpm9A5A0upZysoxLlvgwQAAAACAAAAAAAQZgAAAAEAACAAAACbOsUoDuZJXNkWIzfAABxktVg+Txn7A8Rz
SCvFP7I9YQAAAAAOgAAAAAIAACAAAABz7G7Tpuoje9meLOuugzx1WSoOUfaBtGPM/XZHytjC8hAAAAApt/TDhJ9EqeWEPLIDkd4bQAAAAAN0Q503Pa7X
MxIMOnaO7qd3LKXJa4qhht+jc+Z0HaaV5/md83ipP1vefYAAUXdj8qv4eREeCBSGMqvKjbaOsOg=

如果您想了解如何将 Base64 编码的文本转换回原始文本,请看我们的下一个技能!

PowerShell 技能连载 - 创建 NT4 密码哈希

在内部,ActiveDirectory 将所有密码存储为所谓的 NTLM 哈希。有许多安全分析工具可以读取和转储这些哈希。

幸运的是,没有可行的方法来解密这些散列并检索原始密码,您可以使用(已知)密码并将其转换为 NTLM 哈希。这是字典攻击的基本过程:他们获取到很长的“已知密码”列表,将它们转换为 NTLM 散列,当它们与实际的 AD 帐户散列匹配时,就算破解出了密码。

这样,您的安全部门就可以将密码黑名单中的不安全密码(如“P@ssw0rd”)转换为NTLM散列,并将它们与Active Directory的密码散列进行比较,以识别需要更改密码的帐户。

这是将纯文本变为 NTLM HASH 的 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
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
116
117
118
119
120
121
122
123
124
125
126
127
function ConvertTo-NTLMPasswordHash
{
#Work based on code found here: https://www.myotherpcisacloud.com/post/getmd4hash
#Original Author: Ryan Ries, 2014
param(
[Parameter(Mandatory=$true)][string]$password
)

Function Get-MD4Hash
{
Param ([Parameter(Mandatory=$True, ValueFromPipeline=$False)]
[Byte[]]$DataToHash)

Set-StrictMode -Version Latest
Add-Type -TypeDefinition @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class BCrypt
{
[DllImport("bcrypt.dll", CharSet = CharSet.Auto)]
public static extern NTStatus BCryptOpenAlgorithmProvider(
[Out] out IntPtr phAlgorithm,
[In] string pszAlgId,
[In, Optional] string pszImplementation,
[In] UInt32 dwFlags);

[DllImport("bcrypt.dll")]
public static extern NTStatus BCryptCloseAlgorithmProvider(
[In, Out] IntPtr hAlgorithm,
[In] UInt32 dwFlags);

[DllImport("bcrypt.dll", CharSet = CharSet.Auto)]
public static extern NTStatus BCryptCreateHash(
[In, Out] IntPtr hAlgorithm,
[Out] out IntPtr phHash,
[Out] IntPtr pbHashObject,
[In, Optional] UInt32 cbHashObject,
[In, Optional] IntPtr pbSecret,
[In] UInt32 cbSecret,
[In] UInt32 dwFlags);

[DllImport("bcrypt.dll")]
public static extern NTStatus BCryptDestroyHash(
[In, Out] IntPtr hHash);

[DllImport("bcrypt.dll")]
public static extern NTStatus BCryptHashData(
[In, Out] IntPtr hHash,
[In, MarshalAs(UnmanagedType.LPArray)] byte[] pbInput,
[In] int cbInput,
[In] UInt32 dwFlags);

[DllImport("bcrypt.dll")]
public static extern NTStatus BCryptFinishHash(
[In, Out] IntPtr hHash,
[Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbInput,
[In] int cbInput,
[In] UInt32 dwFlags);

[Flags]
public enum AlgOpsFlags : uint
{
BCRYPT_PROV_DISPATCH = 0x00000001,
BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008,
BCRYPT_HASH_REUSABLE_FLAG = 0x00000020
}

// This is a gigantic enum and I don't want to copy all of it into this Powershell script.
// Basically anything other than zero means something went wrong.
public enum NTStatus : uint
{
STATUS_SUCCESS = 0x00000000
}
}
'@

[Byte[]]$HashBytes = New-Object Byte[] 16
[IntPtr]$PHAlgorithm = [IntPtr]::Zero
[IntPtr]$PHHash = [IntPtr]::Zero
$NTStatus = [BCrypt]::BCryptOpenAlgorithmProvider([Ref] $PHAlgorithm, 'MD4', $Null, 0)
If ($NTStatus -NE 0)
{
Write-Error "BCryptOpenAlgorithmProvider failed with NTSTATUS $NTStatus"
If ($PHAlgorithm -NE [IntPtr]::Zero)
{
$NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
}
Return
}
$NTStatus = [BCrypt]::BCryptCreateHash($PHAlgorithm, [Ref] $PHHash, [IntPtr]::Zero, 0, [IntPtr]::Zero, 0, 0)
If ($NTStatus -ne 0)
{
Write-Error "BCryptCreateHash failed with NTSTATUS $NTStatus"
If ($PHHash -ne [IntPtr]::Zero)
{
$NTStatus = [BCrypt]::BCryptDestroyHash($PHHash)
}
If ($PHAlgorithm -ne [IntPtr]::Zero)
{
$NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
}
Return
}

$NTStatus = [BCrypt]::BCryptHashData($PHHash, $DataToHash, $DataToHash.Length, 0)
$NTStatus = [BCrypt]::BCryptFinishHash($PHHash, $HashBytes, $HashBytes.Length, 0)

If ($PHHash -NE [IntPtr]::Zero)
{
$NTStatus = [BCrypt]::BCryptDestroyHash($PHHash)
}
If ($PHAlgorithm -NE [IntPtr]::Zero)
{
$NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
}

$HashString = New-Object System.Text.StringBuilder
Foreach ($Byte In $HashBytes)
{
$null = $HashString.Append($Byte.ToString("x2"))
}
$HashString.ToString()

}
Get-MD4Hash -DataToHash ([System.Text.Encoding]::Unicode.getBytes($password))
}

PowerShell 技能连载 - 简易的 PowerShell 聊天室

以下是一个有趣的 PowerShell 脚本,您可以用它来创建一个简易的多频道聊天室。您所需的只是一个所有人都有读写权限的网络共享目录。

该聊天室是基于文件的,并且使用 PowerShell 的功能来监视文件的改变。所以基本上,每个聊天频道是一个文本文件,并且无论何时,如果某人想“说”一些内容,那么就会向文件中添加一行。任何连接到该聊天频道的人实际上都在监视这个文件的改变。

显然,这个“聊天室”只适用于试验,并且有许多限制。例如,当某人在写文件时,其他人无法同时写入。但是,它很好地说明了 PowerShell 如何监视文件并在添加新文本时采取行动。您也可以将这个技术用在日志文件上:PowerShell 可以在日志文件有新内容的时候通知您,并且甚至自动过滤新增加的文本并根据触发关键字发出警告或采取行动。

在开始之前,请确保调整 ServerShare 并将它设置为一个可读写的网络共享目录。

下一步,您可以这样进入聊天室:

1
Enter-Chat -ChatChannelName lunchbreak -Name Tobias -ShowOldPosts

-ShowOldPosts 显示已有的聊天信息。如果没有添加这个参数,那么只能看见新的信息。无论何时运行 Enter-Chat,它都会检查在 -ChatChannelName 中指定名称的文件,如果该文件不存在,就会创建它。

Get-ChatChannel 列出共享目录中的所有聊天文件,以及聊天室最后使用的时间。该信息完全取自文件属性 (LastWriteTime)。

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
# make sure you adjust this path
# it must point to a network share where you have read and write permissions
$ServerShare = "\\myserver\chathome"

function Enter-Chat
{
param
(
[Parameter(Mandatory)]
[string]
$ChatChannelName,

[string]
$Name = $env:USERNAME,

[Switch]
$ShowOldPosts,

$HomeShare = $ServerShare

)

if ($ShowOldPosts)
{
$Option = ''
}
else
{
$Option = '-Tail 0'
}

$Path = Join-Path -Path $HomeShare -ChildPath "$ChatChannelName.txt"
$exists = Test-Path -Path $Path
if ($exists -eq $false)
{
$null = New-Item -Path $Path -Force -ItemType File
}

$process = Start-Process -FilePath powershell -ArgumentList "-noprofile -windowstyle hidden -command Get-COntent -Path '$Path' $Option -Wait | Out-GridView -Title 'Chat: [$ChatChannelName]'" -PassThru

Write-Host "To exit, enter: quit"
"[$Name entered the chat]" | Add-Content -Path $Path
do
{
Write-Host "[$ChatChannelName]: " -ForegroundColor Green -NoNewline
$inputText = Read-Host

$isStopCommand = 'quit','exit','stop','leave' -contains $inputText
if ($isStopCommand -eq $false)
{
"[$Name] $inputText" | Add-Content -Path $Path
}


} until ($isStopCommand -eq $true)
"[$Name left the chat]" | Add-Content -Path $Path

$process | Stop-Process
}



function Get-ChatChannel
{
param
(
$HomeShare = $ServerShare

)

Get-ChildItem -Path $HomeShare -Filter *.txt -File |
ForEach-Object {
[PSCustomObject]@{
ChannelName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
LastActive = $_.LastWriteTime
Started = $_.CreationTime
}
}
}

PowerShell 技能连载 - 测试

在之前的技能中,我们讨论过了类似 haveIbeenpwned.com 的服务。它们从之前的黑客攻击中收集泄露的密码,这样您就可以检查您的密码是否已被泄露,并可能被包括在未来的字典攻击中。

以下是两个有用的函数:Test-Password 请求一个 SecureString。当您收到提示时,输入将会被屏蔽。接下来 Convert-SecureStringToText 将会将整个密码转为纯文本。然后 Test-Password 将把输入的整个密码进行哈希运算,并只发送前 5 个字节到 WEB 服务。您的原始密码永远不会泄露。

Web 服务将返回所有以该 5 个字节开头的泄露的密码哈希,这样您就可以检查返回的哈希中是否包含您的哈希。

如果您的密码在之前的攻击中出现,那么您将受到它在攻击中出现的次数。任何返回大于 0 的数字的密码都必须视为不安全的,并且不该使用。

请记住:当某个密码返回一个大于 0 的数字时,这并不意味着您的密码被破解了。它的意思是,在世界上所有地方中,您的密码在攻击中出现,所以要么您或者其他人使用它并且被黑客攻击:该密码现在不安全并且已成为攻击字典的一部分,黑客会尝试账户。如果您继续使用这个密码,黑客有可能在不就得将来利用简单(快速)的字典攻击来破解它。

在之前的提示中,我们已经讨论了haveIbeenpwned.com等服务。他们从以前的黑客攻击中收集泄露的密码,这样你就可以检查你的密码是否已被泄露,并可能被包括在未来的字典攻击中。

下面有两个有用的函数:Test-Password请求SecureString,因此当您收到提示时,您的输入将被屏蔽。然后将输入的密码转换为纯文本。然后,Test-Password散列您输入的密码,只将前5个字节发送给web服务。您的原始密码永远不会泄露。

web服务将返回所有以您的5个字节开头的密码散列,因此您可以检查返回的散列是否与您的散列匹配。

如果您的密码在以前的攻击中被发现,您将收到它参与攻击的次数。任何返回大于0的数字的密码都被认为是不安全的,不应该使用。

请记住:当密码返回一个大于0的数字时,这并不意味着您自己的密码被破解了。它的意思是,在世界上任何地方,你的密码在攻击中出现,所以要么你或其他人使用它并被黑客攻击。无论是你还是别人:这个密码现在是不安全的,因为它成为黑客攻击字典的一部分,黑客会尝试帐户。如果你继续使用这个密码,黑客很有可能在不久的将来利用简单(快速)的字典攻击来破解它。

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
function Convert-SecureStringToText{
param (
[Parameter(Mandatory,ValueFromPipeline)]
[System.Security.SecureString]
$Password )

process {
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
}
}function Test-Password
{ [CmdletBinding()]
param (
[Parameter(Mandatory, Position=0)]
[System.Security.SecureString]
$Password )

$plain = $Password | Convert-SecureStringToText
$bytes = [Text.Encoding]::UTF8.GetBytes($plain)
$stream = [IO.MemoryStream]::new($bytes)
$hash = Get-FileHash -Algorithm 'SHA1' -InputStream $stream $stream.Close()
$stream.Dispose()

$first5hashChars,$remainingHashChars = $hash.Hash -split '(?<=^.{5})'
$url = "https://api.pwnedpasswords.com/range/$first5hashChars" [Net.ServicePointManager]::SecurityProtocol = 'Tls12' $response = Invoke-RestMethod -Uri $url -UseBasicParsing
$lines = $response -split '\r\n' $filteredLines = $lines -like "$remainingHashChars*"
[int]($filteredLines -split ':')[-1]
}