├── PowerView-with-RemoteAccessPolicyEnumeration.ps1 └── README.md /PowerView-with-RemoteAccessPolicyEnumeration.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 2 2 | 3 | # PowerView extensions for enumerating remote access policies through group policy. 4 | # William Knowles (@william_knows) and Jon Cave (@joncave) 5 | # For more details, see: https://labs.mwrinfosecurity.com/blog/enumerating-remote-access-policies-through-gpo 6 | 7 | # The following PowerView extensions were based on the code from commit be932ce 8 | # Obtain a copy of this ... 9 | IEX (New-Object Net.Webclient).DownloadString("https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/be932ce2be3e2a574c403f1635057029e176f858/Recon/PowerView.ps1") 10 | 11 | function Find-ComputersWithRemoteAccessPolicies { 12 | <# 13 | .SYNOPSIS 14 | 15 | Returns the DNS hostnames of computers with remote access policies relevant to lateral movement. 16 | 17 | .DESCRIPTION 18 | 19 | Checks GPO for settings which deal with remote access policies relevant to lateral movement 20 | (e.g., "EnableLUA" and "LocalAccountTokenFilterPolicy"). The OUs to which these GPOs are applied 21 | are then identified, and then the computer objects from each are retrieved. Note that this only 22 | retrieves computer objects who have had the relevent registry keys set through group policy. 23 | 24 | .PARAMETER Domain 25 | 26 | Specifies the domain to use for the query, defaults to the current domain. 27 | 28 | .PARAMETER SearchBase 29 | 30 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 31 | Useful for OU queries. 32 | 33 | .PARAMETER SearchScope 34 | 35 | Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). 36 | 37 | .PARAMETER Server 38 | 39 | Specifies an Active Directory server (domain controller) to bind to. 40 | 41 | .PARAMETER ResultPageSize 42 | 43 | Specifies the PageSize to set for the LDAP searcher object. 44 | 45 | .PARAMETER ServerTimeLimit 46 | 47 | Specifies the maximum amount of time the server spends searching. Default of 120 seconds. 48 | 49 | .PARAMETER Credential 50 | 51 | A [Management.Automation.PSCredential] object of alternate credentials 52 | for connection to the target domain. 53 | 54 | .EXAMPLE 55 | 56 | PS C:\> Find-ComputersWithRemoteAccessPolicies 57 | 58 | Returns the DNS hostnames for computer objects that have GPOs applied which may enable lateral movement. 59 | 60 | .EXAMPLE 61 | 62 | PS C:\> Find-ComputersWithRemoteAccessPolicies -Domain dev.testlab.local 63 | 64 | Returns the DNS hostnames for computer objects that have GPOs applied which may enable lateral movement. Limit to a particular domain. 65 | 66 | .EXAMPLE 67 | 68 | PS C:\> Find-ComputersWithRemoteAccessPolicies -SearchBase "OU=secret,DC=testlab,DC=local" 69 | 70 | Returns the DNS hostnames for computer objects that have GPOs applied which may enable lateral movement. Limit to a particular organisational unit. 71 | 72 | #> 73 | 74 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 75 | [CmdletBinding()] 76 | Param( 77 | [ValidateNotNullOrEmpty()] 78 | [String] 79 | $Domain, 80 | 81 | [ValidateNotNullOrEmpty()] 82 | [Alias('ADSPath')] 83 | [String] 84 | $SearchBase, 85 | 86 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 87 | [String] 88 | $SearchScope = 'Subtree', 89 | 90 | [ValidateNotNullOrEmpty()] 91 | [Alias('DomainController')] 92 | [String] 93 | $Server, 94 | 95 | [ValidateRange(1, 10000)] 96 | [Int] 97 | $ResultPageSize = 200, 98 | 99 | [ValidateRange(1, 10000)] 100 | [Int] 101 | $ServerTimeLimit, 102 | 103 | [Management.Automation.PSCredential] 104 | [Management.Automation.CredentialAttribute()] 105 | $Credential = [Management.Automation.PSCredential]::Empty 106 | 107 | ) 108 | 109 | BEGIN { 110 | $SearcherArguments = @{} 111 | if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } 112 | if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $Domain } 113 | if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } 114 | if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase} 115 | if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope} 116 | if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } 117 | if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } 118 | if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } 119 | 120 | } 121 | 122 | PROCESS { 123 | 124 | $ComputerObjectsWithRemoteAccessPolicies = New-Object PSObject 125 | $ComputerObjectsWithRemoteAccessPolicies | Add-Member NoteProperty EnableLUA (New-Object System.Collections.Generic.List[System.Object]) 126 | $ComputerObjectsWithRemoteAccessPolicies | Add-Member NoteProperty FilterAdministratorToken (New-Object System.Collections.Generic.List[System.Object]) 127 | $ComputerObjectsWithRemoteAccessPolicies | Add-Member NoteProperty LocalAccountTokenFilterPolicy (New-Object System.Collections.Generic.List[System.Object]) 128 | $ComputerObjectsWithRemoteAccessPolicies | Add-Member NoteProperty SeDenyNetworkLogonRight (New-Object System.Collections.Generic.List[System.Object]) 129 | $ComputerObjectsWithRemoteAccessPolicies | Add-Member NoteProperty SeDenyRemoteInteractiveLogonRight (New-Object System.Collections.Generic.List[System.Object]) 130 | 131 | $gpoSearchArguments = @{} 132 | $gpoSearchArguments = $gpoSearchArguments + $SearcherArguments 133 | $gpoSearchArguments.Remove("SearchBase") 134 | $gpoSearchArguments.Remove("SearchScope") 135 | # NOTE: SearchBase is removed here, as we do not wish it to be applied to the initial call to Get-DomainGPORemoteAccessPolicy 136 | # and instead for the search to be conducted across the domain 137 | $RemoteAccessPolicies = Get-DomainGPORemoteAccessPolicy @gpoSearchArguments 138 | 139 | $RemoteAccessPolicies.PSObject.Properties | ForEach-Object { 140 | $policy = $_.Name # EnableLUA, etc 141 | foreach ($guid in $RemoteAccessPolicies.$policy) { 142 | # set arguments for OU search (reading $SearchBase to limit the scope) 143 | $ouSearchArguments = @{} 144 | $ouSearchArguments = $ouSearchArguments + $SearcherArguments 145 | $ouSearchArguments['GPLink'] = $guid 146 | Get-DomainOU @ouSearchArguments | ForEach-Object { 147 | $compSearchArguments = @{} 148 | $compSearchArguments = $compSearchArguments + $SearcherArguments 149 | $compSearchArguments['SearchBase'] = $_.distinguishedname 150 | $OUComputers = Get-DomainComputer @compSearchArguments 151 | $OUComputers | ForEach-Object { 152 | if ($ComputerObjectsWithRemoteAccessPolicies.$policy -notcontains $_.dnshostname) { $ComputerObjectsWithRemoteAccessPolicies.$policy += $_.dnshostname } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | END { 160 | return $ComputerObjectsWithRemoteAccessPolicies 161 | } 162 | } 163 | 164 | function Get-DomainGPORemoteAccessPolicy { 165 | <# 166 | .SYNOPSIS 167 | 168 | Enumerates GPOs that control settings that deal with remote access policies. 169 | 170 | .DESCRIPTION 171 | 172 | Checks GPO for five different remote access policies. Three which relate to User 173 | Account Control (UAC) and two which relate to User Rights Assignment (URA). 174 | The three UAC policies are: 175 | (1) "EnableLUA" which controls "Admin Approval Mode" for the local administrator group. 176 | When set to 0 UAC is disabled. This setting can be controlled by group policy directly 177 | and is stored in "GptTmpl.inf". 178 | (2) "FilterAdministratorToken" controls "Admin Approval Mode" for the RID 500 account. 179 | When set to 0 remote connections for the RID 500 account will be granted a high 180 | integrity token. This setting is disabled by default. This setting can be controlled 181 | by group policy directly and is stored in "GptTmpl.inf". 182 | (3) "LocalAccountTokenFilterPolicy" controls token integrity for remote connections. 183 | When set to 1 all remote connections for local users in the local administrator group 184 | will be granted a high integrity token. This setting can only be set through a custom 185 | registry key and is stored in "Registry.xml". 186 | The order of precedence for the above three UAC commands is: EnableLUA, 187 | LocalAccountTokenFilterPolicy, FilterAdministratorToken. For example, for 188 | FilterAdministratorToken to have an effect EnableLUA would need to be set to 1, and 189 | LocalAccountTokenFilterPolicy to 0. 190 | The two URA policies are: 191 | (4) and (5) "SeDenyNetworkLogonRight" and "SeDenyRemoteInteractiveLogonRight" are 192 | checked to see if they include the SID of the built-in Administrators group. If they 193 | do, any member of this group can not be used to perform network or remote interactive 194 | authentication against the computer object on which they are configured. 195 | 196 | .PARAMETER Identity 197 | 198 | A display name (e.g. 'Test GPO'), DistinguishedName (e.g. 'CN={F260B76D-55C8-46C5-BEF1-9016DD98E272},CN=Policies,CN=System,DC=testlab,DC=local'), 199 | GUID (e.g. '10ec320d-3111-4ef4-8faf-8f14f4adc789'), or GPO name (e.g. '{F260B76D-55C8-46C5-BEF1-9016DD98E272}'). Wildcards accepted. 200 | 201 | .PARAMETER Domain 202 | 203 | Specifies the domain to use for the query, defaults to the current domain. 204 | 205 | .PARAMETER LDAPFilter 206 | 207 | Specifies an LDAP query string that is used to filter Active Directory objects. 208 | 209 | .PARAMETER SearchBase 210 | 211 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 212 | Useful for OU queries. 213 | 214 | .PARAMETER Server 215 | 216 | Specifies an Active Directory server (domain controller) to bind to. 217 | 218 | .PARAMETER SearchScope 219 | 220 | Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). 221 | 222 | .PARAMETER ResultPageSize 223 | 224 | Specifies the PageSize to set for the LDAP searcher object. 225 | 226 | .PARAMETER ServerTimeLimit 227 | 228 | Specifies the maximum amount of time the server spends searching. Default of 120 seconds. 229 | 230 | .PARAMETER Credential 231 | 232 | A [Management.Automation.PSCredential] object of alternate credentials 233 | for connection to the target domain. 234 | 235 | .EXAMPLE 236 | 237 | Get-DomainGPORemoteAccessPolicy 238 | 239 | Returns an object where the key is the remote access policy, and the value is 240 | a list of GPOs which set the policy. 241 | 242 | #> 243 | 244 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 245 | [CmdletBinding()] 246 | Param( 247 | [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 248 | [Alias('DistinguishedName', 'SamAccountName', 'Name')] 249 | [String[]] 250 | $Identity, 251 | 252 | [ValidateNotNullOrEmpty()] 253 | [String] 254 | $Domain, 255 | 256 | [ValidateNotNullOrEmpty()] 257 | [Alias('Filter')] 258 | [String] 259 | $LDAPFilter, 260 | 261 | [ValidateNotNullOrEmpty()] 262 | [Alias('ADSPath')] 263 | [String] 264 | $SearchBase, 265 | 266 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 267 | [String] 268 | $SearchScope = 'Subtree', 269 | 270 | [ValidateNotNullOrEmpty()] 271 | [Alias('DomainController')] 272 | [String] 273 | $Server, 274 | 275 | [ValidateRange(1, 10000)] 276 | [Int] 277 | $ResultPageSize = 200, 278 | 279 | [ValidateRange(1, 10000)] 280 | [Int] 281 | $ServerTimeLimit, 282 | 283 | [Management.Automation.PSCredential] 284 | [Management.Automation.CredentialAttribute()] 285 | $Credential = [Management.Automation.PSCredential]::Empty 286 | ) 287 | 288 | BEGIN { 289 | $SearcherArguments = @{} 290 | if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } 291 | if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $Domain } 292 | if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } 293 | if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } 294 | if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } 295 | if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } 296 | if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } 297 | if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } 298 | 299 | $ConvertArguments = @{} 300 | if ($PSBoundParameters['Domain']) { $ConvertArguments['Domain'] = $Domain } 301 | if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } 302 | if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } 303 | 304 | $SplitOption = [System.StringSplitOptions]::RemoveEmptyEntries 305 | } 306 | 307 | PROCESS { 308 | if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } 309 | 310 | $RemoteAccessPolicies = New-Object PSObject 311 | $RemoteAccessPolicies | Add-Member NoteProperty EnableLUA (New-Object System.Collections.Generic.List[System.Object]) 312 | $RemoteAccessPolicies | Add-Member NoteProperty FilterAdministratorToken (New-Object System.Collections.Generic.List[System.Object]) 313 | $RemoteAccessPolicies | Add-Member NoteProperty LocalAccountTokenFilterPolicy (New-Object System.Collections.Generic.List[System.Object]) 314 | $RemoteAccessPolicies | Add-Member NoteProperty SeDenyNetworkLogonRight (New-Object System.Collections.Generic.List[System.Object]) 315 | $RemoteAccessPolicies | Add-Member NoteProperty SeDenyRemoteInteractiveLogonRight (New-Object System.Collections.Generic.List[System.Object]) 316 | 317 | # get every GPO from the specified domain 318 | Get-DomainGPO @SearcherArguments | ForEach-Object { 319 | 320 | $GPOdisplayName = $_.displayname 321 | $GPOname = $_.name 322 | $GPOPath = $_.gpcfilesyspath 323 | 324 | # EnableLUA and FilterAdministratorToken check via GptTmpl.inf 325 | $ParseArgs = @{ 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" } 326 | if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } 327 | # parse the GptTmpl.inf file (if it exists) for this GPO 328 | $Inf = Get-GptTmpl @ParseArgs 329 | if($Inf -and ($Inf.psbase.Keys -contains "Registry Values")) 330 | { 331 | $EnableLUA = $Inf["Registry Values"]["MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA"] 332 | if ($EnableLUA -and ($EnableLUA[0] -eq 4) -and ($EnableLUA[1] -eq 0)) 333 | { 334 | Write-Verbose "The following GPO enables pass-the-hash by disabling EnableLUA: $GPOdisplayName - $GPOname" 335 | # append to EnableLUA GPO list if it is not already there 336 | if ($RemoteAccessPolicies.EnableLUA -notcontains $GPOname) { $RemoteAccessPolicies.EnableLUA += $GPOname } 337 | } 338 | 339 | $FilterAdministratorToken = $Inf["Registry Values"]["MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\FilterAdministratorToken"] 340 | if ($FilterAdministratorToken -and ($FilterAdministratorToken[0] -eq 4) -and ($FilterAdministratorToken[1] -eq 0)) 341 | { 342 | Write-Verbose "The following GPO exempts the RID 500 account from UAC protection by disabling FilterAdministratorToken: $GPOdisplayName - $GPOname" 343 | # append to FilterAdministratorToken GPO list if it is not already there 344 | if ($RemoteAccessPolicies.FilterAdministratorToken -notcontains $GPOname) { $RemoteAccessPolicies.FilterAdministratorToken += $GPOname } 345 | } 346 | } 347 | 348 | # LocalAccountTokenFilterPolicy check via Registry.xml 349 | # clear $ParseArgs for next use. 350 | $ParseArgs.Clear() 351 | # parse Registry.xml file (if it exists) for LocalAccountTokenFilterPolicy 352 | $ParseArgs = @{ 'RegistryXMLpath' = "$GPOPath\MACHINE\Preferences\Registry\Registry.xml" } 353 | if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } 354 | Get-RegistryXML @ParseArgs | ForEach-Object { 355 | if ($_.property -eq "LocalAccountTokenFilterPolicy" -and ($_.value -eq "00000001")) 356 | { 357 | Write-Verbose "The following GPO enables pass-the-hash by enabling LocalAccountTokenFilterPolicy: $GPOdisplayName - $GPOname" 358 | # append to EnableLUA GPO list if it is not already there 359 | if ($RemoteAccessPolicies.LocalAccountTokenFilterPolicy -notcontains $GPOname) { $RemoteAccessPolicies.LocalAccountTokenFilterPolicy += $GPOname } 360 | } 361 | } 362 | 363 | # SeDenyNetworkLogonRight and SeDenyRemoteInteractiveLogonRight check via GptTmpl.inf 364 | # Use existing object that parsed the file 365 | if($Inf -and ($Inf.psbase.Keys -contains "Privilege Rights")) 366 | { 367 | $SeDenyNetworkLogonRight = $Inf["Privilege Rights"]["SeDenyNetworkLogonRight"] 368 | if ($SeDenyNetworkLogonRight -and ($SeDenyNetworkLogonRight -contains "*S-1-5-32-544")) 369 | { 370 | Write-Verbose "The following GPO includes the built-in Administrators group within the SeDenyNetworkLogonRight: $GPOdisplayName - $GPOname" 371 | # append to SeDenyNetworkLogonRight GPO list if it is not already there 372 | if ($RemoteAccessPolicies.SeDenyNetworkLogonRight -notcontains $GPOname) { $RemoteAccessPolicies.SeDenyNetworkLogonRight += $GPOname } 373 | } 374 | 375 | $SeDenyRemoteInteractiveLogonRight = $Inf["Privilege Rights"]["SeDenyRemoteInteractiveLogonRight"] 376 | if ($SeDenyRemoteInteractiveLogonRight -and ($SeDenyRemoteInteractiveLogonRight -contains "*S-1-5-32-544")) 377 | { 378 | Write-Verbose "The following GPO includes the built-in Administrators group within the SeDenyRemoteInteractiveLogonRight: $GPOdisplayName - $GPOname" 379 | # append to SeDenyRemoteInteractiveLogonRight GPO list if it is not already there 380 | if ($RemoteAccessPolicies.SeDenyRemoteInteractiveLogonRight -notcontains $GPOname) { $RemoteAccessPolicies.SeDenyRemoteInteractiveLogonRight += $GPOname } 381 | } 382 | } 383 | } 384 | } 385 | 386 | END { 387 | # return hash table containing lists of GPOs for each remote access policy 388 | return $RemoteAccessPolicies 389 | } 390 | } 391 | 392 | function Get-RegistryXML { 393 | <# 394 | .SYNOPSIS 395 | 396 | Helper to parse a Registry.xml file path into an array of custom objects. 397 | 398 | .PARAMETER RegistryXMLpath 399 | 400 | The Registry.xml file path name to parse. 401 | 402 | .PARAMETER Credential 403 | 404 | A [Management.Automation.PSCredential] object of alternate credentials 405 | for connection to the remote system. 406 | 407 | #> 408 | 409 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 410 | [CmdletBinding()] 411 | Param ( 412 | [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 413 | [Alias('Path')] 414 | [String] 415 | $RegistryXMLPath, 416 | 417 | [Management.Automation.PSCredential] 418 | [Management.Automation.CredentialAttribute()] 419 | $Credential = [Management.Automation.PSCredential]::Empty 420 | ) 421 | 422 | BEGIN { 423 | $MappedPaths = @{} 424 | } 425 | 426 | PROCESS { 427 | try { 428 | 429 | if (($RegistryXMLPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { 430 | $SysVolPath = "\\$((New-Object System.Uri($RegistryXMLPath)).Host)\SYSVOL" 431 | if (-not $MappedPaths[$SysVolPath]) { 432 | # map IPC$ to this computer if it's not already 433 | Add-RemoteConnection -Path $SysVolPath -Credential $Credential 434 | $MappedPaths[$SysVolPath] = $True 435 | } 436 | } 437 | 438 | [XML]$RegistryXMLcontent = Get-Content $RegistryXMLPath -ErrorAction Stop 439 | 440 | $registryKeyArray = New-Object System.Collections.Generic.List[System.Object] 441 | 442 | # process all registry properties in the XML 443 | $RegistryXMLcontent | Select-Xml "/RegistrySettings/Registry" | Select-Object -ExpandProperty node | ForEach-Object { 444 | 445 | $GPORegistry = New-Object PSObject 446 | $GPORegistry | Add-Member Noteproperty "hive" $_.Properties.hive 447 | $GPORegistry | Add-Member Noteproperty "key" $_.Properties.key 448 | $GPORegistry | Add-Member Noteproperty "property" $_.Properties.name 449 | $GPORegistry | Add-Member Noteproperty "type" $_.Properties.type 450 | $GPORegistry | Add-Member Noteproperty "value" $_.Properties.value 451 | 452 | $registryKeyArray.Add($GPORegistry) 453 | 454 | } 455 | } 456 | catch { 457 | Write-Verbose "[Get-RegistryXML] Error parsing $TargetRegistryXMLPath : $_" 458 | } 459 | } 460 | 461 | END { 462 | # remove the SYSVOL mappings 463 | $MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ } 464 | # return array of regsitry settings 465 | return $registryKeyArray 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gists 2 | Short handy snippets from the @mwrlabs team 3 | 4 | ## [PowerView-with-RemoteAccessPolicyEnumeration.ps1](PowerView-with-RemoteAccessPolicyEnumeration.ps1) 5 | 6 | PowerView extensions for enumerating remote access policies through group policy. 7 | By William Knowles ([@william_knows](https://twitter.com/william_knows)) and Jon Cave ([@joncave](https://twitter.com/joncave)) 8 | For more details, see: https://labs.mwrinfosecurity.com/blog/enumerating-remote-access-policies-through-gpo 9 | --------------------------------------------------------------------------------