Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Test-Connection force enumeration of reparse points?

I've noticed some behavior in PowerShell that I can't explain, but I'm hoping that someone else can.

If I want to build a list of file objects from drive C:\, and I want to ignore shortcut folders (reparse points) such as C:\Documents and Settings\. The following command works well:

$FileList = @(Get-ChildItem -Path C:\ -Recurse -Force -Attributes !ReparsePoint);
$FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

The Where-Object command returns no files as expected, since C:\Documents and Settings\ is a reparse point.

However, if I run the Test-Connection command first, then the Get-ChildItem command appears to ignore the -Attributes !ReparsePoint parameter, and it traverses C:\Documents and Settings\.

Test-Connection -Computer MyComputer;
$FileList = @(Get-ChildItem -Path C:\ -Recurse -Force -Attributes !ReparsePoint);
$FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

In this case, the Where-Object command displays a lot of files. Note that the Test-Connection can be run against any computer, not just the local computer to exhibit this behavior.

I've duplicated this behavior on computers running PowerShell 4.0 and PowerShell 5.1. Can anyone please explain what's happening?


Additional note: To duplicate this behavior, please make sure that you are using an elevated instance of PowerShell (Run As Administrator). If you use a standard instance of PowerShell, you won't have permissions to view C:\Documents and Settings\.

like image 693
Van Vangor Avatar asked Dec 13 '17 21:12

Van Vangor


People also ask

What does Get-ChildItem do?

The Get-ChildItem cmdlet gets the items in one or more specified locations. If the item is a container, it gets the items inside the container, known as child items. You can use the Recurse parameter to get items in all child containers and use the Depth parameter to limit the number of levels to recurse.

What is GCI in PowerShell?

Get-ChildItem (GCI) gets items and if the item is a container, it will get child items available inside the container. Location specified in PowerShell Get-ChildItem can be file system directory, registry, or certificate store. Let's understand the PowerShell Get-ChildItem cmdlet with examples.


1 Answers

To answer the first issue where it seems to ignore the -Attributes !ReparsePoint, see my full SO answer with examples here: How to prevent recursion through node_modules for gci script. The TLDR is that by adding the -Recurse parameter to Get-ChildItem means that it will iterate through -all- items first then it will apply the filtering. That means it will faithfully exclude Reparse Points i.e. the specific folder: C:\Documents and Settings but since it get's all items first, it will also return everything underneath that folder as well. e.g. C:\Documents and Settings\desktop.ini because that isn't a Reparse Point, it's a document. That's why you can't use the -Recurse parameter without additional directory level filtering if that is your intention.

The second issue, executing Test-Connection changes what Get-ChildItem returns, does seem like a bug, in the manner that, it looks like this is a case of PowerShell not "fully" elevating the user when the Get-ChildItem is first executed. But at the same time, this might be intentional by the PowerShell team to remove the case of unintended duplicate items being returned.

To investigate, let's open a new PowerShell prompt as Administrator. I will slightly change the code to include a -Depth 1 just to make it easier to run through the code without having to enumerate my whole C: drive ;-) but, it will still illustrate the issue:

$FileList = @(Get-ChildItem -Path C:\ -Depth 1 -Recurse -Force -Attributes !ReparsePoint);
$FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

When I run this, the first clue to what is going on is the error message:

Get-ChildItem : Access to the path 'C:\Documents and Settings' is denied.

This highlights the fact that even though we are running the PowerShell prompt as Administrator, we are not running in a manner "elevated" enough to access the hidden, reparse point, system folder, "Documents and Settings". We have to keep in mind that the "Documents and Settings" folder is meant as a cheap workaround for legacy (bad coders ;-) compatibility with Windows Vista etc. and is not meant for "daily" normal usage. And if we do use it, it may cause unintentional duplicate/recursive items to be returned. i.e.:

PS C:\> $FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};
PS C:\>
PS C:\> $FileList | Where-Object {$_.DirectoryName -like "*Users*"};


    Directory: C:\Users


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a-hs-       2018-09-15   1:31 AM            174 desktop.ini

We see that we got the contents of the Users folder, but due to the error earlier, we did not return the contents of the Documents and Settings folder. This is good and likely intentional because we did not get any duplicate items. We only got the "real" items on the "real" path.

If we run the Test-Connection command next, followed by the same command (with different variable names to eliminate confusion):

Test-Connection -Computer MyComputer;
$FileList2 = @(Get-ChildItem -Path C:\ -Depth 1 -Recurse -Force -Attributes !ReparsePoint);
$FileList2 | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

We notice 2 things:

  1. We do not get any Access Denied error messages.
  2. We get more (duplicate) items:

.

PS C:\> $FileList2 | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};    

    Directory: C:\Documents and Settings


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a-hs-       2018-09-15   1:31 AM            174 desktop.ini

PS C:\> $FileList2 | Where-Object {$_.DirectoryName -like "*Users*"};


    Directory: C:\Users


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a-hs-       2018-09-15   1:31 AM            174 desktop.ini

This, I would say, is unintentional. Returning the items from the Reparse Point path is not desired. If we were feeding this information into other script tasks, e.g. Remove-Item, then we may run into unintended issues.

Now, let's move on to investigate why a seemingly innocuous command, Test-Connection, would cause a different outcome. The earlier Access Denied error message gives me a lead that something around authentication seems to be causing the difference. Let's close the PowerShell prompt and re-open it. Let's re-run the same first command:

$FileList = @(Get-ChildItem -Path C:\ -Depth 1 -Recurse -Force -Attributes !ReparsePoint);
$FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

And let's check our Credentials to make sure that we are running as Administrator:

PS C:\> $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
PS C:\> $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
True

Indeed, we are running the prompt as Administrator, so that's not the issue. Let's look more into the Identity:

PS C:\> $currentPrincipal.Identities


AuthenticationType : Kerberos
ImpersonationLevel : None
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : Contoso\HAL9256
Owner              : S-1-5-32-544
....
Token              : 3076
....

From this, we can see the "well known" group S-1-5-32-544 as administrators. Notice the Impersonation Level is "None". If we run the Test-Connection and the same commands:

Test-Connection -Computer D4700;
$FileList2 = @(Get-ChildItem -Path C:\ -Depth 1 -Recurse -Force -Attributes !ReparsePoint);
$FileList2 | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};

And get the principal object:

PS C:\> $currentPrincipal2 = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
PS C:\> $currentPrincipal2.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
True

This proves we are still running as administrator. Now let's look at the identity:

PS C:\> $currentPrincipal2.Identities


AuthenticationType : Kerberos
ImpersonationLevel : Impersonation
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : Contoso\HAL9256
Owner              : S-1-5-32-544
....
Token              : 4500
....

This time we see that the Impersonation Level is Impersonation. From the Impersonation Level Enum Docs:

Impersonation 3

The server process can impersonate the client's security context on its local system. The server cannot impersonate the client on remote systems.

When we did the Test-Connection it required a network resource on the local computer in order to perform the ping. In order to do this, it transparently elevated the prompt to Impersonate level from None (we can even see this happened because the Token Number changed). This has the side effect of allowing the Get-ChildItem to now have the elevated rights to access the system file Documents and Settings. i.e. like checking off "show hidden files" in File Explorer, and now allowing it to enumerate through the Reparse Point.

We can also observe that this is the cause when we remote into another machine:

PS C:\> Enter-PSSession -ComputerName RemoteMachine -Credential (Get-Credential) -Authentication Kerberos

[RemoteMachine]: PS C:\Users\HAL9256\Documents> $FileList = @(Get-ChildItem -Path C:\ -Depth 1 -Recurse -Force -Attributes !ReparsePoint);
[RemoteMachine]: PS C:\Users\HAL9256\Documents> $FileList | Where-Object {$_.DirectoryName -like "*Documents and Settings*"};


    Directory: C:\Documents and Settings


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a-hs-        7/16/2016   7:21 AM            174 desktop.ini


[RemoteMachine]: PS C:\Users\HAL9256\Documents>
[RemoteMachine]: PS C:\Users\HAL9256\Documents> $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
[RemoteMachine]: PS C:\Users\HAL9256\Documents> $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
True
[RemoteMachine]: PS C:\Users\HAL9256\Documents>
[RemoteMachine]: PS C:\Users\HAL9256\Documents> $currentPrincipal.Identities

AuthenticationType : Kerberos
ImpersonationLevel : Impersonation
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : Contoso\HAL9256
Owner              : S-1-5-32-544
....
Token              : 4420
....

When we do this, we see that it returns Documents and Settings without needing Test-Connection to be executed first. Looking at the Principal, we see that the Impersonation Level is set to Impersonation.

Ultimately this tells us that the cause of this incongruity is the Impersonation Level setting. In order to access the hidden system file, and Reparse Point, we need the Impersonation Level set to at least Impersonation.

like image 169
HAL9256 Avatar answered Oct 16 '22 14:10

HAL9256