Instantly share code, notes, and snippets.
Last active
November 4, 2024 09:46
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save fbogner/1caac69163f2fdf47baf882549693282 to your computer and use it in GitHub Desktop.
Invoke-Sharefinder-Ignore-Empty-Folder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #requires -version 2 | |
| <# | |
| Implementation of Sharefinder that utilizes | |
| https://github.com/mattifestation/psreflect to | |
| stay off of disk. | |
| By @harmj0y | |
| #> | |
| function New-InMemoryModule | |
| { | |
| <# | |
| .SYNOPSIS | |
| Creates an in-memory assembly and module | |
| Author: Matthew Graeber (@mattifestation) | |
| License: BSD 3-Clause | |
| Required Dependencies: None | |
| Optional Dependencies: None | |
| .DESCRIPTION | |
| When defining custom enums, structs, and unmanaged functions, it is | |
| necessary to associate to an assembly module. This helper function | |
| creates an in-memory module that can be passed to the 'enum', | |
| 'struct', and Add-Win32Type functions. | |
| .PARAMETER ModuleName | |
| Specifies the desired name for the in-memory assembly and module. If | |
| ModuleName is not provided, it will default to a GUID. | |
| .EXAMPLE | |
| $Module = New-InMemoryModule -ModuleName Win32 | |
| #> | |
| [OutputType([Reflection.Emit.ModuleBuilder])] | |
| Param | |
| ( | |
| [Parameter(Position = 0)] | |
| [ValidateNotNullOrEmpty()] | |
| [String] | |
| $ModuleName = [Guid]::NewGuid().ToString() | |
| ) | |
| $DynAssembly = New-Object Reflection.AssemblyName($ModuleName) | |
| $Domain = [AppDomain]::CurrentDomain | |
| $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, 'Run') | |
| $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($ModuleName, $False) | |
| return $ModuleBuilder | |
| } | |
| # A helper function used to reduce typing while defining function | |
| # prototypes for Add-Win32Type. | |
| # Author: Matthew Graeber (@mattifestation) | |
| function func | |
| { | |
| Param | |
| ( | |
| [Parameter(Position = 0, Mandatory = $True)] | |
| [String] | |
| $DllName, | |
| [Parameter(Position = 1, Mandatory = $True)] | |
| [string] | |
| $FunctionName, | |
| [Parameter(Position = 2, Mandatory = $True)] | |
| [Type] | |
| $ReturnType, | |
| [Parameter(Position = 3)] | |
| [Type[]] | |
| $ParameterTypes, | |
| [Parameter(Position = 4)] | |
| [Runtime.InteropServices.CallingConvention] | |
| $NativeCallingConvention, | |
| [Parameter(Position = 5)] | |
| [Runtime.InteropServices.CharSet] | |
| $Charset, | |
| [Switch] | |
| $SetLastError | |
| ) | |
| $Properties = @{ | |
| DllName = $DllName | |
| FunctionName = $FunctionName | |
| ReturnType = $ReturnType | |
| } | |
| if ($ParameterTypes) { $Properties['ParameterTypes'] = $ParameterTypes } | |
| if ($NativeCallingConvention) { $Properties['NativeCallingConvention'] = $NativeCallingConvention } | |
| if ($Charset) { $Properties['Charset'] = $Charset } | |
| if ($SetLastError) { $Properties['SetLastError'] = $SetLastError } | |
| New-Object PSObject -Property $Properties | |
| } | |
| function Add-Win32Type | |
| { | |
| <# | |
| .SYNOPSIS | |
| Creates a .NET type for an unmanaged Win32 function. | |
| Author: Matthew Graeber (@mattifestation) | |
| License: BSD 3-Clause | |
| Required Dependencies: None | |
| Optional Dependencies: func | |
| .DESCRIPTION | |
| Add-Win32Type enables you to easily interact with unmanaged (i.e. | |
| Win32 unmanaged) functions in PowerShell. After providing | |
| Add-Win32Type with a function signature, a .NET type is created | |
| using reflection (i.e. csc.exe is never called like with Add-Type). | |
| The 'func' helper function can be used to reduce typing when defining | |
| multiple function definitions. | |
| .PARAMETER DllName | |
| The name of the DLL. | |
| .PARAMETER FunctionName | |
| The name of the target function. | |
| .PARAMETER ReturnType | |
| The return type of the function. | |
| .PARAMETER ParameterTypes | |
| The function parameters. | |
| .PARAMETER NativeCallingConvention | |
| Specifies the native calling convention of the function. Defaults to | |
| stdcall. | |
| .PARAMETER Charset | |
| If you need to explicitly call an 'A' or 'W' Win32 function, you can | |
| specify the character set. | |
| .PARAMETER SetLastError | |
| Indicates whether the callee calls the SetLastError Win32 API | |
| function before returning from the attributed method. | |
| .PARAMETER Module | |
| The in-memory module that will host the functions. Use | |
| New-InMemoryModule to define an in-memory module. | |
| .PARAMETER Namespace | |
| An optional namespace to prepend to the type. Add-Win32Type defaults | |
| to a namespace consisting only of the name of the DLL. | |
| .EXAMPLE | |
| $Mod = New-InMemoryModule -ModuleName Win32 | |
| $FunctionDefinitions = @( | |
| (func kernel32 GetProcAddress ([IntPtr]) @([IntPtr], [String]) -Charset Ansi -SetLastError), | |
| (func kernel32 GetModuleHandle ([Intptr]) @([String]) -SetLastError), | |
| (func ntdll RtlGetCurrentPeb ([IntPtr]) @()) | |
| ) | |
| $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' | |
| $Kernel32 = $Types['kernel32'] | |
| $Ntdll = $Types['ntdll'] | |
| $Ntdll::RtlGetCurrentPeb() | |
| $ntdllbase = $Kernel32::GetModuleHandle('ntdll') | |
| $Kernel32::GetProcAddress($ntdllbase, 'RtlGetCurrentPeb') | |
| .NOTES | |
| Inspired by Lee Holmes' Invoke-WindowsApi http://poshcode.org/2189 | |
| When defining multiple function prototypes, it is ideal to provide | |
| Add-Win32Type with an array of function signatures. That way, they | |
| are all incorporated into the same in-memory module. | |
| #> | |
| [OutputType([Hashtable])] | |
| Param( | |
| [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] | |
| [String] | |
| $DllName, | |
| [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] | |
| [String] | |
| $FunctionName, | |
| [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] | |
| [Type] | |
| $ReturnType, | |
| [Parameter(ValueFromPipelineByPropertyName = $True)] | |
| [Type[]] | |
| $ParameterTypes, | |
| [Parameter(ValueFromPipelineByPropertyName = $True)] | |
| [Runtime.InteropServices.CallingConvention] | |
| $NativeCallingConvention = [Runtime.InteropServices.CallingConvention]::StdCall, | |
| [Parameter(ValueFromPipelineByPropertyName = $True)] | |
| [Runtime.InteropServices.CharSet] | |
| $Charset = [Runtime.InteropServices.CharSet]::Auto, | |
| [Parameter(ValueFromPipelineByPropertyName = $True)] | |
| [Switch] | |
| $SetLastError, | |
| [Parameter(Mandatory = $True)] | |
| [Reflection.Emit.ModuleBuilder] | |
| $Module, | |
| [ValidateNotNull()] | |
| [String] | |
| $Namespace = '' | |
| ) | |
| BEGIN | |
| { | |
| $TypeHash = @{} | |
| } | |
| PROCESS | |
| { | |
| # Define one type for each DLL | |
| if (!$TypeHash.ContainsKey($DllName)) | |
| { | |
| if ($Namespace) | |
| { | |
| $TypeHash[$DllName] = $Module.DefineType("$Namespace.$DllName", 'Public,BeforeFieldInit') | |
| } | |
| else | |
| { | |
| $TypeHash[$DllName] = $Module.DefineType($DllName, 'Public,BeforeFieldInit') | |
| } | |
| } | |
| $Method = $TypeHash[$DllName].DefineMethod( | |
| $FunctionName, | |
| 'Public,Static,PinvokeImpl', | |
| $ReturnType, | |
| $ParameterTypes) | |
| # Make each ByRef parameter an Out parameter | |
| $i = 1 | |
| foreach($Parameter in $ParameterTypes) | |
| { | |
| if ($Parameter.IsByRef) | |
| { | |
| [void] $Method.DefineParameter($i, 'Out', $null) | |
| } | |
| $i++ | |
| } | |
| $DllImport = [Runtime.InteropServices.DllImportAttribute] | |
| $SetLastErrorField = $DllImport.GetField('SetLastError') | |
| $CallingConventionField = $DllImport.GetField('CallingConvention') | |
| $CharsetField = $DllImport.GetField('CharSet') | |
| if ($SetLastError) { $SLEValue = $True } else { $SLEValue = $False } | |
| # Equivalent to C# version of [DllImport(DllName)] | |
| $Constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String]) | |
| $DllImportAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($Constructor, | |
| $DllName, [Reflection.PropertyInfo[]] @(), [Object[]] @(), | |
| [Reflection.FieldInfo[]] @($SetLastErrorField, $CallingConventionField, $CharsetField), | |
| [Object[]] @($SLEValue, ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), ([Runtime.InteropServices.CharSet] $Charset))) | |
| $Method.SetCustomAttribute($DllImportAttribute) | |
| } | |
| END | |
| { | |
| $ReturnTypes = @{} | |
| foreach ($Key in $TypeHash.Keys) | |
| { | |
| $Type = $TypeHash[$Key].CreateType() | |
| $ReturnTypes[$Key] = $Type | |
| } | |
| return $ReturnTypes | |
| } | |
| } | |
| # A helper function used to reduce typing while defining struct | |
| # fields. | |
| # Author: Matthew Graeber (@mattifestation) | |
| function field | |
| { | |
| Param | |
| ( | |
| [Parameter(Position = 0, Mandatory = $True)] | |
| [UInt16] | |
| $Position, | |
| [Parameter(Position = 1, Mandatory = $True)] | |
| [Type] | |
| $Type, | |
| [Parameter(Position = 2)] | |
| [UInt16] | |
| $Offset, | |
| [Object[]] | |
| $MarshalAs | |
| ) | |
| @{ | |
| Position = $Position | |
| Type = $Type -as [Type] | |
| Offset = $Offset | |
| MarshalAs = $MarshalAs | |
| } | |
| } | |
| # Author: Matthew Graeber (@mattifestation) | |
| function struct | |
| { | |
| <# | |
| .SYNOPSIS | |
| Creates an in-memory struct for use in your PowerShell session. | |
| Author: Matthew Graeber (@mattifestation) | |
| License: BSD 3-Clause | |
| Required Dependencies: None | |
| Optional Dependencies: field | |
| .DESCRIPTION | |
| The 'struct' function facilitates the creation of structs entirely in | |
| memory using as close to a "C style" as PowerShell will allow. Struct | |
| fields are specified using a hashtable where each field of the struct | |
| is comprosed of the order in which it should be defined, its .NET | |
| type, and optionally, its offset and special marshaling attributes. | |
| One of the features of 'struct' is that after your struct is defined, | |
| it will come with a built-in GetSize method as well as an explicit | |
| converter so that you can easily cast an IntPtr to the struct without | |
| relying upon calling SizeOf and/or PtrToStructure in the Marshal | |
| class. | |
| .PARAMETER Module | |
| The in-memory module that will host the struct. Use | |
| New-InMemoryModule to define an in-memory module. | |
| .PARAMETER FullName | |
| The fully-qualified name of the struct. | |
| .PARAMETER StructFields | |
| A hashtable of fields. Use the 'field' helper function to ease | |
| defining each field. | |
| .PARAMETER PackingSize | |
| Specifies the memory alignment of fields. | |
| .PARAMETER ExplicitLayout | |
| Indicates that an explicit offset for each field will be specified. | |
| .EXAMPLE | |
| $Mod = New-InMemoryModule -ModuleName Win32 | |
| $ImageDosSignature = enum $Mod PE.IMAGE_DOS_SIGNATURE UInt16 @{ | |
| DOS_SIGNATURE = 0x5A4D | |
| OS2_SIGNATURE = 0x454E | |
| OS2_SIGNATURE_LE = 0x454C | |
| VXD_SIGNATURE = 0x454C | |
| } | |
| $ImageDosHeader = struct $Mod PE.IMAGE_DOS_HEADER @{ | |
| e_magic = field 0 $ImageDosSignature | |
| e_cblp = field 1 UInt16 | |
| e_cp = field 2 UInt16 | |
| e_crlc = field 3 UInt16 | |
| e_cparhdr = field 4 UInt16 | |
| e_minalloc = field 5 UInt16 | |
| e_maxalloc = field 6 UInt16 | |
| e_ss = field 7 UInt16 | |
| e_sp = field 8 UInt16 | |
| e_csum = field 9 UInt16 | |
| e_ip = field 10 UInt16 | |
| e_cs = field 11 UInt16 | |
| e_lfarlc = field 12 UInt16 | |
| e_ovno = field 13 UInt16 | |
| e_res = field 14 UInt16[] -MarshalAs @('ByValArray', 4) | |
| e_oemid = field 15 UInt16 | |
| e_oeminfo = field 16 UInt16 | |
| e_res2 = field 17 UInt16[] -MarshalAs @('ByValArray', 10) | |
| e_lfanew = field 18 Int32 | |
| } | |
| # Example of using an explicit layout in order to create a union. | |
| $TestUnion = struct $Mod TestUnion @{ | |
| field1 = field 0 UInt32 0 | |
| field2 = field 1 IntPtr 0 | |
| } -ExplicitLayout | |
| .NOTES | |
| PowerShell purists may disagree with the naming of this function but | |
| again, this was developed in such a way so as to emulate a "C style" | |
| definition as closely as possible. Sorry, I'm not going to name it | |
| New-Struct. :P | |
| #> | |
| [OutputType([Type])] | |
| Param | |
| ( | |
| [Parameter(Position = 1, Mandatory = $True)] | |
| [Reflection.Emit.ModuleBuilder] | |
| $Module, | |
| [Parameter(Position = 2, Mandatory = $True)] | |
| [ValidateNotNullOrEmpty()] | |
| [String] | |
| $FullName, | |
| [Parameter(Position = 3, Mandatory = $True)] | |
| [ValidateNotNullOrEmpty()] | |
| [Hashtable] | |
| $StructFields, | |
| [Reflection.Emit.PackingSize] | |
| $PackingSize = [Reflection.Emit.PackingSize]::Unspecified, | |
| [Switch] | |
| $ExplicitLayout | |
| ) | |
| [Reflection.TypeAttributes] $StructAttributes = 'AnsiClass, | |
| Class, | |
| Public, | |
| Sealed, | |
| BeforeFieldInit' | |
| if ($ExplicitLayout) | |
| { | |
| $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::ExplicitLayout | |
| } | |
| else | |
| { | |
| $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::SequentialLayout | |
| } | |
| $StructBuilder = $Module.DefineType($FullName, $StructAttributes, [ValueType], $PackingSize) | |
| $ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0] | |
| $SizeConst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst')) | |
| $Fields = New-Object Hashtable[]($StructFields.Count) | |
| # Sort each field according to the orders specified | |
| # Unfortunately, PSv2 doesn't have the luxury of the | |
| # hashtable [Ordered] accelerator. | |
| foreach ($Field in $StructFields.Keys) | |
| { | |
| $Index = $StructFields[$Field]['Position'] | |
| $Fields[$Index] = @{FieldName = $Field; Properties = $StructFields[$Field]} | |
| } | |
| foreach ($Field in $Fields) | |
| { | |
| $FieldName = $Field['FieldName'] | |
| $FieldProp = $Field['Properties'] | |
| $Offset = $FieldProp['Offset'] | |
| $Type = $FieldProp['Type'] | |
| $MarshalAs = $FieldProp['MarshalAs'] | |
| $NewField = $StructBuilder.DefineField($FieldName, $Type, 'Public') | |
| if ($MarshalAs) | |
| { | |
| $UnmanagedType = $MarshalAs[0] -as ([Runtime.InteropServices.UnmanagedType]) | |
| if ($MarshalAs[1]) | |
| { | |
| $Size = $MarshalAs[1] | |
| $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, | |
| $UnmanagedType, $SizeConst, @($Size)) | |
| } | |
| else | |
| { | |
| $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @($UnmanagedType)) | |
| } | |
| $NewField.SetCustomAttribute($AttribBuilder) | |
| } | |
| if ($ExplicitLayout) { $NewField.SetOffset($Offset) } | |
| } | |
| # Make the struct aware of its own size. | |
| # No more having to call [Runtime.InteropServices.Marshal]::SizeOf! | |
| $SizeMethod = $StructBuilder.DefineMethod('GetSize', | |
| 'Public, Static', | |
| [Int], | |
| [Type[]] @()) | |
| $ILGenerator = $SizeMethod.GetILGenerator() | |
| # Thanks for the help, Jason Shirk! | |
| $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) | |
| $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, | |
| [Type].GetMethod('GetTypeFromHandle')) | |
| $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, | |
| [Runtime.InteropServices.Marshal].GetMethod('SizeOf', [Type[]] @([Type]))) | |
| $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ret) | |
| # Allow for explicit casting from an IntPtr | |
| # No more having to call [Runtime.InteropServices.Marshal]::PtrToStructure! | |
| $ImplicitConverter = $StructBuilder.DefineMethod('op_Implicit', | |
| 'PrivateScope, Public, Static, HideBySig, SpecialName', | |
| $StructBuilder, | |
| [Type[]] @([IntPtr])) | |
| $ILGenerator2 = $ImplicitConverter.GetILGenerator() | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Nop) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldarg_0) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, | |
| [Type].GetMethod('GetTypeFromHandle')) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, | |
| [Runtime.InteropServices.Marshal].GetMethod('PtrToStructure', [Type[]] @([IntPtr], [Type]))) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Unbox_Any, $StructBuilder) | |
| $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ret) | |
| $StructBuilder.CreateType() | |
| } | |
| function Test-Server { | |
| <# | |
| .SYNOPSIS | |
| Tests a connection to a remote server. | |
| .DESCRIPTION | |
| This function uses either ping (test-connection) or RPC | |
| (through WMI) to test connectivity to a remote server. | |
| .PARAMETER Server | |
| The hostname/IP to test connectivity to. | |
| .OUTPUTS | |
| $True/$False | |
| .EXAMPLE | |
| > Test-Server -Server WINDOWS7 | |
| Tests ping connectivity to the WINDOWS7 server. | |
| .EXAMPLE | |
| > Test-Server -RPC -Server WINDOWS7 | |
| Tests RPC connectivity to the WINDOWS7 server. | |
| .LINK | |
| http://gallery.technet.microsoft.com/scriptcenter/Enhanced-Remote-Server-84c63560 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory = $True)] | |
| [String] | |
| $Server, | |
| [Switch] | |
| $RPC | |
| ) | |
| if ($RPC){ | |
| $WMIParameters = @{ | |
| namespace = 'root\cimv2' | |
| Class = 'win32_ComputerSystem' | |
| ComputerName = $Name | |
| ErrorAction = 'Stop' | |
| } | |
| if ($Credential -ne $null) | |
| { | |
| $WMIParameters.Credential = $Credential | |
| } | |
| try | |
| { | |
| Get-WmiObject @WMIParameters | |
| } | |
| catch { | |
| Write-Verbose -Message 'Could not connect via WMI' | |
| } | |
| } | |
| # otherwise, use ping | |
| else{ | |
| Test-Connection -ComputerName $Server -count 1 -Quiet | |
| } | |
| } | |
| function Get-ShuffledArray { | |
| <# | |
| .SYNOPSIS | |
| Returns a randomly-shuffled version of a passed array. | |
| .DESCRIPTION | |
| This function takes an array and returns a randomly-shuffled | |
| version. | |
| .PARAMETER Array | |
| The passed array to shuffle. | |
| .OUTPUTS | |
| System.Array. The passed array but shuffled. | |
| .EXAMPLE | |
| > $shuffled = Get-ShuffledArray $array | |
| Get a shuffled version of $array. | |
| .LINK | |
| http://sqlchow.wordpress.com/2013/03/04/shuffle-the-deck-using-powershell/ | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Array]$Array | |
| ) | |
| Begin{} | |
| Process{ | |
| $len = $Array.Length | |
| while($len){ | |
| $i = Get-Random ($len --) | |
| $tmp = $Array[$len] | |
| $Array[$len] = $Array[$i] | |
| $Array[$i] = $tmp | |
| } | |
| $Array; | |
| } | |
| } | |
| function Get-NetCurrentUser { | |
| <# | |
| .SYNOPSIS | |
| Gets the name of the current user. | |
| .DESCRIPTION | |
| This function returns the username of the current user context, | |
| with the domain appended if appropriate. | |
| .OUTPUTS | |
| System.String. The current username. | |
| .EXAMPLE | |
| > Get-NetCurrentUser | |
| Return the current user. | |
| #> | |
| [System.Security.Principal.WindowsIdentity]::GetCurrent().Name | |
| } | |
| function Get-NetDomain { | |
| <# | |
| .SYNOPSIS | |
| Returns the name of the current user's domain. | |
| .DESCRIPTION | |
| This function utilizes ADSI (Active Directory Service Interface) to | |
| get the currect domain root and return its distinguished name. | |
| It then formats the name into a single string. | |
| .PARAMETER Base | |
| Just return the base of the current domain (i.e. no .com) | |
| .OUTPUTS | |
| System.String. The full domain name. | |
| .EXAMPLE | |
| > Get-NetDomain | |
| Return the current domain. | |
| .EXAMPLE | |
| > Get-NetDomain -base | |
| Return just the base of the current domain. | |
| .LINK | |
| http://social.technet.microsoft.com/Forums/scriptcenter/en-US/0c5b3f83-e528-4d49-92a4-dee31f4b481c/finding-the-dn-of-the-the-domain-without-admodule-in-powershell?forum=ITCG | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Switch] | |
| $Base | |
| ) | |
| # just get the base of the domain name | |
| if ($Base){ | |
| $temp = [string] ([adsi]'').distinguishedname -replace 'DC=','' -replace ',','.' | |
| $parts = $temp.split('.') | |
| $parts[0..($parts.length-2)] -join '.' | |
| } | |
| else{ | |
| ([adsi]'').distinguishedname -replace 'DC=','' -replace ',','.' | |
| } | |
| } | |
| function Get-NetComputers { | |
| <# | |
| .SYNOPSIS | |
| Gets an array of all current computers objects in a domain. | |
| .DESCRIPTION | |
| This function utilizes adsisearcher to query the current AD context | |
| for current computer objects. Based off of Carlos Perez's Audit.psm1 | |
| script in Posh-SecMod (link below). | |
| .PARAMETER HostName | |
| Return computers with a specific name, wildcards accepted. | |
| .PARAMETER SPN | |
| Return computers with a specific service principal name, wildcards accepted. | |
| .PARAMETER OperatingSystem | |
| Return computers with a specific operating system, wildcards accepted. | |
| .PARAMETER ServicePack | |
| Return computers with a specific service pack, wildcards accepted. | |
| .PARAMETER FullData | |
| Return full user computer objects instead of just system names (the default). | |
| .PARAMETER Domain | |
| The domain to query for computers. | |
| .OUTPUTS | |
| System.Array. An array of found system objects. | |
| .EXAMPLE | |
| > Get-NetComputers | |
| Returns the current computers in current domain. | |
| .EXAMPLE | |
| > Get-NetComputers -SPN mssql* | |
| Returns all MS SQL servers on the domain. | |
| .EXAMPLE | |
| > Get-NetComputers -Domain testing | |
| Returns the current computers in 'testing' domain. | |
| > Get-NetComputers -Domain testing -FullData | |
| Returns full computer objects in the 'testing' domain. | |
| .LINK | |
| https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 | |
| #> | |
| [CmdletBinding()] | |
| Param ( | |
| [string] | |
| $HostName = '*', | |
| [string] | |
| $SPN = '*', | |
| [string] | |
| $OperatingSystem = '*', | |
| [string] | |
| $ServicePack = '*', | |
| [Switch] | |
| $FullData, | |
| [string] | |
| $Domain | |
| ) | |
| # if a domain is specified, try to grab that domain | |
| if ($Domain){ | |
| # try to grab the primary DC for the current domain | |
| try{ | |
| $PrimaryDC = ([Array](Get-NetDomainControllers))[0].Name | |
| } | |
| catch{ | |
| $PrimaryDC = $Null | |
| } | |
| try { | |
| # reference - http://blogs.msdn.com/b/javaller/archive/2013/07/29/searching-across-active-directory-domains-in-powershell.aspx | |
| $dn = "DC=$($Domain.Replace('.', ',DC='))" | |
| # if we could grab the primary DC for the current domain, use that for the query | |
| if($PrimaryDC){ | |
| $CompSearcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$PrimaryDC/$dn") | |
| } | |
| else{ | |
| # otherwise try to connect to the DC for the target domain | |
| $CompSearcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$dn") | |
| } | |
| # create the searcher object with our specific filters | |
| if ($ServicePack -ne '*'){ | |
| $CompSearcher.filter="(&(objectClass=Computer)(dnshostname=$HostName)(operatingsystem=$OperatingSystem)(operatingsystemservicepack=$ServicePack)(servicePrincipalName=$SPN))" | |
| } | |
| else{ | |
| # server 2012 peculiarity- remove any mention to service pack | |
| $CompSearcher.filter="(&(objectClass=Computer)(dnshostname=$HostName)(operatingsystem=$OperatingSystem)(servicePrincipalName=$SPN))" | |
| } | |
| } | |
| catch{ | |
| Write-Warning "The specified domain $Domain does not exist, could not be contacted, or there isn't an existing trust." | |
| } | |
| } | |
| else{ | |
| # otherwise, use the current domain | |
| if ($ServicePack -ne '*'){ | |
| $CompSearcher = [adsisearcher]"(&(objectClass=Computer)(dnshostname=$HostName)(operatingsystem=$OperatingSystem)(operatingsystemservicepack=$ServicePack)(servicePrincipalName=$SPN))" | |
| } | |
| else{ | |
| # server 2012 peculiarity- remove any mention to service pack | |
| $CompSearcher = [adsisearcher]"(&(objectClass=Computer)(dnshostname=$HostName)(operatingsystem=$OperatingSystem)(servicePrincipalName=$SPN))" | |
| } | |
| } | |
| if ($CompSearcher){ | |
| # eliminate that pesky 1000 system limit | |
| $CompSearcher.PageSize = 200 | |
| $CompSearcher.FindAll() | ForEach-Object { | |
| # return full data objects | |
| if ($FullData){ | |
| $_.properties | |
| } | |
| else{ | |
| # otherwise we're just returning the DNS host name | |
| $_.properties.dnshostname | |
| } | |
| } | |
| } | |
| } | |
| function Get-NetShare { | |
| <# | |
| .SYNOPSIS | |
| Gets share information for a specified server. | |
| .DESCRIPTION | |
| This function will execute the NetShareEnum Win32API call to query | |
| a given host for open shares. This is a replacement for | |
| "net share \\hostname" | |
| .PARAMETER HostName | |
| The hostname to query for shares. | |
| .OUTPUTS | |
| SHARE_INFO_1 structure. A representation of the SHARE_INFO_1 | |
| result structure which includes the name and note for each share. | |
| .EXAMPLE | |
| > Get-NetShare | |
| Returns active shares on the local host. | |
| .EXAMPLE | |
| > Get-NetShare -HostName sqlserver | |
| Returns active shares on the 'sqlserver' host | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string] | |
| $HostName = 'localhost' | |
| ) | |
| If ($PSBoundParameters['Debug']) { | |
| $DebugPreference = 'Continue' | |
| } | |
| # arguments for NetShareEnum | |
| $QueryLevel = 1 | |
| $ptrInfo = [IntPtr]::Zero | |
| $EntriesRead = 0 | |
| $TotalRead = 0 | |
| $ResumeHandle = 0 | |
| # get the share information | |
| $Result = $Netapi32::NetShareEnum($HostName, $QueryLevel,[ref]$ptrInfo,-1,[ref]$EntriesRead,[ref]$TotalRead,[ref]$ResumeHandle) | |
| # Locate the offset of the initial intPtr | |
| $offset = $ptrInfo.ToInt64() | |
| Write-Debug "Get-NetShare result: $Result" | |
| # 0 = success | |
| if (($Result -eq 0) -and ($offset -gt 0)) { | |
| # Work out how mutch to increment the pointer by finding out the size of the structure | |
| $Increment = $SHARE_INFO_1::GetSize() | |
| # parse all the result structures | |
| for ($i = 0; ($i -lt $EntriesRead); $i++){ | |
| # create a new int ptr at the given offset and cast | |
| # the pointer as our result structure | |
| $newintptr = New-Object system.Intptr -ArgumentList $offset | |
| $Info = $newintptr -as $SHARE_INFO_1 | |
| # return all the sections of the structure | |
| $Info | Select-Object * | |
| $offset = $newintptr.ToInt64() | |
| $offset += $increment | |
| } | |
| # free up the result buffer | |
| $Netapi32::NetApiBufferFree($ptrInfo) | Out-Null | |
| } | |
| else | |
| { | |
| switch ($Result) { | |
| (5) {Write-Debug 'The user does not have access to the requested information.'} | |
| (124) {Write-Debug 'The value specified for the level parameter is not valid.'} | |
| (87) {Write-Debug 'The specified parameter is not valid.'} | |
| (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} | |
| (8) {Write-Debug 'Insufficient memory is available.'} | |
| (2312) {Write-Debug 'A session does not exist with the computer name.'} | |
| (2351) {Write-Debug 'The computer name is not valid.'} | |
| (2221) {Write-Debug 'Username not found.'} | |
| (53) {Write-Debug 'Hostname could not be found'} | |
| } | |
| } | |
| } | |
| function Invoke-ShareFinder { | |
| <# | |
| .SYNOPSIS | |
| Finds (non-standard) shares on machines in the domain. | |
| Author: @harmj0y | |
| .DESCRIPTION | |
| This function finds the local domain name for a host using Get-NetDomain, | |
| queries the domain for all active machines with Get-NetComputers, then for | |
| each server it lists of active shares with Get-NetShare. Non-standard shares | |
| can be filtered out with -Exclude* flags. | |
| .PARAMETER HostList | |
| List of hostnames/IPs to search. | |
| .PARAMETER ExcludeStandard | |
| Exclude standard shares from display (C$, IPC$, print$ etc.) | |
| .PARAMETER ExcludePrint | |
| Exclude the print$ share | |
| .PARAMETER ExcludeIPC | |
| Exclude the IPC$ share | |
| .PARAMETER CheckShareAccess | |
| Only display found shares that the local user has access to. | |
| .PARAMETER CheckAdmin | |
| Only display ADMIN$ shares the local user has access to. | |
| .PARAMETER Ping | |
| Ping each host to ensure it's up before enumerating. | |
| .PARAMETER NoPing | |
| Ping each host to ensure it's up before enumerating. | |
| .PARAMETER NoPing | |
| Don't ping each host to ensure it's up before enumerating. | |
| .PARAMETER Delay | |
| Delay between enumerating hosts, defaults to 0 | |
| .PARAMETER Jitter | |
| Jitter for the host delay, defaults to +/- 0.3 | |
| .PARAMETER Domain | |
| Domain to query for machines. | |
| .EXAMPLE | |
| > Invoke-ShareFinder | |
| Find shares on the domain. | |
| .EXAMPLE | |
| > Invoke-ShareFinder -ExcludeStandard | |
| Find non-standard shares on the domain. | |
| .EXAMPLE | |
| > Invoke-ShareFinder -Delay 60 | |
| Find shares on the domain with a 60 second (+/- *.3) | |
| randomized delay between touching each host. | |
| .EXAMPLE | |
| > Invoke-ShareFinder -HostList hosts.txt | |
| Find shares for machines in the specified hostlist. | |
| .LINK | |
| http://blog.harmj0y.net | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string] | |
| $HostList, | |
| [Switch] | |
| $ExcludeStandard, | |
| [Switch] | |
| $ExcludePrint, | |
| [Switch] | |
| $ExcludeIPC, | |
| [Switch] | |
| $Ping, | |
| [Switch] | |
| $NoPing, | |
| [Switch] | |
| $CheckShareAccess, | |
| [Switch] | |
| $CheckAdmin, | |
| [UInt32] | |
| $Delay = 0, | |
| [double] | |
| $Jitter = .3, | |
| [String] | |
| $Domain | |
| ) | |
| If ($PSBoundParameters['Debug']) { | |
| $DebugPreference = 'Continue' | |
| } | |
| # figure out the shares we want to ignore | |
| [String[]] $excludedShares = @('') | |
| if ($ExcludePrint){ | |
| $excludedShares = $excludedShares + "PRINT$" | |
| } | |
| if ($ExcludeIPC){ | |
| $excludedShares = $excludedShares + "IPC$" | |
| } | |
| if ($ExcludeStandard){ | |
| $excludedShares = @('', "ADMIN$", "IPC$", "C$", "PRINT$") | |
| } | |
| # random object for delay | |
| $randNo = New-Object System.Random | |
| # get the current user | |
| $CurrentUser = Get-NetCurrentUser | |
| # get the target domain | |
| if($Domain){ | |
| $targetDomain = $Domain | |
| } | |
| else{ | |
| # use the local domain | |
| $targetDomain = Get-NetDomain | |
| } | |
| Write-Verbose "[*] Running ShareFinder on domain $targetDomain with delay of $Delay" | |
| $servers = @() | |
| # if we're using a host list, read the targets in and add them to the target list | |
| if($HostList){ | |
| if (Test-Path -Path $HostList){ | |
| $servers = Get-Content -Path $HostList | |
| } | |
| else { | |
| Write-Warning "`r`n[!] Input file '$HostList' doesn't exist!`r`n" | |
| return $null | |
| } | |
| } | |
| else{ | |
| # otherwise, query the domain for target servers | |
| Write-Verbose "[*] Querying domain $targetDomain for hosts...`r`n" | |
| $servers = Get-NetComputers -Domain $targetDomain | |
| } | |
| # randomize the server list | |
| $servers = Get-ShuffledArray $servers | |
| if (($servers -eq $null) -or ($servers.Count -eq 0)){ | |
| Write-Warning "`r`n[!] No hosts found!" | |
| return $null | |
| } | |
| else{ | |
| # return/output the current status lines | |
| $counter = 0 | |
| foreach ($server in $servers){ | |
| $counter = $counter + 1 | |
| Write-Verbose "[*] Enumerating server $server ($counter of $($servers.count))" | |
| if ($server -ne ''){ | |
| # sleep for our semi-randomized interval | |
| Start-Sleep -Seconds $randNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) | |
| # optionally check if the server is up first | |
| $up = $true | |
| if(-not $NoPing){ | |
| $up = Test-Server -Server $server | |
| } | |
| if($up){ | |
| # get the shares for this host and display what we find | |
| $shares = Get-NetShare -HostName $server | |
| foreach ($share in $shares) { | |
| Write-Debug "[*] Server share: $share" | |
| $netname = $share.shi1_netname | |
| $remark = $share.shi1_remark | |
| $path = '\\'+$server+'\'+$netname | |
| # make sure we get a real share name back | |
| if (($netname) -and ($netname.trim() -ne '')){ | |
| # if we're just checking for access to ADMIN$ | |
| if($CheckAdmin){ | |
| if($netname.ToUpper() -eq "ADMIN$"){ | |
| try{ | |
| $f=[IO.Directory]::GetFiles($path) | |
| "\\$server\$netname `t- $remark" | |
| } | |
| catch {} | |
| } | |
| } | |
| # skip this share if it's in the exclude list | |
| elseif ($excludedShares -notcontains $netname.ToUpper()){ | |
| # see if we want to check access to this share | |
| if($CheckShareAccess){ | |
| # check if the user has access to this path | |
| try{ | |
| $f=[IO.Directory]::EnumerateFileSystemEntries($path).GetEnumerator().MoveNext() | |
| # If the folder is empty we ignore the share | |
| if ($f -eq $false) { | |
| throw "Folder is empty - ignoring" | |
| } | |
| "\\$server\$netname `t- $remark" | |
| } | |
| catch {} | |
| } | |
| else{ | |
| "\\$server\$netname `t- $remark" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function Invoke-ShareFinderThreaded { | |
| <# | |
| .SYNOPSIS | |
| Finds (non-standard) shares on machines in the domain. | |
| Threaded version of Invoke-ShareFinder. | |
| Author: @harmj0y | |
| .DESCRIPTION | |
| This function finds the local domain name for a host using Get-NetDomain, | |
| queries the domain for all active machines with Get-NetComputers, then for | |
| each server it lists of active shares with Get-NetShare. Non-standard shares | |
| can be filtered out with -Exclude* flags. | |
| Threaded version of Invoke-ShareFinder. | |
| .PARAMETER HostList | |
| List of hostnames/IPs to search. | |
| .PARAMETER ExcludedShares | |
| Shares to exclude from output, wildcards accepted (i.e. IPC*) | |
| .PARAMETER CheckShareAccess | |
| Only display found shares that the local user has access to. | |
| .PARAMETER CheckAdmin | |
| Only display ADMIN$ shares the local user has access to. | |
| .PARAMETER NoPing | |
| Don't ping each host to ensure it's up before enumerating. | |
| .PARAMETER Domain | |
| Domain to query for machines. | |
| .PARAMETER MaxThreads | |
| The maximum concurrent threads to execute. | |
| .EXAMPLE | |
| > Invoke-ShareFinder | |
| Find shares on the domain. | |
| .EXAMPLE | |
| > Invoke-ShareFinder -ExcludedShares IPC$,PRINT$ | |
| Find shares on the domain excluding IPC$ and PRINT$ | |
| .EXAMPLE | |
| > Invoke-ShareFinder -HostList hosts.txt | |
| Find shares for machines in the specified hostlist. | |
| .LINK | |
| http://blog.harmj0y.net | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string] | |
| $HostList, | |
| [string[]] | |
| $ExcludedShares = @(), | |
| [Switch] | |
| $NoPing, | |
| [Switch] | |
| $CheckShareAccess, | |
| [Switch] | |
| $CheckAdmin, | |
| [String] | |
| $Domain, | |
| [Int] | |
| $MaxThreads = 10 | |
| ) | |
| If ($PSBoundParameters['Debug']) { | |
| $DebugPreference = 'Continue' | |
| } | |
| # get the current user | |
| $CurrentUser = Get-NetCurrentUser | |
| # get the target domain | |
| if($Domain){ | |
| $targetDomain = $Domain | |
| } | |
| else{ | |
| # use the local domain | |
| $targetDomain = Get-NetDomain | |
| } | |
| Write-Verbose "[*] Running Invoke-ShareFinderThreaded on domain $targetDomain with delay of $Delay" | |
| $servers = @() | |
| # if we're using a host list, read the targets in and add them to the target list | |
| if($HostList){ | |
| if (Test-Path -Path $HostList){ | |
| $servers = Get-Content -Path $HostList | |
| } | |
| else { | |
| Write-Warning "`r`n[!] Input file '$HostList' doesn't exist!`r`n" | |
| return $null | |
| } | |
| } | |
| else{ | |
| # otherwise, query the domain for target servers | |
| Write-Verbose "[*] Querying domain $targetDomain for hosts...`r`n" | |
| $servers = Get-NetComputers -Domain $targetDomain | |
| } | |
| # randomize the server list | |
| $servers = Get-ShuffledArray $servers | |
| if (($servers -eq $null) -or ($servers.Count -eq 0)){ | |
| Write-Warning "`r`n[!] No hosts found!" | |
| return $null | |
| } | |
| # script block that eunmerates a server | |
| # this is called by the multi-threading code later | |
| $EnumServerBlock = { | |
| param($Server, $Ping, $CheckShareAccess, $ExcludedShares, $CheckAdmin) | |
| # optionally check if the server is up first | |
| $up = $true | |
| if($Ping){ | |
| $up = Test-Server -Server $Server | |
| } | |
| if($up){ | |
| # get the shares for this host and check what we find | |
| $shares = Get-NetShare -HostName $Server | |
| foreach ($share in $shares) { | |
| Write-Debug "[*] Server share: $share" | |
| $netname = $share.shi1_netname | |
| $remark = $share.shi1_remark | |
| $path = '\\'+$server+'\'+$netname | |
| # make sure we get a real share name back | |
| if (($netname) -and ($netname.trim() -ne '')){ | |
| # if we're just checking for access to ADMIN$ | |
| if($CheckAdmin){ | |
| if($netname.ToUpper() -eq "ADMIN$"){ | |
| try{ | |
| $f=[IO.Directory]::GetFiles($path) | |
| "\\$server\$netname `t- $remark" | |
| } | |
| catch {} | |
| } | |
| } | |
| # skip this share if it's in the exclude list | |
| elseif ($excludedShares -notcontains $netname.ToUpper()){ | |
| # see if we want to check access to this share | |
| if($CheckShareAccess){ | |
| # check if the user has access to this path | |
| try{ | |
| $f=[IO.Directory]::GetFiles($path) | |
| "\\$server\$netname `t- $remark" | |
| } | |
| catch {} | |
| } | |
| else{ | |
| "\\$server\$netname `t- $remark" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| # Adapted from: | |
| # http://powershell.org/wp/forums/topic/invpke-parallel-need-help-to-clone-the-current-runspace/ | |
| $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() | |
| $sessionState.ApartmentState = [System.Threading.Thread]::CurrentThread.GetApartmentState() | |
| # grab all the current variables for this runspace | |
| $MyVars = Get-Variable -Scope 1 | |
| # these Variables are added by Runspace.Open() Method and produce Stop errors if you add them twice | |
| $VorbiddenVars = @("?","args","ConsoleFileName","Error","ExecutionContext","false","HOME","Host","input","InputObject","MaximumAliasCount","MaximumDriveCount","MaximumErrorCount","MaximumFunctionCount","MaximumHistoryCount","MaximumVariableCount","MyInvocation","null","PID","PSBoundParameters","PSCommandPath","PSCulture","PSDefaultParameterValues","PSHOME","PSScriptRoot","PSUICulture","PSVersionTable","PWD","ShellId","SynchronizedHash","true") | |
| # Add Variables from Parent Scope (current runspace) into the InitialSessionState | |
| ForEach($Var in $MyVars) { | |
| If($VorbiddenVars -notcontains $Var.Name) { | |
| $sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Var.name,$Var.Value,$Var.description,$Var.options,$Var.attributes)) | |
| } | |
| } | |
| # Add Functions from current runspace to the InitialSessionState | |
| ForEach($Function in (Get-ChildItem Function:)) { | |
| $sessionState.Commands.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $Function.Definition)) | |
| } | |
| # threading adapted from | |
| # https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407 | |
| # Thanks Carlos! | |
| $counter = 0 | |
| # create a pool of maxThread runspaces | |
| $pool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $sessionState, $host) | |
| $pool.Open() | |
| $jobs = @() | |
| $ps = @() | |
| $wait = @() | |
| $serverCount = $servers.count | |
| "`r`n[*] Enumerating $serverCount servers..." | |
| foreach ($server in $servers){ | |
| # make sure we get a server name | |
| if ($server -ne ''){ | |
| Write-Verbose "[*] Enumerating server $server ($counter of $($servers.count))" | |
| While ($($pool.GetAvailableRunspaces()) -le 0) { | |
| Start-Sleep -milliseconds 500 | |
| } | |
| # create a "powershell pipeline runner" | |
| $ps += [powershell]::create() | |
| $ps[$counter].runspacepool = $pool | |
| # add the script block + arguments | |
| [void]$ps[$counter].AddScript($EnumServerBlock).AddParameter('Server', $server).AddParameter('Ping', -not $NoPing).AddParameter('CheckShareAccess', $CheckShareAccess).AddParameter('ExcludedShares', $ExcludedShares).AddParameter('CheckAdmin', $CheckAdmin) | |
| # start job | |
| $jobs += $ps[$counter].BeginInvoke(); | |
| # store wait handles for WaitForAll call | |
| $wait += $jobs[$counter].AsyncWaitHandle | |
| } | |
| $counter = $counter + 1 | |
| } | |
| Write-Verbose "Waiting for scanning threads to finish..." | |
| $waitTimeout = Get-Date | |
| while ($($jobs | ? {$_.IsCompleted -eq $false}).count -gt 0 -or $($($(Get-Date) - $waitTimeout).totalSeconds) -gt 60) { | |
| Start-Sleep -milliseconds 500 | |
| } | |
| # end async call | |
| for ($y = 0; $y -lt $counter; $y++) { | |
| try { | |
| # complete async job | |
| $ps[$y].EndInvoke($jobs[$y]) | |
| } catch { | |
| Write-Warning "error: $_" | |
| } | |
| finally { | |
| $ps[$y].Dispose() | |
| } | |
| } | |
| $pool.Dispose() | |
| } | |
| $Mod = New-InMemoryModule -ModuleName Win32 | |
| # all of the Win32 API functions we need | |
| $FunctionDefinitions = @( | |
| (func netapi32 NetShareEnum ([Int]) @([string], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), | |
| (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])) | |
| ) | |
| # the NetShareEnum result structure | |
| $SHARE_INFO_1 = struct $Mod SHARE_INFO_1 @{ | |
| shi1_netname = field 0 String -MarshalAs @('LPWStr') | |
| shi1_type = field 1 UInt32 | |
| shi1_remark = field 2 String -MarshalAs @('LPWStr') | |
| } | |
| $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' | |
| $Netapi32 = $Types['netapi32'] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment