1
votes

I have an issue with a function I've wrote to return members of local groups when it is run against a remote machine. We use secondary domain accounts for admin privileges so I've used Invoke-Command so we can run the script block with that account, however, when I do this as opposed to running a new PowerShell window with my admin credentials, it can't enumerate members of the local group that aren't local users.

$computers = "blah"
$creds = Get-Credential
$sb = {
    param($c)
    Add-Type -AssemblyName System.DirectoryServices.AccountManagement
    $ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine
    $context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ctype,$c
    $idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName
    $lg = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($context,$idtype,"administrators")
    $members = $lg.Members
    return $members
}

foreach ($c in $computers) {
    if ($c -eq $env:COMPUTERNAME) { & $sb -c $c }
    else {
        Invoke-Command -ComputerName $c -Credential $creds -ScriptBlock $sb -ArgumentList $c
    }
}

When run against a machine that I'm logged in to locally it returns all members of the local group. It also works if I start a new console with my second account. If credentials are passed with Invoke-Command though, I receive errors pertaining to a lack of network access and it appears to happen after successfully listing the two local accounts on a machine first.

Information returned for lg variable when failing:

PSComputerName        : blah
RunspaceId            : hex...
IsSecurityGroup       : True
GroupScope            : Local
Members               : {local_admin, local_user}
Context               : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType           : Machine
Description           : Administrators have complete and unrestricted access to the computer/domain
DisplayName           :
SamAccountName        : Administrators
UserPrincipalName     :
Sid                   : SID...
Guid                  :
DistinguishedName     :
StructuralObjectClass :
Name                  : Administrators

When it's successful, the Members section includes domain groups and users too (identical results if remotely with shell run as second account or locally on the server logged on as second account):

IsSecurityGroup       : True
GroupScope            : Local
Members               : {local_admin, local_user, domain_group, domain_group, domain_user...}
Context               : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType           : Machine
Description           : Administrators have complete and unrestricted access to the computer/domain
DisplayName           :
SamAccountName        : Administrators
UserPrincipalName     :
Sid                   : SID...
Guid                  :
DistinguishedName     :
StructuralObjectClass :
Name                  : Administrators

Two different errors received with slightly different approaches:

An error occurred while enumerating through a collection: The network path was not found.
.
    + CategoryInfo          : InvalidOperation: (System.Director...ctionEnumerator:PrincipalCollectionEnumerator) [], RuntimeException
    + FullyQualifiedErrorId : BadEnumeration
    + PSComputerName        : blah

Cannot convert value "System.DirectoryServices.AccountManagement.PrincipalCollection" to type "System.Array". Error: "The network path was not found.
"
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException
    + PSComputerName        : blah

The first message is from just trying to return the members variable, the second one was when I tried to make that variable an array. I think they essentially have the same root cause. I tried adding the -EnableNetworkAccess switch to Invoke-Command, but this doesn't change the error received.

I appreciate that I already know a way to make this work, but we'd like to see if there is any way around running the shell with admin credentials and only introducing them when they need to be passed for remote servers. It doesn't appear to be an authentication issue because we can run more simple commands using Invoke-Command i.e. ipconfig or whoami.

I'm using PowerShell 5.1

Thanks.

2

2 Answers

0
votes

PoSH remoteing requires the account to be a local admin on the target host, with the exception of the commands listed below. Is this...

'We use secondary domain accounts for admin privileges'

... a local admin?

https://technet.microsoft.com/en-us/library/ff699046.aspx Some cmdlets have a –ComputerName parameter that lets you work with a remote computer without using Windows PowerShell remoting. This means you can use the cmdlet on any computer that is running Windows PowerShell, even if the computer is not configured for Windows PowerShell remoting. These cmdlets include the following

If you are trying to remote across domain, you are going to get hit with the Windows double hop restriction and you'll need to plan and configure for that type of scenario. See the below.

https://blogs.msdn.microsoft.com/clustering/2009/06/25/powershell-remoting-and-the-double-hop-problem An Easy “Double-Hop” Solution

You can use CredSSP to delegate your credentials to the remote computer so every remote access from the remote machine will also work. To enable this, you will need to run (from an elevated command prompt) the following command on the client machine:

Or this option...

https://blogs.technet.microsoft.com/ashleymcglone/2016/08/30/powershell-remoting-kerberos-double-hop-solved-securely Are you facing issues with PowerShell remoting and credentials? You remote into your jump box, but then any remoting beyond there gets a big red ACCESS DENIED. Maybe you’ve tried CredSSP, but people say that isn’t safe. Read today’s post for a completely legit, secure, safe, and easy way to enable Kerberos double hop for PowerShell remoting.

0
votes

Thanks, postanote, for your links.

In the end I've looked at a process of invoking the command using the second account credentials locally while remaining in the same shell. This is done by creating a script file and running it in a separate process, capturing the standard output/error and parsing the returned string. I hope this makes sense, I'm terrible at explaining things sometimes.

Here's what I came back with: (I should say here that I create custom .NET types as part of a separate script that loads as part of the module manifest that imports the functions)

[CmdletBinding(SupportsShouldProcess)]
Param (
    [string[]]$computer = $global:LoggedOnUser.Computer,
    [string]$group = "administrators",
    [System.Management.Automation.PSCredential]$creds = $global:AdminUser.Credentials
)
Begin {
    # Initialise output array
    $groupArray = @()

    # Check C:\Temp exists locally
    if (!(Test-Path C:\Temp)) { New-Item -Path C:\Temp -ItemType Directory -Force }

    # Run this locally with second administrator account in a new process
    $secondAccountSB = {
        param([System.Management.Automation.PSCredential]$creds = $creds,$c = $c, $group = $group)
        $prcsi = New-Object System.Diagnostics.ProcessStartInfo "PowerShell"
        $prcsi.Arguments = "-File C:\Temp\Local_Group_Check.ps1 -c $c -group $group"
        $prcsi.RedirectStandardError = $true
        $prcsi.RedirectStandardOutput = $true
        $prcsi.LoadUserProfile = $false
        $prcsi.UserName = $creds.UserName
        $prcsi.Password = $creds.Password
        $prcsi.Domain = "Domain"
        $prcsi.UseShellExecute = $false
        $prcsi.WorkingDirectory = "C:\Temp"
        $prc = New-Object System.Diagnostics.Process
        $prc.StartInfo = $prcsi
        $prc.Start() | Out-Null
        $prc.WaitForExit()
        $output = $prc.StandardOutput.ReadToEnd()
        $failed = $prc.StandardError.ReadToEnd()
        $output
        $failed
    }

    # Create string for a script
    $groupReportSB = "
        param(`$c,`$group)
        # Add Directory Services .NET Type
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        `$ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine

        # Initialise array
        `$localGroups = @()

        # Connect to local Directory Services
        `$context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList `$ctype,`$c
        `$idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName
        Try { `$lg = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity(`$context,`$idtype,`$group) }
        Catch [System.Management.Automation.MethodInvocationException] {
            `$errorMessage = `$_.Exception.InnerException.Message
            Write-Host `"`$($`c.ToUpper()): `$(`$errorMessage.Trim())`" -ForegroundColor Yellow -BackgroundColor Black
            continue
        }

        # Obtain a list of each local group's members
        `$members = `$lg.Members

        # Create PSObject
        foreach (`$member in `$members) {
            if ((`$member.StructuralObjectClass -eq `$null) -or (`$member.StructuralObjectClass -ilike `"user`")) {
                `$groupMembers = `"N/A`"
            }
            else { [string]`$groupMembers = `$member.Members -join `"; `" }
            `$gm = New-Object -TypeName psobject
            `$gm | Add-Member -MemberType NoteProperty -Name Account -Value `$member.SamAccountName
            `$gm | Add-Member -MemberType NoteProperty -Name Computer -Value `$c.ToUpper()
            `$gm | Add-Member -MemberType NoteProperty -Name Description -Value `$member.Description
            `$gm | Add-Member -MemberType NoteProperty -Name Object -Value `$member.StructuralObjectClass
            `$gm | Add-Member -MemberType NoteProperty -Name Group -Value `$lg.Name
            `$gm | Add-Member -MemberType NoteProperty -Name Members -Value `$groupMembers
            `$gm | Add-Member -MemberType NoteProperty -Name Context -Value `$member.ContextType
            `$gm | Add-Member -MemberType NoteProperty -Name Scope -Value `$member.GroupScope
            `$localGroups += `$gm
        }

        # Return Local Groups for this machine
        `$localGroups
    "
}
Process {
    $count = 1
    $total = $computer.Count
    foreach ($c in $computer) {
        Write-Host "[$($count)/$($total)] Collecting information from $c..."
        if (Test-Path C:\Temp\Local_Group_Check.ps1) { Remove-Item C:\Temp\Local_Group_Check.ps1 -Force }
        $groupReportSB | Out-File -FilePath C:\Temp\Local_Group_Check.ps1 -Force -NoClobber
        $returnedOutput = & $secondAccountSB -c $c -group $group
        $groupArray += ConvertTo-WinPSObject -inputObject $returnedOutput -dataType LocalGroups
        $count ++
    }
}
End {
    # Return Group Array
    $groupArray
}

As I said, the purpose of this was to make it fit along side other admin functions that have better functionality for running remotely, and to keep the user in the same shell that isn't being run the whole time with a secondary, elevated account.

I know the code isn't displaying properly, but it will in an editor with PS syntax functionality.