├── .gitignore ├── test_output.png ├── test_report.xml ├── Epinephelus_malabaricus.jpg ├── TODO ├── LICENSE ├── README.md └── grouper.psm1 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /test_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Grouper/master/test_output.png -------------------------------------------------------------------------------- /test_report.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Grouper/master/test_report.xml -------------------------------------------------------------------------------- /Epinephelus_malabaricus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Grouper/master/Epinephelus_malabaricus.jpg -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Get-GPOFilePerms - parse SDDL 2 | Get-GPOAccountSettings - implement filtering 3 | Get-GPOFolderRedirection - get permissions on target path 4 | Implement pipelining of Get-GPOReport output 5 | add screensaver checks 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grouper 2 | 3 | A PowerShell script for helping to find vulnerable settings in AD Group Policy. 4 | 5 | ![A picture of a fish](./Epinephelus_malabaricus.jpg) 6 | ###### *Photo by Jon Hanson* - - 7 | 8 | ## Summary 9 | Grouper is a PowerShell module designed for pentesters and redteamers (although probably also useful for sysadmins) which sifts through the (usually very noisy) XML output from the Get-GPOReport cmdlet (part of Microsoft's Group Policy module) and identifies all the settings defined in Group Policy Objects (GPOs) that might prove useful to someone trying to do something fun/evil. 10 | 11 | Examples of the kinds of stuff it finds in GPOs: 12 | * GPOs which grant modify permissions on the GPO itself to non-default users. 13 | * Startup and shutdown scripts 14 | * arguments and script themselves often include creds. 15 | * scripts are often stored with permissions that allow you to modify them. 16 | * MSI installers being automatically deployed 17 | * again, often stored somewhere that will grant you modify permissions. 18 | * Good old fashioned Group Policy Preferences passwords. 19 | * Autologon registry entries containing credentials. 20 | * Other creds being stored in the registry for fun stuff like VNC. 21 | * Scheduled tasks with stored credentials. 22 | * Also often run stuff from poorly secured file shares. 23 | * User Rights 24 | * Handy to spot where admins accidentally granted 'Domain Users' RDP access or those fun rights that let you run mimikatz even without full admin privs. 25 | * Tweaks to local file permissions 26 | * Good for finding those machines where the admins just stamped "Full Control" for "Everyone" on "C:\Program Files". 27 | * File Shares 28 | * INI Files 29 | * Environment Variables 30 | * ... and much more! (well, not very much, but some) 31 | 32 | Yes it's pretty rough, but it saves me an enormous amount of time reading through those awful 150MB HTML GPO reports, and if it works for me it might work for you. 33 | 34 | Note: While some function names might include the word audit, Groper is explicitly NOT meant to be an exhaustive audit for best practice configurations etc. If you want that, you should be using Microsoft SCT and LGPO.exe or something. 35 | 36 | ## Usage 37 | 38 | Generate a GPO Report on a Windows machine with the Group Policy cmdlets installed. 39 | These are installed on Domain Controllers by default, can be installed on Windows clients using RSAT, or can be enabled through the "Add Feature" wizard on Windows servers. 40 | 41 | ``` 42 | Get-GPOReport -All -ReportType xml -Path C:\temp\gporeport.xml 43 | ``` 44 | 45 | Import the Grouper module. 46 | 47 | ``` 48 | Import-Module .\grouper.ps1 49 | ``` 50 | 51 | Run Grouper. 52 | 53 | ``` 54 | Invoke-AuditGPOReport -Path C:\temp\gporeport.xml 55 | ``` 56 | 57 | ## Parameters 58 | There's also a couple of parameters you can mess with that alter which policy settings Grouper will show you: 59 | ``` 60 | -showDisabled 61 | ``` 62 | By default, Grouper will only show you GPOs that are currently enabled and linked to an OU in AD. This toggles that behaviour. 63 | ``` 64 | -Online 65 | ``` 66 | By default Grouper only works with the actual XML output from Get-GPOReport, and does no network comms at all, making it quite "opsec safe", though I do hate that term. 67 | 68 | If you invoke it with -Online, Grouper will turn on checks that require talking to (at least) the AD domain from which the report was generated, but will also likely involve talking to e.g. file servers. This will allow Grouper to do handy things like report the ACLs on files targeted by GPOs, and check if e.g. the current user can write to the file in question. 69 | ``` 70 | -Level 71 | ``` 72 | Grouper has 3 levels of filtering you can apply to its output. 73 | 74 | 1. Show me all the settings you can. 75 | 2. (Default) Show me only settings that seem 'interesting' but may or may not be vulnerable. 76 | 3. Show me only settings that are definitely a super bad idea and will probably have creds in them or are going to otherwise grant me admin on a host. 77 | 78 | Usage is straightforward. -Level 3, -Level 2, etc. 79 | 80 | ___ 81 | ## Frequently Asked Questions 82 | 83 | ### I'm on a gig and can't find a domain-joined machine that I have access to with the Group Policy cmdlets installed and I don't want to install them because that's noisy and messy! 84 | 85 | Get-GPOReport works just fine on non-domain-joined machines via runas /netonly. You'll need some low-priv creds but that's to be expected. 86 | 87 | Do like this: 88 | 89 | ``` 90 | runas /netonly /user:domain\user powershell.exe 91 | ``` 92 | 93 | on a non-domain-joined machine that can communicate with a domain controller. 94 | 95 | Then in the resulting PowerShell session do like this: 96 | 97 | ``` 98 | Get-GpoReport -Domain example.com -All -ReportType xml -Path C:\temp\gporeport.xml 99 | ``` 100 | 101 | Easy. 102 | 103 | ### I don't trust you so I don't want to run your skeevy looking script on a domain-joined machine, but I want to try Grouper. 104 | 105 | All Grouper needs to work is PowerShell 2.0 and the xml file output from Get-GPOReport. You can run it on a VM with no network card if you're worried and it'll still work fine. 106 | 107 | That said, it's pretty basic code so it shouldn't be hard to see that it's not doing anything remotely sketchy. 108 | 109 | ### I think it's dumb that you are relying on the MS Group Policy cmdlets/RSAT for Grouper. You should just write it to directly query the domain or parse the policy files straight out of SYSVOL. 110 | 111 | Short answer: Yep. 112 | 113 | Long answer: Yep, doing one of those things would be better, but there are a couple of things that prevented me from doing them YET. 114 | 115 | Ideally I'd like to parse the policy files straight off SYSVOL, but they are stored in a bunch of different file formats, some are proprietary, they're a real pain to read, and I have neither the time nor the inclination to write a bunch of parsers for them from scratch when Microsoft already provide cmdlets that do the job very nicely. 116 | 117 | In the not-too-distant future I'd like to bake Microsoft's Get-GPOReport into Grouper, so you wouldn't need RSAT at all, but I need to figure out if that's going to be some kind of copyright violation. I also need to figure out how to actually do that thing I just said. 118 | ___ 119 | 120 | ## Questions that I am anticipating 121 | 122 | ### Grouper is showing me all these settings that aren't vulnerable. WTF BRO FALSE POSITIVE MUCH? 123 | 124 | Grouper is not a vulnerability scanner. Grouper merely filters the enormous amount of fluff and noise in Group Policy reports to show you only the policy settings that COULD be configured in exploitable ways. 125 | 126 | To the extent possible I am working through each of the categories of checks to add in some extra filtering to remove obviously non-vulnerable configurations and reduce the noise levels even further, but Group Policy is extremely flexible and it's pretty difficult to anticipate every possible mistake an admin might make. 127 | 128 | ### Grouper didn't show me a thing that I know is totally vulnerable in Group Policy. WTF BRO FALSE NEGATIVE MUCH? 129 | 130 | Cool, you just found a way to make Grouper better! Scroll down and you'll see where I've provided a little guide to adding new checks to Grouper. 131 | 132 | ### I don't have a lab environment and I don't have a GPO report file handy! I'm also very impatient! 133 | I got your back, kid. There's a test_report.xml in the repo that you can try it out with. It's got a bunch of bad settings in it so you can see what that looks like. 134 | 135 | You'll need to run it with the -showDisabled flag because it's so full of really awful configurations I didn't even want to enable the GPO in a lab environment. 136 | 137 | ### I'm even more impatient than that last guy and I demand pretty pictures immediately! 138 | OK. 139 | 140 | ![Screenshot of test output](./test_output.png) 141 | 142 | ### But wait, how do I figure out which users/computers these policies apply to? Your thing is useless! 143 | Short Answer: PowerView will do a decent job of this. 144 | 145 | Longer Answer: I'll be trying to add this functionality at some point but in the meantime, shut up and use PowerView. 146 | 147 | ### I hate one of the checks Grouper does and I never want to see it again. 148 | 149 | Cool, easily fixed. 150 | 151 | Pop open grouper.ps1, find the "$polchecks" array and just comment out the line where that check gets added to the array. 152 | 153 | Done. 154 | 155 | ### I want to make Grouper better but I can't make sense of your awful spaghetti-code. Help me help you. 156 | 157 | Sure thing, sounds good. 158 | 159 | 1. Get some GPOReport xml output that includes the type of policy/setting you want Grouper to be able to find. This may require knocking up a suitable policy in a lab environment. 160 | 161 | 2. Find the \ xml object that matches your target policy. 162 | 163 | 3. Find the subsection of the xml that matches the info you want to pull out of the policy. Policy settings are divided into either User or Computer policy, so this will usually be in either: 164 | ``` 165 | GPO.Computer.ExtensionData.Extension 166 | or 167 | GPO.User.ExtensionData.Extension 168 | ``` 169 | 170 | 4. Now's the annoying part - the reason this code is such a mess is that each policy setting section is structured differently and they use wildly differing naming conventions, so you're going to need to figure out how your target policy is structured. Good luck? 171 | 172 | 6. Here's a skeleton of a check function you can use to get started. Make sure it either doesn't return at all or returns $null if nothing interesting is found. 173 | 174 | ``` 175 | Function Get-GPOThing { 176 | [cmdletbinding()] 177 | Param ( 178 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] [System.Xml.XmlElement]$polXML, 179 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 180 | ) 181 | 182 | ###### 183 | # Description: Checks for Things. 184 | # Vulnerable: Description of what it shows if Level -eq 3 185 | # Interesting: Description of what it shows if Level -eq 2 186 | # Boring: All Things. 187 | ###### 188 | 189 | $settingsThings = ($polXml.Thing.ExtensionData.Extension.Thing | Sort-Object GPOSettingOrder) 190 | 191 | if ($settingsThings) { 192 | foreach ($setting in $settingsThings) { 193 | if ($level -eq 1) { 194 | $output = @{} 195 | $output.Add("Name", $setting.Name) 196 | if ($setting.SettingBoolean) { 197 | $output.Add("SettingBoolean", $setting.SettingBoolean) 198 | } 199 | if ($setting.SettingNumber) { 200 | $output.Add("SettingNumber", $setting.SettingNumber) 201 | } 202 | $output.Add("Type", $setting.Type.InnerText) 203 | Write-Output $output 204 | "" 205 | } 206 | } 207 | } 208 | } 209 | 210 | ``` 211 | 212 | 7. Ctrl-f your way down to "$polchecks" and add it to the array of checks with the others. 213 | 214 | 8. Test it out. 215 | 216 | 9. If it works, submit a pull request! 217 | 218 | 10. If you get stuck, hit me up. I'll try to help if I can scrounge a few minutes together. 219 | 220 | ___ 221 | 222 | ## Credits, complaints, comments, death threats, errata 223 | 224 | Thank you very much to: 225 | * @harmj0y for his GPP password decryption helper function. 226 | * @sysop_host and @prashant3535 for their assistance and encouragement. I believe there is probably still a line or two stolen from @sysop_host still in this thing but I'm really not sure where and I would hate to blame him for my shitty code. 227 | 228 | Speaking of shitty code, yes I know this is a bit of a mess. I've tried to make it as modular as possible so others should be able to add additional checks without too much hassle, but it still needs a lot of love. If you see a mistake I've made that desperately needs fixing, please let me know. 229 | -------------------------------------------------------------------------------- /grouper.psm1: -------------------------------------------------------------------------------- 1 | # Grouper - Get-GPOReport XML Parser 2 | 3 | <# 4 | .SYNOPSIS 5 | Consumes a Get-GPOReport XML formatted report and outputs potentially vulnerable settings. 6 | .DESCRIPTION 7 | GPP cpassword decryption function stolen shamelessly from @harmj0y 8 | Other small snippets and ideas stolen shamelessly from @sysop_host 9 | .EXAMPLE 10 | So first you need to generate a report on a machine with the Group Policy PS module installed. Do that like this: 11 | 12 | "Get-GPOReport -All -ReportType XML -Path C:\temp\gporeport.xml" 13 | 14 | Then import this module and: 15 | 16 | "Invoke-AuditGPOReport -Path C:\temp\gporeport.xml" 17 | 18 | -showDisabled or else by default we just filter out policy objects that aren't enabled or linked anywhere. 19 | 20 | -Level (1, 2, or 3) - adjusts whether to show everything (1) or only interesting (2) or only definitely vulnerable (3) settings. Defaults to 2. 21 | 22 | -lazyMode (without -Path) will run the initial generation of the GPOReport for you but will need to be running as a domain user on a domain-joined machine. 23 | .NOTES 24 | Author : Mike Loss - mike@mikeloss.net 25 | #> 26 | 27 | # Some arrays of group names that are 'interesting', either because they are canonically 'low-priv' and huge numbers of accounts will be in them 28 | # or because they are high-priv enough that members are very likely to have privileged access to a host, whole domain or a number of hosts. 29 | 30 | # TODO ADD TO THIS LIST 31 | $intPrivLocalGroups = @() 32 | $intPrivLocalGroups += "Administrators" 33 | $intPrivLocalGroups += "Backup Operators" 34 | $intPrivLocalGroups += "Hyper-V Administrators" 35 | $intPrivLocalGroups += "Power Users" 36 | $intPrivLocalGroups += "Print Operators" 37 | $intPrivLocalGroups += "Remote Desktop Users" 38 | $intPrivLocalGroups += "Remote Management Users" 39 | 40 | # TODO ADD TO THIS LIST? 41 | $intLowPrivDomGroups = @() 42 | $intLowPrivDomGroups += "Domain Users" 43 | $intLowPrivDomGroups += "Authenticated Users" 44 | $intLowPrivDomGroups += "Everyone" 45 | 46 | # TODO ADD TO THIS LIST? 47 | $intLowPrivLocalGroups = @() 48 | $intLowPrivLocalGroups += "Users" 49 | $intLowPrivLocalGroups += "Everyone" 50 | $intLowPrivLocalGroups += "Authenticated Users" 51 | 52 | # TODO ADD TO THIS LIST? 53 | $intLowPrivGroups = @() 54 | $intLowPrivGroups += "Domain Users" 55 | $intLowPrivGroups += "Authenticated Users" 56 | $intLowPrivGroups += "Everyone" 57 | $intLowPrivGroups += "Users" 58 | 59 | # TODO ADD TO THIS LIST 60 | $intPrivDomGroups = @() 61 | $intPrivDomGroups += "Domain Admins" 62 | $intPrivDomGroups += "Administrators" 63 | $intPrivDomGroups += "DNS Admins" 64 | $intPrivDomGroups += "Backup Operators" 65 | $intPrivDomGroups += "Enterprise Admins" 66 | $intPrivDomGroups += "Schema Admins" 67 | $intPrivDomGroups += "Server Operators" 68 | $intPrivDomGroups += "Account Operators" 69 | 70 | # THIS ONE IS FINE 71 | $intRights = @() 72 | $intRights += "SeTrustedCredManAccessPrivilege" 73 | $intRights += "SeTcbPrivilege" 74 | $intRights += "SeMachineAccountPrivilege" 75 | $intRights += "SeBackupPrivilege" 76 | $intRights += "SeCreateTokenPrivilege" 77 | $intRights += "SeAssignPrimaryTokenPrivilege" 78 | $intRights += "SeRestorePrivilege" 79 | $intRights += "SeDebugPrivilege" 80 | $intRights += "SeTakeOwnershipPrivilege" 81 | $intRights += "SeCreateGlobalPrivilege" 82 | $intRights += "SeLoadDriverPrivilege" 83 | $intRights += "SeRemoteInteractiveLogonRight" 84 | 85 | $boringTrustees = @() 86 | $boringTrustees += "BUILTIN\Administrators" 87 | $boringTrustees += "NT AUTHORITY\SYSTEM" 88 | 89 | #____________________ GPO Check functions _______________ 90 | 91 | # There's a whole pile of these functions. Each one consumes a single object from a Get-GPOReport XML report, 92 | # then depending on the -Level parameter it should output interesting/vulnerable/any policy it can process. 93 | # The rule of thumb for whether a check function should exist at all is "Does this class of policy have any possible settings with security impact?" 94 | # The rule of thumb for whether a setting is "Vulnerable" enough for Level 3 is "If it is likely to result in large numbers of users or the current user 95 | # being able to get a shell or an RDP session on a host". 96 | # The rule of thumb for whether a setting is "Interesting" enough for Level 2 is "if it could meet the criteria for Level 3 but Grouper can't tell 97 | # whether it does without user intervention." 98 | # At the moment Level 1 is pretty much just showing all the settings that Grouper can parse, but in the future it should filter out settings that 99 | # have been configured 'securely', where there is a clear best-practice option. 100 | 101 | Function Get-GPOUsers { 102 | [cmdletbinding()] 103 | # Consumes a single object from a Get-GPOReport XML report. 104 | 105 | ###### 106 | # Description: Checks for changes made to local users. 107 | # Level 3: Only show instances where a password has been set, i.e. GPP Passwords. 108 | # Level 2: All users and all changes. 109 | # Level 1: All users and all changes. 110 | ###### 111 | 112 | Param ( 113 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 114 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 115 | ) 116 | 117 | $GPOisinteresting = $false 118 | $GPOisvulnerable = $false 119 | 120 | # Grab an array of the settings we're interested in from the GPO. 121 | $settingsUsers = ($polXml.ExtensionData.Extension.LocalUsersAndGroups.User | Sort-Object GPOSettingOrder) 122 | 123 | # Check if there's actually anything in the array. 124 | if ($settingsUsers) { 125 | $output = @{} 126 | 127 | # Iterate over array of settings, writing out only those we care about. 128 | foreach ($setting in $settingsUsers) { 129 | 130 | #see if we have any stored encrypted passwords 131 | $cpasswordcrypt = $setting.properties.cpassword 132 | if ($cpasswordcrypt) { 133 | $GPOisvulnerable = $true 134 | 135 | # decrypt it with harmj0y's function 136 | $cpasswordclear = Get-DecryptedCpassword -Cpassword $cpasswordcrypt 137 | } 138 | #if so, or if we're showing boring, show the rest of the setting 139 | if (($cpasswordcrypt) -Or ($level -le 2)) { 140 | $GPOisinteresting = $true 141 | $output = @{} 142 | $output.Add("Name", $setting.Name) 143 | $output.Add("New Name", $setting.properties.NewName) 144 | $output.Add("Description", $setting.properties.Description) 145 | $output.Add("changeLogon", $setting.properties.changeLogon) 146 | $output.Add("noChange", $setting.properties.noChange) 147 | $output.Add("neverExpires", $setting.properties.neverExpires) 148 | $output.Add("Disabled", $setting.properties.acctDisabled) 149 | $output.Add("UserName", $setting.properties.userName) 150 | $output.Add("Password", $($cpasswordclear, "Password Not Set" -ne $null)[0]) 151 | Write-NoEmpties -output $output 152 | "`r`n" 153 | } 154 | } 155 | } 156 | 157 | if ($GPOisinteresting) { 158 | $Global:GPOsWithIntSettings += 1 159 | } 160 | if ($GPOisvulnerable) { 161 | $Global:GPOsWithVulnSettings += 1 162 | } 163 | } 164 | 165 | Function Get-GPOGroups { 166 | [cmdletbinding()] 167 | Param ( 168 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 169 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 170 | ) 171 | 172 | ###### 173 | # Description: Checks for changes made to local groups. 174 | # Level 3: If Domain Users, Everyone, Authenticated Users get added to 'interesting groups'. 175 | # Level 2: Show changes to groups that grant meaningful security-relevant access. 176 | # Level 1: All groups and all changes. 177 | ###### 178 | 179 | $GPOIsInteresting = $false 180 | $GPOIsVulnerable = $false 181 | 182 | $settingsGroups = ($polXml.ExtensionData.Extension.LocalUsersAndGroups.Group | Sort-Object GPOSettingOrder) 183 | 184 | if ($settingsGroups) { 185 | foreach ($setting in $settingsGroups) { 186 | $settingIsInteresting = $false 187 | $settingIsVulnerable = $false 188 | $groupIsInteresting = $false 189 | 190 | # check if the group being modified is one of the high-priv local groups array, 191 | $groupName = $setting.properties.groupName 192 | if ($intPrivLocalGroups -Contains $groupName) { 193 | $GPOIsInteresting = $true 194 | $settingIsInteresting = $true 195 | $groupIsInteresting = $true 196 | } 197 | 198 | # if it's in that array AND a member being modified is a low-priv domain group, we flag the setting as vulnerable. 199 | $groupmembers = $setting.properties.members.member 200 | foreach ($groupmember in $groupmembers) { 201 | $groupMemberName = $groupmember.name 202 | foreach ($lowPrivDomGroup in $intLowPrivDomGroups) { 203 | if (($groupMemberName -match $lowPrivDomGroup) -And ($groupIsInteresting)){ 204 | $settingIsVulnerable = $true 205 | $GPOIsVulnerable = $true 206 | } 207 | } 208 | } 209 | 210 | if ((($settingIsVulnerable) -And ($level -le 3)) -Or (($settingIsInteresting) -And ($level -le 2)) -Or ($level -eq 1)) { 211 | $output = @{} 212 | $output.Add("Name", $setting.Name) 213 | $output.Add("NewName", $setting.properties.NewName) 214 | $output.Add("Description", $setting.properties.Description) 215 | $output.Add("Group Name", $groupName) 216 | Write-NoEmpties -output $output 217 | 218 | foreach ($member in $setting.properties.members.member) { 219 | $output = @{} 220 | $output.Add("Name", $member.name) 221 | $output.Add("Action", $member.action) 222 | $output.Add("UserName", $member.userName) 223 | Write-NoEmpties -output $output 224 | } 225 | "`r`n" 226 | } 227 | } 228 | } 229 | 230 | if ($GPOisinteresting) { 231 | $Global:GPOsWithIntSettings += 1 232 | } 233 | if ($GPOIsVulnerable) { 234 | $Global:GPOsWithVulnSettings += 1 235 | } 236 | } 237 | 238 | Function Get-GPOUserRights { 239 | [cmdletbinding()] 240 | 241 | Param ( 242 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 243 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 244 | ) 245 | 246 | ###### 247 | # Description: Checks for user rights granted to users and groups. 248 | # Level 3: Only show "Interesting" rights, i.e. those that can be used for local privilege escalation or remote access, 249 | # and only if they've been assigned to Domain Users, Authenticated Users, or Everyone. 250 | # Level 2: Only show "Interesting" rights, i.e. those that can be used for local privilege escalation or remote access. 251 | # Level 1: All non-default. 252 | ###### 253 | 254 | $GPOIsInteresting = $false 255 | $GPOIsVulnerable = $false 256 | 257 | $uraSettings = ($polXml.Computer.ExtensionData.Extension.UserRightsAssignment) 258 | 259 | $uraSettings = ($uraSettings | ? {$_}) #Strips null elements from array - nfi why I was getting so many of these. 260 | 261 | if ($uraSettings) { 262 | foreach ($setting in $uraSettings) { 263 | $settingIsInteresting = $false 264 | $settingIsVulnerable = $false 265 | $rightIsInteresting = $false 266 | 267 | $userRight = $setting.Name 268 | 269 | $members = @() 270 | foreach ($member in $setting.Member) { 271 | $members += ($member.Name.Innertext) 272 | } 273 | 274 | # if the right being assigned is in our array of interesting rights, the setting is interesting. 275 | if ($intRights -contains $userRight) { 276 | $GPOisinteresting = $true 277 | $settingIsInteresting = $true 278 | $rightIsInteresting = $true 279 | } 280 | 281 | # then we construct an array of trustees being granted the right, so we can see if they are in any of our interesting low priv groups. 282 | if ($rightIsInteresting) { 283 | foreach ($lowPrivGroup in $intLowPrivGroups) { 284 | foreach ($member in $members) { 285 | if ($member -match $lowPrivGroup) { 286 | $GPOIsVulnerable = $true 287 | $settingIsVulnerable = $true 288 | } 289 | } 290 | } 291 | } 292 | 293 | if ((($settingIsVulnerable) -And ($level -le 3)) -Or (($settingIsInteresting) -And ($level -le 2)) -Or ($level -eq 1)) { 294 | $output = @{} 295 | $output.Add("Right", $userRight) 296 | $output.Add("Members", $members -join ',') 297 | Write-NoEmpties -output $output 298 | "`r`n" 299 | } 300 | } 301 | } 302 | 303 | if ($GPOisinteresting) { 304 | $Global:GPOsWithIntSettings += 1 305 | } 306 | 307 | if ($GPOIsVulnerable) { 308 | $Global:GPOsWithVulnSettings += 1 309 | } 310 | } 311 | 312 | Function Get-GPOSchedTasks { 313 | [cmdletbinding()] 314 | Param ( 315 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 316 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 317 | ) 318 | 319 | ###### 320 | # Description: Checks for scheduled tasks being configured on a host. 321 | # Level 3: Only show instances where a password has been set. 322 | # Level 2: TODO If a password has been set or the thing being run is non-local or there are arguments set. 323 | # Level 1: All scheduled tasks. 324 | ###### 325 | 326 | $GPOisinteresting = $false 327 | $GPOisvulnerable = $false 328 | 329 | $settingsSchedTasks = ($polXml.Computer.ExtensionData.Extension.ScheduledTasks.Task | Sort-Object GPOSettingOrder) 330 | 331 | if ($settingsSchedTasks) { 332 | foreach ($setting in $settingsSchedTasks) { 333 | #see if we have any stored encrypted passwords 334 | $cpasswordcrypt = $setting.properties.cpassword 335 | if ($cpasswordcrypt) { 336 | $GPOisvulnerable = $true 337 | $GPOisinteresting = $true 338 | 339 | # decrypt it with harmj0y's function 340 | $cpasswordclear = Get-DecryptedCpassword -Cpassword $cpasswordcrypt 341 | } 342 | #see if any arguments have been set 343 | $taskArgs = $setting.Properties.args 344 | if ($taskArgs) { 345 | $GPOisinteresting = $true 346 | } 347 | 348 | #if so, or if we're showing everything, or if there are args and we're at level 2, show the setting. 349 | if ((($cpasswordcrypt) -And ($level -le 3)) -Or (($taskArgs) -And ($level -le 2)) -Or ($level -eq 1)) { 350 | $output = @{} 351 | $output.Add("Name", $setting.Properties.name) 352 | $output.Add("runAs", $setting.Properties.runAs) 353 | $output.Add("Password", $($cpasswordclear, "Password Not Set" -ne $null)[0]) 354 | $output.Add("Action", $setting.Properties.action) 355 | $output.Add("appName", $setting.Properties.appName) 356 | $output.Add("args", $setting.Properties.args) 357 | $output.Add("startIn", $setting.Properties.startIn) 358 | Write-NoEmpties -output $output 359 | 360 | if ($setting.Properties.Triggers) { 361 | foreach ($trigger in $setting.Properties.Triggers) { 362 | $output = @{} 363 | $output.Add("type", $trigger.Trigger.type) 364 | $output.Add("startHour", $trigger.Trigger.startHour) 365 | $output.Add("startMinutes", $trigger.Trigger.startMinutes) 366 | Write-NoEmpties -output $output 367 | "`r`n" 368 | } 369 | } 370 | } 371 | } 372 | } 373 | 374 | if ($GPOisinteresting) { 375 | $Global:GPOsWithIntSettings += 1 376 | } 377 | 378 | if ($GPOisvulnerable) { 379 | $Global:GPOsWithVulnSettings += 1 380 | } 381 | } 382 | 383 | Function Get-GPOMSIInstallation { 384 | [cmdletbinding()] 385 | Param ( 386 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 387 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 388 | ) 389 | 390 | ###### 391 | # Description: Checks for MSI installers being used to install software. 392 | # Level 3: TODO Only show instances where the file is writable by the current user or 'Everyone' or 'Domain Users' or 'Authenticated Users'. 393 | # Level 2: All MSI installations. 394 | # Level 1: All MSI installations. 395 | ###### 396 | 397 | $MSIInstallation = ($polXml.ExtensionData.Extension.MsiApplication | Sort-Object GPOSettingOrder) 398 | 399 | if ($MSIInstallation) { 400 | $GPOisinteresting = $true 401 | $GPOisvulnerable = $false 402 | 403 | foreach ($setting in $MSIInstallation) { 404 | $output = @{} 405 | $MSIPath = $setting.Path 406 | $output.Add("Name", $setting.Name) 407 | $output.Add("Path", $MSIPath) 408 | 409 | if ($Global:onlineChecks) { 410 | if ($MSIPath.StartsWith("\\")) { 411 | $ACLData = Find-IntACL -Path $MSIPath 412 | $output.Add("Owner",$ACLData["Owner"]) 413 | if ($ACLData["Vulnerable"] -eq "True") { 414 | $settingIsVulnerable = $true 415 | $GPOisvulnerable = $true 416 | $output.Add("[!]", "Source file writable by current user!") 417 | } 418 | $MSIPathAccess = $ACLData["Trustees"] 419 | } 420 | } 421 | 422 | if (($level -le 2) -Or (($level -le 3) -And ($settingisVulnerable))) { 423 | Write-NoEmpties -output $output 424 | "" 425 | if ($MSIPathAccess) { 426 | Write-Title -Text "Permissions on source file:" -DividerChar "-" 427 | Write-Output $MSIPathAccess 428 | "" 429 | } 430 | } 431 | } 432 | } 433 | 434 | if ($GPOisinteresting) { 435 | $Global:GPOsWithIntSettings += 1 436 | } 437 | if ($GPOisvulnerable) { 438 | $Global:GPOsWithVulnSettings += 1 439 | } 440 | } 441 | 442 | Function Get-GPOScripts { 443 | [cmdletbinding()] 444 | Param ( 445 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 446 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 447 | ) 448 | 449 | ###### 450 | # Description: Checks for startup/shutdown/logon/logoff scripts. 451 | # Level 3: TODO Only show instances where the file is writable by the current user or 'Everyone' or 'Domain Users' or 'Authenticated Users'. 452 | # Level 2: All scripts. 453 | # Level 1: All scripts. 454 | ###### 455 | 456 | $settingsScripts = ($polXml.ExtensionData.Extension.Script | Sort-Object GPOSettingOrder) 457 | 458 | if ($settingsScripts) { 459 | $GPOisinteresting = $true 460 | $GPOisvulnerable = $false 461 | 462 | foreach ($setting in $settingsScripts) { 463 | $commandPath = $setting.Command 464 | $output = @{} 465 | $output.Add("Command", $commandPath) 466 | $output.Add("Type", $setting.Type) 467 | $output.Add("Parameters", $setting.Parameters) 468 | $settingIsVulnerable = $false 469 | 470 | if ($Global:onlineChecks) { 471 | if ($commandPath.StartsWith("\\")) { 472 | $ACLData = Find-IntACL -Path $commandPath 473 | $output.Add("Owner",$ACLData["Owner"]) 474 | if ($ACLData["Vulnerable"] -eq "True") { 475 | $settingIsVulnerable = $true 476 | $GPOisvulnerable = $true 477 | $output.Add("[!]", "Source file writable by current user!") 478 | } 479 | $commandPathAccess = $ACLData["Trustees"] 480 | } 481 | } 482 | 483 | if (($level -le 2) -Or (($level -le 3) -And ($settingisVulnerable))) { 484 | Write-NoEmpties -output $output 485 | "" 486 | if ($commandPathAccess) { 487 | Write-Title -Text "Permissions on source file:" -DividerChar "-" 488 | Write-Output $commandPathAccess 489 | "" 490 | } 491 | } 492 | } 493 | } 494 | 495 | if ($GPOisinteresting) { 496 | $Global:GPOsWithIntSettings += 1 497 | } 498 | if ($GPOisvulnerable) { 499 | $Global:GPOsWithVulnSettings += 1 500 | } 501 | 502 | } 503 | 504 | Function Get-GPOFileUpdate { 505 | [cmdletbinding()] 506 | Param ( 507 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 508 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 509 | ) 510 | 511 | ###### 512 | # Description: Checks for files being copied/updated/whatever. 513 | # Level 3: TODO Only show instances where the 'fromPath' file is writable by the current user or 'Everyone' or 'Domain Users' or 'Authenticated Users'. 514 | # Level 2: All File Updates where FromPath is a network share 515 | # Level 1: All File Updates. 516 | ###### 517 | 518 | $settingsFiles = ($polXml.ExtensionData.Extension.FilesSettings | Sort-Object GPOSettingOrder) 519 | 520 | if ($settingsFiles) { 521 | $GPOisinteresting = $true 522 | $GPOisvulnerable = $false 523 | foreach ($setting in $settingsFiles.File) { 524 | $fromPath = $setting.Properties.fromPath 525 | $targetPath = $setting.Properties.targetPath 526 | $output = @{} 527 | $output.Add("Name", $setting.name) 528 | $output.Add("Action", $setting.Properties.action) 529 | $output.Add("fromPath", $fromPath) 530 | $output.Add("targetPath", $targetPath) 531 | $settingIsVulnerable = $false 532 | 533 | if ($Global:onlineChecks) { 534 | if ($fromPath.StartsWith("\\")) { 535 | $ACLData = Find-IntACL -Path $fromPath 536 | $output.Add("Owner",$ACLData["Owner"]) 537 | if ($ACLData["Vulnerable"] -eq "True") { 538 | $settingIsVulnerable = $true 539 | $GPOisvulnerable = $true 540 | $output.Add("[!]", "Source file writable by current user!") 541 | } 542 | $fromPathAccess = $ACLData["Trustees"] 543 | } 544 | } 545 | 546 | if (($level -le 2) -Or (($level -le 3) -And ($settingisVulnerable))) { 547 | Write-NoEmpties -output $output 548 | "" 549 | if ($fromPathAccess) { 550 | Write-Title -Text "Permissions on source file:" -DividerChar "-" 551 | Write-Output $fromPathAccess 552 | "" 553 | } 554 | } 555 | } 556 | } 557 | if ($GPOisinteresting) { 558 | $Global:GPOsWithIntSettings += 1 559 | } 560 | if ($GPOisvulnerable) { 561 | $Global:GPOsWithVulnSettings += 1 562 | } 563 | 564 | } 565 | 566 | Function Get-GPOFilePerms { 567 | [cmdletbinding()] 568 | Param ( 569 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 570 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 571 | ) 572 | 573 | ###### 574 | # Description: Checks for changes to local file permissions. 575 | # Level 3: TODO Only show instances where the file is writable by the current user or 'Everyone' or 'Domain Users' or 'Authenticated Users'. 576 | # Level 2: TODO Also show instances where any user/group other than the usual default Domain/Enterprise Admins has 'Full Control'. 577 | # Level 1: All file permission changes. 578 | ###### 579 | 580 | $settingsFilePerms = ($polXml.Computer.ExtensionData.Extension.File | Sort-Object GPOSettingOrder) 581 | 582 | if ($settingsFilePerms) { 583 | foreach ($setting in $settingsFilePerms) { 584 | if ($level -eq 1) { 585 | $output = @{} 586 | $output.Add("Path", $setting.Path) 587 | $output.Add("SDDL", $setting.SecurityDescriptor.SDDL.innertext) 588 | Write-NoEmpties -output $output 589 | "`r`n" 590 | } 591 | } 592 | } 593 | } 594 | 595 | Function Get-GPOSecurityOptions { 596 | [cmdletbinding()] 597 | Param ( 598 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 599 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 600 | ) 601 | 602 | ###### 603 | # Description: Checks for potentially vulnerable "Security Options" settings. 604 | # Level 3: TODO. 605 | # Level 2: Show everything that matches $intKeyNames or $intSysAccPolName. 606 | # Level 1: All settings. 607 | ###### 608 | 609 | $GPOisinteresting = $false 610 | $settingsSecurityOptions = ($polXml.Computer.ExtensionData.Extension.SecurityOptions | Sort-Object GPOSettingOrder) 611 | 612 | if ($settingsSecurityOptions) { 613 | if ($level -le 2) { 614 | $intKeyNameBools = @{} 615 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\DisableDomainCreds", "false") 616 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\EveryoneIncludesAnonymous", "true") 617 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\LimitBlankPasswordUse", "false") 618 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\NoLMHash", "false") 619 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\RestrictAnonymous", "false") 620 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\RestrictAnonymousSAM", "false") 621 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl", "true") 622 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Lsa\UseMachineId", "true") 623 | $intKeyNameBools.Add("MACHINE\System\CurrentControlSet\Control\Print\Providers\LanMan Print Services\Servers\AddPrinterDrivers", "false") 624 | 625 | $intKeyNameLists = @() 626 | $intKeyNameLists += "MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedExactPaths\Machine" 627 | $intKeyNameLists += "MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedPaths\Machine" 628 | $intKeyNameLists += "MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\NullSessionPipes" 629 | $intKeyNameLists += "MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\NullSessionShares" 630 | $intKeyNameLists += "MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\RestrictNullSessAccess" 631 | 632 | $intSysAccPolBools = @{} 633 | $intSysAccPolBools.Add("EnableGuestAccount", 1) 634 | $intSysAccPolBools.Add("EnableAdminAccount", 1) 635 | $intSysAccPolBools.Add("LSAAnonymousNameLookup", 1) 636 | 637 | $intSysAccPolStrings = @{} 638 | $intSysAccPolStrings.Add("NewAdministratorName", "") 639 | $intSysAccPolStrings.Add("NewGuestName", "") 640 | 641 | foreach ($setting in $settingsSecurityOptions) { 642 | #Check if it's a registry based option 643 | if ($setting.KeyName) { 644 | $keyname = $setting.KeyName 645 | $output = @{} 646 | $values = @{} 647 | $foundit = 0 648 | if ($foundit -eq 0) { 649 | if ($intKeyNameLists -contains $keyname) { 650 | $GPOisinteresting = $true 651 | $foundit = 1 652 | $output.Add("Name", $setting.Display.Name) 653 | $output.Add("KeyName", $setting.KeyName) 654 | $dispstrings = $setting.Display.DisplayStrings.Value 655 | #here we have to iterate over the list of values 656 | $i = 0 657 | foreach ($dispstring in $dispstrings) { 658 | $values.Add("Path/Pipe$i", $dispstring) 659 | $i += 1 660 | } 661 | Write-NoEmpties -output $output 662 | Write-NoEmpties -output $values 663 | "`r`n" 664 | } 665 | } 666 | if ($foundit -eq 0) { 667 | foreach ($intKeyNameBool in $intKeyNamesBools) { 668 | if (($keyNameBool.ContainsKey($keyname)) -And ($keyNameBool.ContainsValue($setting.Display.DisplayBoolean))) { 669 | $GPOIsInteresting =1 670 | $foundit = 1 671 | $output.Add("Name", $setting.Display.Name) 672 | $output.Add("KeyName", $setting.KeyName) 673 | $values.Add("DisplayBoolean", $setting.Display.Displayboolean) 674 | Write-NoEmpties -output $output 675 | Write-NoEmpties -output $values 676 | "`r`n" 677 | } 678 | } 679 | } 680 | } 681 | # or a 'system access policy name' 682 | elseif ($setting.SystemAccessPolicyName) { 683 | $output = @{} 684 | foreach ($SAP in $intSysAccPolBools) { 685 | if (($SAP.ContainsKey($setting.SystemAccessPolicyName)) -And ($SAP.ContainsValue($setting.SettingNumber))) { 686 | $output.Add("Name", $setting.SystemAccessPolicyName) 687 | $output.Add("SettingNumber",$setting.SettingNumber) 688 | $GPOisinteresting = $true 689 | Write-NoEmpties -output $output 690 | "`r`n" 691 | } 692 | } 693 | foreach ($SAP in $intSysAccPolStrings) { 694 | if ($SAP.ContainsKey($setting.SystemAccessPolicyName)) { 695 | $output.Add("Name", $setting.SystemAccessPolicyName) 696 | $output.Add("SettingString",$setting.SettingString) 697 | $GPOisinteresting = $true 698 | Write-NoEmpties -output $output 699 | "`r`n" 700 | } 701 | } 702 | } 703 | } 704 | } 705 | } 706 | 707 | if ($GPOisinteresting) { 708 | $Global:GPOsWithIntSettings += 1 709 | } 710 | } 711 | 712 | Function Get-GPORegKeys { 713 | [cmdletbinding()] 714 | Param ( 715 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 716 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 717 | ) 718 | 719 | ###### 720 | # Description: Checks for registry keys being set that may contain sensitive information. 721 | # Level 3: Any key that matches '$intKeys'. 722 | # Level 2: TODO Also show instances containing the strings 'pass', 'pwd', 'cred', or 'vnc'. 723 | # Level 1: All Registry Keys 724 | ###### 725 | 726 | $GPOisinteresting = $false 727 | $GPOisvulnerable = $false 728 | 729 | $settingsRegKeys = ($polXml.ExtensionData.Extension.RegistrySettings.Registry | Sort-Object GPOSettingOrder) 730 | 731 | $vulnKeys = @() 732 | $vulnKeys += "Software\Network Associates\ePolicy Orchestrator" 733 | $vulnKeys += "SOFTWARE\FileZilla Server" 734 | $vulnKeys += "SOFTWARE\Wow6432Node\FileZilla Server" 735 | $vulnKeys += "Software\Wow6432Node\McAfee\DesktopProtection - McAfee VSE" 736 | $vulnKeys += "Software\McAfee\DesktopProtection - McAfee VSE" 737 | $vulnKeys += "Software\ORL\WinVNC3" 738 | $vulnKeys += "Software\ORL\WinVNC3\Default" 739 | $vulnKeys += "Software\ORL\WinVNC\Default" 740 | $vulnKeys += "Software\RealVNC\WinVNC4" 741 | $vulnKeys += "Software\RealVNC\Default" 742 | $vulnKeys += "Software\TightVNC\Server" 743 | $vulnKeys += "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" 744 | 745 | $intWords = @() 746 | $intWords += "vnc" 747 | $intWords += "vpn" 748 | $intWords += "pwd" 749 | $intWords += "cred" 750 | $intWords += "key" 751 | $intWords += "pass" 752 | 753 | 754 | if ($settingsRegKeys) { 755 | foreach ($setting in $settingsRegKeys) { 756 | $settingkey = $setting.Properties.key 757 | $settingisInteresting = $false 758 | $settingIsVulnerable = $false 759 | 760 | if ($vulnKeys -Contains $settingkey) { 761 | $GPOisvulnerable = $true 762 | $settingIsVulnerable = $true 763 | } 764 | 765 | foreach ($intWord in $intWords) { 766 | # if either key or value include our interesting words as a substring, mark the setting as interesting 767 | if (($settingkey -match $intWord) -Or ($settingValue -match $intWord)) { 768 | $GPOisinteresting = $true 769 | $settingisInteresting = $true 770 | } 771 | } 772 | 773 | # if setting matches any of our criteria for printing (combined interest level + output level) 774 | if ((($settingisVulnerable) -And ($level -le 3)) -Or (($settingisInteresting) -And ($level -le 2)) -Or ($level -eq 1)) { 775 | $output = @{} 776 | $output.Add("Key", $settingkey) 777 | $output.Add("Action", $setting.Properties.action) 778 | $output.Add("Hive", $setting.Properties.hive) 779 | $output.Add("Name", $setting.Properties.name) 780 | $output.Add("Value", $setting.Properties.value) 781 | Write-NoEmpties -output $output 782 | "`r`n" 783 | } 784 | } 785 | } 786 | 787 | # update the global counters 788 | if ($GPOisivulnerable) { 789 | $Global:GPOsWithVulnSettings += 1 790 | } 791 | 792 | if ($GPOisinteresting) { 793 | $Global:GPOsWithIntSettings += 1 794 | } 795 | 796 | } 797 | 798 | Function Get-GPOFolderRedirection { 799 | [cmdletbinding()] 800 | Param ( 801 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 802 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 803 | ) 804 | 805 | ###### 806 | # Description: Checks for user Folder redirections. 807 | # Level 3: TODO Only show instances where DestPath is writable by the current user or 'Everyone' or 'Domain Users' or 'Authenticated Users'. 808 | # Level 2: TODO Also show instances where any user/group other than the usual default Domain/Enterprise Admins has 'Full Control'. 809 | # Level 1: All Folder Redirection. 810 | ###### 811 | 812 | $settingsFolderRedirection = ($polXml.User.ExtensionData.Extension.Folder | Sort-Object GPOSettingOrder) 813 | 814 | if ($settingsFolderRedirection) { 815 | foreach ($setting in $settingsFolderRedirection) { 816 | if ($level -eq 1) { 817 | $output = @{} 818 | $output.Add("DestPath", $setting.Location.DestinationPath) 819 | $output.Add("Target Group", $setting.Location.SecurityGroup.Name.innertext) 820 | $output.Add("Target SID", $setting.Location.SecurityGroup.SID.innertext) 821 | $output.Add("ID", $setting.Id) 822 | Write-NoEmpties -output $output 823 | "`r`n" 824 | } 825 | } 826 | } 827 | } 828 | 829 | Function Get-GPOAccountSettings { 830 | [cmdletbinding()] 831 | Param ( 832 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 833 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 834 | ) 835 | 836 | ###### 837 | # Description: Checks for Account Settings. 838 | # Level 3: TODO 839 | # Level 2: If it matches our list of interesting settings - undecided if i want to include weak password policy here. 840 | # Level 1: All Account Settings. 841 | ###### 842 | 843 | $settingsAccount = ($polXml.Computer.ExtensionData.Extension.Account | Sort-Object GPOSettingOrder) 844 | 845 | $GPOisinteresting = $false 846 | 847 | $intAccSettingBools = @{} 848 | $intAccSettingBools.Add("ClearTextPassword","true") 849 | 850 | if ($settingsAccount) { 851 | foreach ($setting in $settingsAccount) { 852 | $settingName = $setting.Name 853 | $settingisInteresting = $false 854 | 855 | foreach ($intAccSetting in $intAccSettingBools) { 856 | if (($intAccSetting.ContainsKey($settingName)) -And ($intAccSetting.containsValue($setting.SettingBoolean))) { 857 | $settingisInteresting = $true 858 | $GPOisinteresting = $true 859 | } 860 | } 861 | 862 | if (($level -eq 1) -Or (($settingisInteresting) -And ($level -le 2))) { 863 | $output = @{} 864 | $output.Add("Name", $settingName) 865 | if ($setting.SettingBoolean) { 866 | $output.Add("SettingBoolean", $setting.SettingBoolean) 867 | } 868 | if ($setting.SettingNumber) { 869 | $output.Add("SettingNumber", $setting.SettingNumber) 870 | } 871 | $output.Add("Type", $setting.Type) 872 | Write-NoEmpties -output $output 873 | "`r`n" 874 | } 875 | } 876 | } 877 | 878 | # update the global counters 879 | if ($GPOisinteresting) { 880 | $Global:GPOsWithIntSettings += 1 881 | } 882 | } 883 | 884 | Function Get-GPOFolders { 885 | [cmdletbinding()] 886 | Param ( 887 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 888 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 889 | ) 890 | 891 | ###### 892 | # Description: Checks for creation/renaming of local folders 893 | # Level 3: TODO 894 | # Level 2: TODO Need to generate a list of 'interesting' settings. 895 | # Level 1: All folders changes. 896 | ###### 897 | 898 | $settingsFolders = ($polXml.ExtensionData.Extension.Folders.Folder | Sort-Object GPOSettingOrder) 899 | 900 | if ($settingsFolders) { 901 | foreach ($setting in $settingsFolders) { 902 | if ($level -eq 1) { 903 | $output = @{} 904 | $output.Add("Name", $setting.name) 905 | $output.Add("Action", $setting.Properties.action) 906 | $output.Add("Path", $setting.Properties.path) 907 | Write-NoEmpties -output $output 908 | "`r`n" 909 | } 910 | } 911 | } 912 | } 913 | 914 | Function Get-GPONetworkShares { 915 | [cmdletbinding()] 916 | Param ( 917 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 918 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 919 | ) 920 | 921 | ###### 922 | # Description: Checks for Network Shares being created on hosts. 923 | # Level 3: TODO 924 | # Level 2: All Network Shares. 925 | # Level 1: All Network Shares. 926 | ###### 927 | 928 | $GPOisinteresting = $false 929 | 930 | $settingsNetShares = ($polXml.Computer.ExtensionData.Extension.NetworkShares.Netshare | Sort-Object GPOSettingOrder) 931 | 932 | if ($settingsNetShares) { 933 | foreach ($setting in $settingsNetShares) { 934 | if ($level -le 2) { 935 | $GPOisinteresting = $true 936 | $output = @{} 937 | $output.Add("Name", $setting.name) 938 | $output.Add("Action", $setting.Properties.action) 939 | $output.Add("PropName", $setting.Properties.name) 940 | $output.Add("Path", $setting.Properties.path) 941 | $output.Add("Comment", $setting.Properties.comment) 942 | Write-NoEmpties -output $output 943 | "`r`n" 944 | } 945 | } 946 | } 947 | 948 | if ($GPOisinteresting) { 949 | $Global:GPOsWithIntSettings += 1 950 | } 951 | 952 | } 953 | 954 | Function Get-GPOIniFiles { 955 | [cmdletbinding()] 956 | Param ( 957 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 958 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 959 | ) 960 | 961 | ###### 962 | # Description: Checks for changes to .INI files. 963 | # Level 3: TODO 964 | # Level 2: TODO Need to generate a list of 'interesting' settings. 965 | # Level 1: All .INI file changes. 966 | ###### 967 | 968 | $settingsIniFiles = ($polXml.ExtensionData.Extension.IniFiles.Ini | Sort-Object GPOSettingOrder) 969 | 970 | if ($settingsIniFiles) { 971 | 972 | foreach ($setting in $settingsIniFiles) { 973 | if ($level -eq 1) { 974 | $output = @{} 975 | $output.Add("Name", $setting.name) 976 | $output.Add("Path", $setting.Properties.path) 977 | $output.Add("Section", $setting.Properties.section) 978 | $output.Add("Value", $setting.Properties.value) 979 | $output.Add("Property", $setting.Properties.property) 980 | $output.Add("Action", $setting.Properties.action) 981 | Write-NoEmpties -output $output 982 | "`r`n" 983 | } 984 | } 985 | } 986 | } 987 | 988 | Function Get-GPOEnvVars { 989 | [cmdletbinding()] 990 | Param ( 991 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 992 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 993 | ) 994 | 995 | ###### 996 | # Description: Checks for environment variables being set. 997 | # Level 3: TODO 998 | # Level 2: TODO Need to generate a list of 'interesting' settings. 999 | # Level 1: All environment variables. 1000 | ###### 1001 | 1002 | $settingsEnvVars = ($polXml.ExtensionData.Extension.EnvironmentVariables.EnvironmentVariable | Sort-Object GPOSettingOrder) 1003 | 1004 | if ($settingsEnvVars) { 1005 | foreach ($setting in $settingsEnvvars) { 1006 | if ($level -eq 1) { 1007 | $output = @{} 1008 | $output.Add("Name", $setting.name) 1009 | $output.Add("Status", $setting.status) 1010 | $output.Add("Value", $setting.properties.value) 1011 | $output.Add("Action", $setting.properties.action) 1012 | Write-NoEmpties -output $output 1013 | "`r`n" 1014 | } 1015 | } 1016 | } 1017 | } 1018 | 1019 | Function Get-GPORegSettings { 1020 | [cmdletbinding()] 1021 | Param ( 1022 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 1023 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 1024 | ) 1025 | 1026 | ###### 1027 | # Description: Checks for "Registry Settings" i.e. a bunch of Windows options that are defined via the registry. 1028 | # Level 3: TBD. 1029 | # Level 2: Need to generate a list of 'interesting' settings. 1030 | # Level 1: All Registry Settings. 1031 | ###### 1032 | 1033 | $settingsRegSettings = ($polXml.ExtensionData.Extension.Policy | Sort-Object GPOSettingOrder) 1034 | 1035 | if ($settingsRegSettings) { 1036 | 1037 | $intRegSettings = @() 1038 | $intRegSettings += "Allow CredSSP authentication" 1039 | $intRegSettings += "Allow Basic Authentication" 1040 | $intRegSettings += "Set the default source path for Update-Help" 1041 | $intRegSettings += "Default Source Path" 1042 | $intRegSettings += "Allow remote server management through WinRM" 1043 | $intRegSettings += "Specify intranet Microsoft update service location" 1044 | $intRegSettings += "Set the intranet update service for detecting updates:" 1045 | $intRegSettings += "Set the intranet statistics server:" 1046 | $intRegSettings += "Allow Remote Shell Access" 1047 | $intRegSettings += "Allow unencrypted traffic" 1048 | $intRegSettings += "Sign-in last interactive user automatically after a system-initiated restart" 1049 | $intRegSettings += "Intranet proxy servers for apps" 1050 | $intRegSettings += "Type a proxy server IP address for the intranet" 1051 | $intRegSettings += "Internet proxy servers for apps" 1052 | $intRegSettings += "Domain Proxies" 1053 | $intRegSettings += "Restrict Unauthenticated RPC clients" 1054 | $intRegSettings += "RPC Runtime Unauthenticated Client Restriction to Apply" 1055 | $intRegSettings += "Enable RPC Endpoint Mapper Client Authentication" 1056 | $intRegSettings += "Always install with elevated privileges" 1057 | $intRegSettings += "Specify communities" 1058 | $intRegSettings += "Communities" 1059 | $intRegSettings += "Allow non-administrators to install drivers for these device setup classes" 1060 | $intRegSettings += "Allow Users to install device drivers for these classes:" 1061 | 1062 | $vulnRegSettings = @() 1063 | $vulnRegSettings += "Always install with elevated privileges" 1064 | $vulnRegSettings += "Specify communities" 1065 | $vulnRegSettings += "Communities" 1066 | $vulnRegSettings += "Allow non-administrators to install drivers for these device setup classes" 1067 | $vulnRegSettings += "Allow Users to install device drivers for these classes:" 1068 | 1069 | 1070 | # I hate this nested looping shit more than anything I've ever written. 1071 | foreach ($setting in $settingsRegSettings) { 1072 | if ($true) { 1073 | $output = @{} 1074 | $output.Add("Setting Name", $setting.Name) 1075 | $output.Add("State", $setting.State) 1076 | $output.Add("Supported", $setting.Supported) 1077 | $output.Add("Category", $setting.Category) 1078 | $output.Add("Explain", $setting.Explain) 1079 | 1080 | if (($level -eq 1) -Or (($level -eq 2) -And ($intRegSettings -Contains $setting.Name)) -Or (($level -eq 3) -And ($vulnRegSettings -Contains $setting.Name))) { 1081 | Write-NoEmpties -output $output 1082 | 1083 | foreach ($thing in $setting.EditText) { 1084 | $output = @{} 1085 | $output.Add("Name", $thing.Name) 1086 | $output.Add("Value", $thing.Value) 1087 | $output.Add("State", $thing.State) 1088 | Write-NoEmpties -output $output 1089 | } 1090 | 1091 | foreach ($thing in $setting.DropDownList) { 1092 | $output = @{} 1093 | $output.Add("Name", $thing.Name) 1094 | $output.Add("Value", $thing.Value.Name) 1095 | $output.Add("State", $thing.State) 1096 | Write-NoEmpties -output $output 1097 | } 1098 | 1099 | foreach ($thing in $setting.ListBox) { 1100 | $output = @{} 1101 | $output.Add("Name", $thing.Name) 1102 | $output.Add("ExplicitValue", $thing.ExplicitValue) 1103 | $output.Add("State", $thing.State) 1104 | $output.Add("Additive", $thing.Additive) 1105 | $output.Add("ValuePrefix", $thing.ValuePrefix) 1106 | $data = @() 1107 | foreach ($subthing in $thing.Value) { 1108 | foreach ($subsubthing in $subthing.Element) { 1109 | $data += $subsubthing.Data 1110 | } 1111 | } 1112 | $output.Add("Data", $data) 1113 | Write-NoEmpties -output $output 1114 | } 1115 | 1116 | foreach ($thing in $setting.Checkbox) { 1117 | $output = @{} 1118 | $output.Add("Value", $thing.Name) 1119 | $output.Add("State", $thing.State) 1120 | Write-NoEmpties -output $output 1121 | } 1122 | 1123 | foreach ($thing in $setting.Numeric) { 1124 | $output = @{} 1125 | $output.Add("Name", $thing.Name) 1126 | $output.Add("Value", $thing.Value) 1127 | $output.Add("State", $thing.State) 1128 | Write-NoEmpties -output $output 1129 | } 1130 | Write-Output "`r`n" 1131 | } 1132 | } 1133 | } 1134 | } 1135 | } 1136 | 1137 | Function Get-GPOShortcuts { 1138 | [cmdletbinding()] 1139 | # Consumes a single object from a Get-GPOReport XML report. 1140 | 1141 | ###### 1142 | # Description: Checks for changes made to shortcuts or new shortcuts added. 1143 | # Level 3: Only show instances where current user can write to target of shortcut. 1144 | # Level 2: All shortcut settings that reference a network path. 1145 | # Level 1: All shortcut settings. 1146 | ###### 1147 | 1148 | Param ( 1149 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][System.Xml.XmlElement]$polXML, 1150 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 1151 | ) 1152 | 1153 | # Grab an array of the settings we're interested in from the GPO. 1154 | $settingsShortcuts = ($polXml.ExtensionData.Extension.ShortcutSettings.Shortcut | Sort-Object GPOSettingOrder) 1155 | # Check if there's actually anything in the array. 1156 | if ($settingsShortcuts) { 1157 | $GPOisinteresting = $false 1158 | $GPOisvulnerable = $false 1159 | # Iterate over array of settings, writing out only those we care about. 1160 | foreach ($setting in $settingsShortcuts) { 1161 | $settingisInteresting = $false 1162 | $targetPath = $setting.properties.targetPath 1163 | $output = @{} 1164 | $output.Add("Name", $setting.name) 1165 | $output.Add("Status", $setting.status) 1166 | $output.Add("targetType", $setting.properties.targetType) 1167 | $output.Add("Action", $setting.properties.Action) 1168 | $output.Add("comment", $setting.properties.comment) 1169 | $output.Add("startIn", $setting.properties.startIn) 1170 | $output.Add("arguments", $setting.properties.arguments) 1171 | $output.Add("targetPath", $setting.properties.targetPath) 1172 | $output.Add("iconPath", $setting.properties.iconPath) 1173 | $output.Add("shortcutPath", $setting.properties.shortcutPath) 1174 | if ($Global:onlineChecks) { 1175 | if ($targetPath.StartsWith("\\")) { 1176 | $settingisInteresting = $true 1177 | $GPOisinteresting = $true 1178 | $ACLData = Find-IntACL -Path $targetPath 1179 | $output.Add("Owner",$ACLData["Owner"]) 1180 | if ($ACLData["Vulnerable"] -eq "True") { 1181 | $settingIsVulnerable = $true 1182 | $GPOisvulnerable = $true 1183 | $output.Add("[!]", "Source file writable by current user!") 1184 | } 1185 | $targetPathAccess = $ACLData["Trustees"] 1186 | } 1187 | } 1188 | 1189 | if (($level -eq 1) -Or (($level -le 2) -And ($settingisInteresting)) -Or (($level -le 3) -And ($settingisVulnerable))) { 1190 | Write-NoEmpties -output $output 1191 | "" 1192 | if ($targetPathAccess) { 1193 | Write-Title -Text "Permissions on source file:" -DividerChar "-" 1194 | Write-Output $targetPathAccess 1195 | "`r`n" 1196 | } 1197 | } 1198 | } 1199 | } 1200 | 1201 | if ($GPOisinteresting) { 1202 | $Global:GPOsWithIntSettings += 1 1203 | } 1204 | if ($GPOisvulnerable) { 1205 | $Global:GPOsWithVulnSettings += 1 1206 | } 1207 | } 1208 | 1209 | ################################# 1210 | # 1211 | # 1212 | # 1213 | # 1214 | # Here endeth the gross GPO check functions! 1215 | # 1216 | # 1217 | # 1218 | # 1219 | # 1220 | ################################# 1221 | 1222 | #__________________________GPP decryption helper function stolen from PowerUp.ps1 by @harmjoy__________________ 1223 | function Get-DecryptedCpassword { 1224 | [CmdletBinding()] 1225 | Param ( 1226 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string] $Cpassword 1227 | ) 1228 | 1229 | try { 1230 | # Append appropriate padding based on string length 1231 | $Mod = ($Cpassword.length % 4) 1232 | 1233 | switch ($Mod) { 1234 | '1' {$Cpassword = $Cpassword.Substring(0,$Cpassword.Length -1)} 1235 | '2' {$Cpassword += ('=' * (4 - $Mod))} 1236 | '3' {$Cpassword += ('=' * (4 - $Mod))} 1237 | } 1238 | 1239 | $Base64Decoded = [Convert]::FromBase64String($Cpassword) 1240 | 1241 | # Create a new AES .NET Crypto Object 1242 | $AesObject = New-Object System.Security.Cryptography.AesCryptoServiceProvider 1243 | [Byte[]] $AesKey = @(0x4e,0x99,0x06,0xe8,0xfc,0xb6,0x6c,0xc9,0xfa,0xf4,0x93,0x10,0x62,0x0f,0xfe,0xe8, 1244 | 0xf4,0x96,0xe8,0x06,0xcc,0x05,0x79,0x90,0x20,0x9b,0x09,0xa4,0x33,0xb6,0x6c,0x1b) 1245 | 1246 | # Set IV to all nulls to prevent dynamic generation of IV value 1247 | $AesIV = New-Object Byte[]($AesObject.IV.Length) 1248 | $AesObject.IV = $AesIV 1249 | $AesObject.Key = $AesKey 1250 | $DecryptorObject = $AesObject.CreateDecryptor() 1251 | [Byte[]] $OutBlock = $DecryptorObject.TransformFinalBlock($Base64Decoded, 0, $Base64Decoded.length) 1252 | 1253 | return [System.Text.UnicodeEncoding]::Unicode.GetString($OutBlock) 1254 | } 1255 | 1256 | catch { 1257 | Write-Error $Error[0] 1258 | } 1259 | } 1260 | 1261 | Function Write-NoEmpties { 1262 | Param ( 1263 | $output 1264 | ) 1265 | # this function literally just prints hash tables but skips any with an empty value. 1266 | Foreach ($outpair in $output.GetEnumerator()) { 1267 | if (-Not (("", $null) -Contains $outpair.Value)) { 1268 | Write-Output ($outpair) 1269 | } 1270 | } 1271 | } 1272 | 1273 | Function Write-ColorText { 1274 | Param ( 1275 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Text, 1276 | [Parameter(Mandatory=$false)][ValidateSet('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White', ignorecase=$true)] [string]$Color = $host.ui.RawUI.ForegroundColor 1277 | ) 1278 | # does what it says on the tin - writes text in colour. 1279 | $DefForegroundColor = $host.ui.RawUI.ForegroundColor 1280 | $host.ui.RawUI.ForegroundColor = $Color 1281 | Write-Output $Text 1282 | $host.ui.RawUI.ForegroundColor = $DefForegroundColor 1283 | } 1284 | 1285 | Function Write-Title { 1286 | [cmdletbinding()] 1287 | Param ( 1288 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Text, 1289 | [Parameter(Mandatory=$false)][ValidateSet('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White', ignorecase=$true)] [string]$Color = $host.ui.RawUI.ForegroundColor, 1290 | [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()][string]$DividerChar = "-" 1291 | ) 1292 | Write-ColorText -Text $Text -Color $Color 1293 | $divider = $DividerChar * $Text.Length 1294 | Write-ColorText -Text $divider -Color $Color 1295 | } 1296 | 1297 | Function Write-Banner { 1298 | 1299 | $barf = @' 1300 | .,-:::::/ :::::::.. ... ... :::::::::::::. .,:::::: :::::::.. 1301 | ,;;-'````' ;;;;``;;;; .;;;;;;;. ;; ;;; `;;;```.;;;;;;;'''' ;;;;``;;;; 1302 | [[[ [[[[[[/[[[,/[[[' ,[[ \[[,[[' [[[ `]]nnn]]' [[cccc [[[,/[[[' 1303 | "$$c. "$$ $$$$$$c $$$, $$$$$ $$$ $$$"" $$"""" $$$$$$c 1304 | `Y8bo,,,o88o888b "88bo,"888,_ _,88P88 .d888 888o 888oo,__ 888b "88bo, 1305 | `'YMUP"YMMMMMM "W" "YMMMMMP" "YmmMMMM"" YMMMb """"YUMMMMMMM "W" 1306 | github.com/mikeloss 1307 | @mikeloss 1308 | '@ -split "`n" 1309 | 1310 | $Pattern = ('White','Yellow','Red','Red','DarkRed','DarkRed','White','White') 1311 | "" 1312 | "" 1313 | $i = 0 1314 | foreach ($barfline in $barf) { 1315 | Write-ColorText -Text $barfline -Color $Pattern[$i] 1316 | $i += 1 1317 | } 1318 | } 1319 | 1320 | Function Find-IntACL { 1321 | Param ( 1322 | [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Path 1323 | ) 1324 | # Consumes a file path, returns a hash table containing the owner, a hash table of trustees, and a value for 1325 | # "Vulnerable" to show if current user can write the target file, determined by attempting to open the target 1326 | # file for writing, then immediately closing it. 1327 | $ACLData = @{} 1328 | try { 1329 | $targetPathACL = Get-ACL $Path -ErrorAction Stop 1330 | $targetPathOwner = $targetPathACL.Owner 1331 | $targetPathAccess = $targetPathACL.Access | Where-Object {-Not ($boringTrustees -Contains $_.IdentityReference)} | select FileSystemRights,AccessControlType,IdentityReference 1332 | $ACLData.Add("Owner", $targetPathOwner) 1333 | $ACLData.Add("Trustees", $targetPathAccess) 1334 | Try { 1335 | [io.file]::OpenWrite($targetPath).close() 1336 | $ACLData.Add("Vulnerable","True") 1337 | } 1338 | Catch { 1339 | $ACLData.Add("Vulnerable","False") 1340 | } 1341 | } 1342 | catch [System.Exception] { 1343 | $ACLData.Add("Vulnerable","Error") 1344 | } 1345 | return $ACLData 1346 | } 1347 | 1348 | #_____________________________________________________________________ 1349 | Function Invoke-AuditGPO { 1350 | [cmdletbinding()] 1351 | # Consumes objects from a Get-GPOReport xml report and returns findings based on the $level filter. 1352 | Param ( 1353 | [Parameter(Mandatory=$true)][System.Xml.XmlElement]$xmlgpo, 1354 | [Parameter(Mandatory=$true)][ValidateSet(1,2,3)][int]$level 1355 | ) 1356 | 1357 | #check the GPO is even enabled 1358 | $gpoisenabled = $xmlgpo.LinksTo.Enabled 1359 | #and if it's not, increment our count of GPOs that don't do anything 1360 | if (($gpoisenabled -ne "true") -And (!$Global:showdisabled)) { 1361 | $Global:unlinkedPols += 1 1362 | return $null 1363 | } 1364 | 1365 | #check if it's linked somewhere 1366 | $gpopath = $xmlgpo.LinksTo.SOMName 1367 | #and if it's not, increment our count of GPOs that don't do anything 1368 | if ((-Not $gpopath) -And (!$Global:showdisabled)) { 1369 | $Global:unlinkedPols += 1 1370 | return $null 1371 | } 1372 | 1373 | # Define settings groups so we can send through both if the same type of policy settings can appear in either. 1374 | $computerSettings = $xmlgpo.Computer 1375 | $userSettings = $xmlgpo.User 1376 | 1377 | # Build an array of all our Get-GPO* check scriptblocks 1378 | $polchecks = @() 1379 | $polchecks += {Get-GPORegKeys -Level $level -polXML $computerSettings} 1380 | $polchecks += {Get-GPORegKeys -Level $level -polXML $userSettings} 1381 | $polchecks += {Get-GPOUsers -Level $level -polXML $userSettings} 1382 | $polchecks += {Get-GPOUsers -Level $level -polXML $computerSettings} 1383 | $polchecks += {Get-GPOGroups -Level $level -polXML $userSettings} 1384 | $polchecks += {Get-GPOGroups -Level $level -polXML $computerSettings} 1385 | $polchecks += {Get-GPOScripts -Level $level -polXML $userSettings} 1386 | $polchecks += {Get-GPOScripts -Level $level -polXML $computerSettings} 1387 | $polchecks += {Get-GPOFileUpdate -Level $level -polXML $userSettings} 1388 | $polchecks += {Get-GPOFileUpdate -Level $level -polXML $computerSettings} 1389 | $polchecks += {Get-GPOMSIInstallation -Level $level -polXML $userSettings} 1390 | $polchecks += {Get-GPOMSIInstallation -Level $level -polXML $computerSettings} 1391 | $polchecks += {Get-GPOUserRights -Level $level -polXML $xmlgpo} 1392 | $polchecks += {Get-GPOSchedTasks -Level $level -polXML $xmlgpo} 1393 | $polchecks += {Get-GPOFolderRedirection -Level $level -polXML $xmlgpo} 1394 | $polchecks += {Get-GPOFilePerms -Level $level -polXML $xmlgpo} 1395 | $polchecks += {Get-GPOSecurityOptions -Level $level -polXML $xmlgpo} 1396 | $polchecks += {Get-GPOAccountSettings -Level $level -polXML $xmlgpo} 1397 | $polchecks += {Get-GPONetworkShares -Level $level -polXml $xmlgpo} 1398 | $polchecks += {Get-GPOFolders -Level $level -polXML $userSettings} 1399 | $polchecks += {Get-GPOFolders -Level $level -polXML $computerSettings} 1400 | $polchecks += {Get-GPORegSettings -Level $level -polXML $computerSettings} 1401 | $polchecks += {Get-GPORegSettings -Level $level -polXML $userSettings} 1402 | $polchecks += {Get-GPOIniFiles -Level $level -polXML $computerSettings} 1403 | $polchecks += {Get-GPOIniFiles -Level $level -polXML $userSettings} 1404 | $polchecks += {Get-GPOEnvVars -Level $level -polXML $computerSettings} 1405 | $polchecks += {Get-GPOEnvVars -Level $level -polXML $userSettings} 1406 | $polchecks += {Get-GPOShortcuts -Level $level -polXml $userSettings} 1407 | $polchecks += {Get-GPOShortcuts -Level $level -polXml $computerSettings} 1408 | 1409 | # Write a pretty green header with the report name and some other nice details 1410 | $headers = @() 1411 | $headers += {'==============================================================='} 1412 | $headers += {'Policy UID: {0}' -f $xmlgpo.Identifier.Identifier.InnerText} 1413 | $headers += {'Policy created on: {0:G}' -f ([DateTime]$xmlgpo.CreatedTime)} 1414 | $headers += {'Policy last modified: {0:G}' -f ([DateTime]$xmlgpo.ModifiedTime)} 1415 | $headers += {'Policy owner: {0}' -f $xmlgpo.SecurityDescriptor.Owner.Name.InnerText} 1416 | $headers += {'Linked OU: {0}' -f $gpopath} 1417 | $headers += {'Link enabled: {0}' -f $gpoisenabled} 1418 | $headers += {'==============================================================='} 1419 | 1420 | # In each GPO we parse, iterate through the list of checks to see if any of them return anything. 1421 | $headerprinted = $false 1422 | foreach ($polcheck in $polchecks) { 1423 | $finding = & $polcheck # run the check and store the output 1424 | if ($finding) { 1425 | # the first time one of the checks returns something, show the user the header with the policy name and so on 1426 | if (!$headerprinted) { 1427 | # Increment the total counter of displayed policies. 1428 | $Global:displayedPols += 1 1429 | # Write the title of the GPO in nice green text 1430 | Write-ColorText -Color "Green" -Text $xmlgpo.Name 1431 | # Write the headers from above 1432 | foreach ($header in $headers) { 1433 | & $header 1434 | } 1435 | 1436 | # Parse and print out the GPO's Permissions 1437 | $GPOPermissions = $xmlgpo.SecurityDescriptor.Permissions.TrusteePermissions 1438 | # an array of permissions that aren't exciting 1439 | $boringPerms = @() 1440 | $boringPerms += "Read" 1441 | $boringPerms += "Apply Group Policy" 1442 | # an array of users who have RW permissions on GPOs by default, so they're boring too. 1443 | $boringTrustees = @() 1444 | $boringTrustees += "Domain Admins" 1445 | $boringTrustees += "Enterprise Admins" 1446 | $boringTrustees += "ENTERPRISE DOMAIN CONTROLLERS" 1447 | $boringTrustees += "SYSTEM" 1448 | 1449 | $permOutput = @{} 1450 | 1451 | # iterate over each permission entry for the GPO 1452 | foreach ($GPOACE in $GPOPermissions) { 1453 | $ACEType = $GPOACE.Standard.GPOGroupedAccessEnum # allow v deny 1454 | $trusteeName = $GPOACE.Trustee.Name.InnerText # who does it apply to 1455 | $trusteeSID = $GPOACE.Trustee.SID.InnerText # SID of the account/group it applies to 1456 | $ACEInteresting = $true # ACEs are default interesting unless proven boring. 1457 | 1458 | # check if our trustee is a 'boring' default one 1459 | if ($trusteeName) { 1460 | foreach ($boringTrustee in $boringTrustees) { 1461 | if ($trusteeName -match $boringTrustee) { 1462 | $ACEInteresting = $false 1463 | } 1464 | } 1465 | } 1466 | # check if our permission is boring 1467 | if (($boringPerms -Contains $ACEType) -Or ($GPOACE.Type.PermissionType -eq "Deny")){ 1468 | $ACEInteresting = $false 1469 | } 1470 | 1471 | # if it's still interesting, 1472 | if ($ACEInteresting) { 1473 | #if we have a valid trustee name, add it to the output 1474 | if ($trusteeName) { 1475 | $permOutput.Add("Trustee",$trusteeName) 1476 | } 1477 | #if we have a SID, add it to the output 1478 | elseif ($trusteeSID) { 1479 | $permOutput.Add("Trustee SID", $trusteeSID) 1480 | } 1481 | #add our other stuff to the output 1482 | $permOutput.Add("Type", $GPOACE.Type.PermissionType) 1483 | $permOutput.Add("Access", $GPOACE.Standard.GPOGroupedAccessEnum) 1484 | } 1485 | } 1486 | # then print out the GPO's permissions 1487 | if ($permOutput.Count -gt 0) { 1488 | Write-Title -DividerChar "#" -Color "Yellow" -Text "GPO Permissions" 1489 | Write-Output $permOutput "`r`n" 1490 | } 1491 | 1492 | # then we set $headerprinted to 1 so we don't print it all again 1493 | $headerprinted = 1 1494 | } 1495 | # Then for each actual finding we write the name of the check function that found something. 1496 | $polcheckbits = ($polcheck.ToString()).split(' ') 1497 | $polchecktitle = $polcheckbits[0] 1498 | 1499 | Switch ($polcheckbits[4]) 1500 | { 1501 | '$computerSettings' { $polchecktype = 'Computer Policy'; break } 1502 | '$userSettings' { $polchecktype = 'User Policy'; break } 1503 | '$xmlgpo' { $polchecktype = 'All Policy'; break } 1504 | default {''; break} 1505 | } 1506 | 1507 | $polchecktitle = "$polchecktitle - $polchecktype" 1508 | Write-Title -DividerChar "#" -Color "Yellow" -Text $polchecktitle 1509 | # Write out the actual finding 1510 | $finding 1511 | } 1512 | } 1513 | [System.GC]::Collect() 1514 | } 1515 | 1516 | Function Invoke-AuditGPOReport { 1517 | [cmdletbinding(DefaultParameterSetName='NoArgs')] 1518 | param( 1519 | [Parameter(ParameterSetName='WithFile', Mandatory=$true, HelpMessage="Path to XML GPO report")] 1520 | [Parameter(ParameterSetName='OnlineDomain', Mandatory=$false, HelpMessage="Path to XML GPO report")] 1521 | [ValidateScript({if(Test-Path $_ -PathType 'Leaf'){$true}else{Throw "Invalid path given: $_"}})] 1522 | [ValidateScript({if($_ -Match '\.xml'){$true}else{Throw "Supplied file is not XML: $_"}})] 1523 | [System.IO.FileInfo]$Path, 1524 | 1525 | [Parameter(ParameterSetName='WithFile', Mandatory=$false, HelpMessage="Toggle filtering GPOs that aren't linked anywhere")] 1526 | [Parameter(ParameterSetName='WithoutFile', Mandatory=$false, HelpMessage="Toggle filtering GPOs that aren't linked anywhere")] 1527 | [Parameter(ParameterSetName='OnlineDomain', Mandatory=$false, HelpMessage="Toggle filtering GPOs that aren't linked anywhere")] 1528 | [switch]$showDisabled, 1529 | 1530 | [Parameter(ParameterSetName='WithFile', Mandatory=$false, HelpMessage="Set verbosity level (1 = most verbose, 3 = only show things that are definitely bad)")] 1531 | [Parameter(ParameterSetName='WithoutFile', Mandatory=$false, HelpMessage="Set verbosity level (1 = most verbose, 3 = only show things that are definitely bad)")] 1532 | [Parameter(ParameterSetName='OnlineDomain', Mandatory=$false, HelpMessage="Set verbosity level (1 = most verbose, 3 = only show things that are definitely bad)")] 1533 | [ValidateSet(1,2,3)] 1534 | [int]$level = 2, 1535 | 1536 | [Parameter(ParameterSetName='OnlineDomain', Mandatory=$true, HelpMessage="Perform online checks by actively contacting DCs within the target domain")] 1537 | [switch]$online, 1538 | 1539 | [Parameter(ParameterSetName='OnlineDomain', Mandatory=$false, HelpMessage="FQDN for the domain to target for online checks")] 1540 | [ValidateNotNullOrEmpty()] 1541 | [string]$domain = $env:UserDomain 1542 | ) 1543 | 1544 | # This sucker actually consumes the file, does the stuff, this is the guy, you know? 1545 | 1546 | Write-Banner 1547 | 1548 | if ($PSVersionTable.PSVersion.Major -le 2) { 1549 | Write-ColorText -Color "Red" -Text "Sorry, Grouper is not yet compatible with PowerShell 2.0." 1550 | break 1551 | } 1552 | 1553 | #check if an xml report is specified, otherwise try to generate the report using Get-GPOReport 1554 | if ($Path -eq $null) { 1555 | $lazyMode = $true 1556 | } 1557 | 1558 | # couple of counters for the stats at the end 1559 | $Global:unlinkedPols = 0 1560 | $Global:GPOsWithIntSettings = 0 1561 | $Global:GPOsWithVulnSettings = 0 1562 | $Global:displayedPols = 0 1563 | 1564 | #handle our arguments 1565 | $Global:showDisabled = $false 1566 | if ($showDisabled) { 1567 | $Global:showDisabled = $true 1568 | } 1569 | 1570 | # quick and dirty check to make sure that if the user said to do 'online' checks that we can actually reach the domain. 1571 | $Global:onlineChecks = $false 1572 | if ($online) { 1573 | if ((Test-Path "\\$($domain)\SYSVOL") -eq $true) { 1574 | Write-ColorText -Text "`r`n[i] Confirmed connectivity to AD domain $domain, including online-only checks.`r`n" -Color "Green" 1575 | $Global:onlineChecks = $true 1576 | } 1577 | else { 1578 | Write-ColorText -Text "`r`n[!] Couldn't talk to the domain $domain, falling back to offline mode.`r`n" -Color "Red" 1579 | $Global:onlineChecks = $False 1580 | } 1581 | } 1582 | 1583 | # if the user set $lazyMode, confirm that the relevant module is available, then generate a gporeport using some default settings. 1584 | if ($lazyMode) { 1585 | $requiredModules = @('GroupPolicy') 1586 | $requiredModules | Import-Module -Verbose:$false -ErrorAction SilentlyContinue 1587 | if (($requiredModules | Get-Module) -eq $null) { 1588 | Write-Warning ('[!] Could not import required modules, confirm the following modules exist on this host: {0}' -f $($requiredModules -join ', ')) 1589 | Break 1590 | } 1591 | 1592 | if ($PSBoundParameters.Domain) { 1593 | $reportPath = "$($pwd)\$($domain)_gporeport.xml" 1594 | Get-GPOReport -All -ReportType xml -Path $reportPath -Domain $domain 1595 | } 1596 | else { 1597 | $reportPath = "$($pwd)\gporeport.xml" 1598 | Get-GPOReport -All -ReportType xml -Path $reportPath 1599 | } 1600 | [xml]$xmldoc = get-content $reportPath 1601 | } 1602 | # and if the user didn't set $lazyMode, get the contents of the report they asked us to look at 1603 | elseif ($Path){ 1604 | # get the contents of the report file 1605 | [xml]$xmldoc = get-content $Path 1606 | } 1607 | 1608 | # get all the GPOs into an array 1609 | $xmlgpos = $xmldoc.report.GPO 1610 | 1611 | # iterate over them running the selected checks 1612 | foreach ($xmlgpo in $xmlgpos) { 1613 | Invoke-AuditGPO -xmlgpo $xmlgpo -Level $level 1614 | } 1615 | 1616 | $gpocount = ($xmlgpos.Count, 1 -ne $null)[0] 1617 | 1618 | Write-Title -Color "Green" -DividerChar "*" -Text "Stats" 1619 | $stats = @() 1620 | $stats += ('Display Level: {0}' -f $level) 1621 | $stats += ('Online Checks Performed: {0}' -f $Global:onlineChecks) 1622 | $stats += ('Displayed GPOs: {0}' -f $Global:displayedPols) 1623 | $stats += ('Unlinked GPOs: {0}' -f $Global:unlinkedPols) 1624 | #$stats += ('Interesting Settings: {0}' -f $Global:GPOsWithIntSettings) 1625 | #$stats += ('Vulnerable Settings: {0}' -f $Global:GPOsWithVulnSettings) 1626 | $stats += ('Total GPOs: {0}' -f $gpocount) 1627 | Write-Output $stats 1628 | } 1629 | 1630 | Export-ModuleMember -Function 'Invoke-AuditGPOReport' 1631 | --------------------------------------------------------------------------------