How to create a com object in another domain? - powershell

I'm trying to use PowerShell to quickly find the Scheduled Tasks in the root folder of a remote server. I find all sorts of scripts that others have written, but they're either looking at the localhost or on a server in the same domain. I support servers in dozens of domains, so I need some way to pass along credentials.
Here's the meat of my script:
$server = "<computername>"
$schedule = new-object -com("Schedule.Service")
$Schedule.connect($server)
$folder = $schedule.GetFolder("")
$tasks = $folder.GetTasks("")
foreach($task in $tasks) {
if (($task = $Folder.GetTasks(0))) {
$Tasks| ForEach-Object {[array]$results += $_}
$Tasks | Foreach-Object {
New-Object -TypeName PSCustomObject -Property #{
'Name' = $_.name
<etc.>
<etc.>
}
}
}
That code works fine either on my localhost or a server in the same domain as my workstation. In other scripts, I use Get-Credential to create $creds and (in various ways) pass that to the appropriate cmdlet. But with this one, I'm not sure. 'New-Object' doesn't accept a -Credential parameter. I've tried wrapping various parts inside an Invoke-Command scriptblock, since that accepts -Credential, but it fails in various ways. I'm not sure what needs to be wrapped in Invoke-Command--just the new-object? The foreach loop? The entire thing?
Thanks in advance.

When doing the Connect call, you can pass the server, domain, username, and password:
$Schedule.Connect($serverName, $user, $domain, $password);
This should allow you to use that object on the new domain.
MSDN Reference

Related

Passing active Powershell PSSession connection as argument in Start-Job

I am writing a script which gathers data from Exchange Online concerning mailbox permissions for each mailbox in our organization. To do this I gather all mailbox data into a variable then I use foreach to iterate through each mailbox and check the mailbox permissions applied to it. This takes time when you are working with over 15000 mailboxes.
I would like to use Powershell Jobs to speed this process up by having multiple jobs checking permissions and appending them to a single CSV file. Is there a way to pass an active PSSession into a new job so that the job "shares" the active session of the parent process that spawned the job and does not require a new one to be established?
I could place a New-PSSession call into the function but Microsoft has active session limits in Exchange Online PSSessions so it would limit the number of jobs I could have running at one time to 3. The rest would have to be queued through a while loop. If I can share a single session between multiple jobs I would be limited by computer resources rather than connection restrictions.
Has anyone successfully passed an active PSSession through to a job before?
Edit:
I've been working on using runspaces to try to accomplish this with Boe Prox's PoshRSJobs module. Still having some difficulty getting it to work properly. Doesn't create the CSV or append to it but only if I try to sort out the permissions within the foreach statement. The Write-Output inside the scriptblock only outputs the implicit remoting information too which is odd.
Code is below.
Connect-ToOffice365TenantPSSession
$mailboxes = Get-Mailbox -ResultSize 10 -IncludeInactiveMailbox
$indexCount = 1
foreach ($mailbox in $mailboxes) {
$script = #"
`$cred = Import-Clixml -Path 'C:\Users\Foo\.credentials\StoredLocalCreds.xml'
`$o365Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid/" -Credential `$cred -Authentication Basic -AllowRedirection
Import-PSSession `$o365Session -CommandName #('Get-Mailbox','Get-MailboxPermission')
`$internal_mailbox = `$Using:mailbox
`$mailboxPermissions = `$internal_mailbox | Get-MailboxPermission
foreach (`$permission in (`$mailboxPermissions | Where-Object {`$_.User -match 'tenantName|companyDomain'}))
{
`$userPermissions = `$permission | Select-Object Identity, User, AccessRights
`$permissionObject = [PSCustomObject]#{
"MailboxName" = `$userPermissions.Identity
"MailboxAddress" = `$internal_mailbox.PrimarySmtpAddress
"MailboxType" = `$internal_mailbox.RecipientTypeDetails
"UserWithAccess" = `$userPermissions.User
"AccessRights" = `$userPermissions.AccessRights
}
if (Test-Path 'C:\Scripts\MailboxPermissions.csv') {
`$permissionObject | Export-Csv 'C:\Scripts\MailboxPermissions.csv' -NoTypeInformation -Append
} else {
New-Item -Path 'C:\Scripts\MailboxPermissions.csv'
`$permissionObject | Export-Csv 'C:\Scripts\MailboxPermissions.csv' -NoTypeInformation -Append
}
Write-Output `$permissionObject
}
"#
$scriptBlock = [scriptblock]::Create($script)
$continue = $false
do
{
if ((Get-RSJob | Where-Object {$_.State -eq "Running"}).count -lt 3) {
Start-RSJob -Name "Mailbox $indexCount" -ScriptBlock $scriptBlock
$indexCount++
$continue = $true
}
else {
Start-Sleep 1
}
} while ($continue -eq $false)
}
Get-RSJob | Receive-RSJob
Thanks for the suggestions.
You have specifications here, but you are not showing code and yet, asking for an opinion.
That is, as many would say here, off topic, because the goal here is to assist with code that is not working or the like. So, at this point, have you tried what you are asking, and if so, what happened?
So, IMHO … Yet, since you are here. let's not send you away empty handed.
As per a similar ask here, the accepted answer delivered was...
… you have a current PSSession opened up on your console and then you
are attempting to use that same session in a background job. This will
not work because you are using two different PowerShell processes and
are unable to share the live data between the runspaces as it is
deserialized and loses all of its capabilities. You will need to look
at creating your own PowerShell runspaces if your goal is to share
live variables across multiple PowerShell sessions.
Even with the above, you still have those consumption limits you mention and technically based on your use case, you'd more than likely end up with serious performance issues as well.

New-Object -ComObject -Servername

I wanted to know in PowerShell if there is a way New-Object -ComObject can be run against a remote server like how it's possible natively in VBScript WScript.CreateObject("COM Server", "RemoteServerName").
If not, is there an option without having to rely on PS Remoting technologies?
I don't think it's possible to instantiate remote COM objects via New-Object. You should be able to get the desired result via the GetTypeFromProgID() and CreateInstance() methods, though:
$server = 'RemoteServerName'
$type = [Type]::GetTypeFromProgID('COM Server', $server, $true)
if ($type) {
$obj = [Activator]::CreateInstance($type)
}
[source]

X509Store.Open() throwing an Exception

why does $store.Open($openFlags) throw an exception, and is there a better way than my "work around" to make it work?
<#
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
$openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed
$store.Open($openFlags) #Exception calling "Open" with "1" argument(s): "The parameter is incorrect.
#>
#Work Around:
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
$openFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed
$startIndexOfStoreName = $store.Name.LastIndexOf("\") + 1
$lengthOfStoreName = $store.Name.Length - $startIndexOfStoreName
$storeNameString = $store.Name.Substring($startIndexOfStoreName, $lengthOfStoreName)
$storeName = [System.Security.Cryptography.X509Certificates.StoreName]$storeNameString
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, $store.Location)
$store.Open($openFlags) #No Exception thrown!
Update: Seems as though when using the X509Store(String) constructor, you are NOT allowed to have any slashes (correct me if I'm wrong). So $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My") works.
Define you certificate store using
$store = Get-Item "Cert:\CurrentUser\My"
instead of
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Cert:\CurrentUser\My")
To be honest I'm still trying to figure out why it works, or how.
The first method returns a $store called "My", so I'm assuming that it targets the store specifically and you can open it with
$store.Open($openFlags)
The second method returns a $store called "Cert:\CurrentUser\My". Open method on this will fail.
I wanted to comment on this, since, as is already pointed out, "the mixing of .NET Framework and the use of PowerShell Providers" in the previous examples. For me, I needed this to work as a pure .NET way of getting the certs to test out some C# equivalent code without the full development environment on a users computer.
Here's what I came up with, which worked:
$Location = [Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
$StoreName = [Security.Cryptography.X509Certificates.StoreName]::My
$Store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $StoreName, $Location
$OpenFlags = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly
$Store.Open($OpenFlags)
$Store.Certificates
Actually, you are mixing methods. One is via a provider (Cert:) the other is a .Net type (X509Store). Very different processes for attaching to the cert stores and pulling cert details.
Think of "Cert:" like a PSDrive (which it basically is). So you can get-childitem, etc. and don't need to "open" the store. In this mindset, the cert store locations are folders, and certs are individual objects:
# List the store locations
gci Cert:\
# List store names in CurrentUser store location
gci Cert:\CurrentUser
# List certs in the My store of CurrentUser store location
gci Cert:\CurrentUser\My | format-list
The catch to using the Cert: provider is that if you want to work with certs on remote systems, remoting (WinRM) needs to be enabled so you can "Invoke-Command". Not every environment allows this. That is where the .Net X509Store comes in. Not sure how well it works with "CurrentUser", but I've never been concerned about that - I am more interested in what is in the "LocalMachine" stores (specifically "My" since that is where the system holds web and auth certs). Modified snippet to list these certs (pulled from a script I built for interrogating all the servers in SharePoint farms).
# Change as necessary
$strTarget = $env:computername
$strCertStoreLocation = 'LocalMachine'
$strCertStoreName = 'My'
# Set up store parameters, connect and open store
[System.Security.Cryptography.X509Certificates.StoreLocation]$strStoreLoc = [String]$strCertStoreLocation
[System.Security.Cryptography.X509Certificates.StoreName]$strStoreName = [String]$strCertStoreName
$objCertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList "\\$($strTarget)\$($strStoreName)", $strStoreLoc
$objCertStore.Open('ReadOnly')
# List cert details in bulk
$objCertStore.Certificates | Format-List
# List specific props
foreach ($Cert in $objCertStore.Certificates) {
"Subject: $($Cert.Subject)"
"Issuer: $($Cert.Issuer)"
"Issued: $($Cert.NotBefore)"
"Expires: $($Cert.NotAfter)"
""
}
For a bit more details about each, hit up your favorite tech repository (MSDN, PowerShell.org, Hey Scripting Guy, etc.) :)

Powershell - Compiling with Modules

I have created a log-on script based on Active-Directory Module, that queries the user group membership in order to map his drives etc.
I have compiled it with PowerGui, and created an EXE file.
the problem is, the module doesn't exist on the users computers.
Is there a way to do this without the module, or add the module to the compilation?
For group memberships, you could also get it without connecting to AD, and parse the output of the WHOAMI utility
$groups = WHOAMI /GROUPS /FO CSV | ConvertFrom-Csv | Select-Object -ExpandProperty 'Group Name'
if($groups -contains 'group1')
{
do something
}
One way is using Active-Directory Service Interface (ADSI).
you can find in another SO post (Can I match a user to a group accross different domains?) one way to find all the groups a user belongs to, using ADSI, in the post it's a C# code, but it's easy to translate.
Here is a small example of a simple search to begin.
Clear-Host
$dn = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://WM2008R2ENT:389/dc=dom,dc=fr","jpb#dom.fr","Pwd")
# Look for a user
$user2Find = "user1"
$Rech = new-object System.DirectoryServices.DirectorySearcher($dn)
$rc = $Rech.filter = "((sAMAccountName=$user2Find))"
$rc = $Rech.SearchScope = "subtree"
$rc = $Rech.PropertiesToLoad.Add("mail");
$theUser = $Rech.FindOne()
if ($theUser -ne $null)
{
Write-Host $theUser.Properties["mail"]
}
Another way is to use System.DirectoryServices.AccountManagement Namespace.
This way is also using ADSI, but it's encapsulated, and you need the Framework .NET 3.5. You will also find in the same post but in the Edited (2011-10-18 13:25) part, a C# code using this way.
You can also use WMI :
$user2Find = "user1"
$query = "SELECT * FROM ds_user where ds_sAMAccountName='$user2find'"
$user = Get-WmiObject -Query $query -Namespace "root\Directory\LDAP"
$user.DS_mail
You can use this solution localy on your server or from a computer inside the domain, but it's a bit more complicated to authenticate to WMI from outside the domain.

OpenRemoteBaseKey() credentials

I'm attempting to use powershell to access a remote registry like so:
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $server)
$key = $reg.OpenSubkey($subkeyPath)
Depending on some factors that I'm not yet able to determine I either get
Exception calling "OpenSubKey" with "1" argument(s): "Requested registry access is not allowed."
Or
System.UnauthorizedAccessException: Attempted to perform an unauthorized operation.
at Microsoft.Win32.RegistryKey.Win32ErrorStatic(Int32 errorCode, String str)
at Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(RegistryHive hKey, String machineName)
It seems pretty clear that this is because the user I'm running the powershell script as doesn't have the appropriate credentials to access the remote registry. I'd like to be able to supply a set of credentials to use for the remote registry access, but I can find no documentation anywhere of a way to do this. I'm also not clear on exactly where to specify which users are allowed to access the registry remotely.
Just thought I'd add my answer to anyone with this problem as well. It seems there is no way to add Credentials using RemoteRegistry. You can however use WMI to query a remote registry using alternative credentials as follows:
$reg = Get-WmiObject -List -Namespace root\default -ComputerName RemotePC -Credential "Domain\User" | Where-Object {$_.Name -eq "StdRegProv"}
From here you can call standard Registry methods. The below example will return the operating system.
$HKLM = 2147483650
$reg.GetStringValue($HKLM,"SOFTWARE\Microsoft\Windows NT\CurrentVersion","ProductName").sValue
Hope this helps someone :)
Are you running remote registry service? It is disabled by default and that must be causing the issue. Check the status of this service on all remote machines you are trying to access.
I couldn't comment directly on bentaylr's entry above, but I've taken what he contributed and added PSCredentials creation (figured out from here) to allow you to hard code credentials into the script.
Peace of mind disclaimer: Be careful when using plaintext credentials in a script. In my case, I'm using generic credentials on machines I'm launching. Depending on your case, you might consider creating an encrypted credential file to store the password in (see link above).
The credentials you use would need to be able to access the registry if you were logged into that user on the machine you are targeting.
$user = "Domain\Username"
$pass = ConvertTo-SecureString "Password" -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $user,$pass
$reg = Get-WmiObject -List -Namespace root\default -ComputerName $server -Credential $cred | Where-Object {$_.Name -eq "StdRegProv"}
$HKLM = 2147483650
$value = $reg.GetStringValue($HKLM,"Software\Microsoft\.NetFramework","InstallRoot").sValue
$key.OpenSubKey($subkeyName) opens the subkey in write protected mode,
$key.OpenSubKey($subkeyName,$true) opens it in writable mode
Therefore after $key.OpenSubKey($subkeyName,$true) you should be able to create a new subkey or value
If you try the same thing after $key.OpenSubKey($subkeyName) you will get "UnauthorizedAccessException"
PS C:\>$regKey.OpenSubKey
OverloadDefinitions
Microsoft.Win32.RegistryKey OpenSubKey(string name, **bool Writable**)
try
PS C:\>$key.OpenSubKey($subkeyName,**$true**)
http://msdn.microsoft.com/en-us/library/xthy8s8d%28v=vs.110%29.aspx
Came looking for the answer to your question, but in a little googling this morning I noticed that the first parameter is a type rather than a String... hope this helps:
$machine = "<Machine Name Goes Here>"
$type = [Microsoft.Win32.RegistryHive]::LocalMachine
$regkey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($type,$machine)
$subkey = $regKey.OpenSubKey($key)
foreach ($sub in $regKey.GetSubKeyNames()){$sub}
I wanted to first thank all for answers above really helpful, wanted to add that you can use Get-Credential command to collect credentials without having to hard code it in your script. I have written using the above suggestions into my script the following code and query:
$userCredentials = Get-Credential -Credential <domain\username>
$objReg = Get-WmiObject -List -Namespace root\default -ComputerName $server -Credential $userCredentials | Where-Object{$_.Name -eq "StdRegProv"}
$subKeyNames = $objReg.EnumKey($HKLM,"SOFTWARE\Microsoft\Updates\Microsoft .Net Framework 4.5.1").sNames
The above code returns all sub key names in the specified key so that I can determine installed updates other than OS which have been applied to a server. If you want to determine all collection possibilities with the $objReg variable then run:
$objReg | Get-Member
You will see a list of all possible queries which can be performed against the registry. Hope this helps!