I wanted to grab all my old posts from 2006-2010 from a now-gone blogger.com (I think?) I used. As part of that I
- Converted the rest of this blog's front-end to react (sort-of, it works for viewing but I have other plans for the CU part of the CRUD).
- Migrated the PostgreSQL from my desktop into a docker container
- Setup proper backups for it
- Added proper Cloudflare proxy with the correct origin certificate config
- Rolled in PowerShell highlight.js and fixed some issues with CDN/build of that.
- A couple other things but I'm just noting those because I need to write those up too... So I guess here we go!
I was a bit "spicy" back then as a less experienced person when social media and "everything lives forever on the internet" wasn't such a thought. So I'm sucking stuff out of archive.org and cleaning some of the sass & spice. Some of the stuff was very much PowerShell V1 and solutions to problems that are no longer good ideas, so I'll leave off the obvious deprecated stuff.
Now that all that's in place, here's the first few. I'll update this as I'm able to pull out more.
I'll maybe just put the most interesting one first. I never had a chance to polish it because it was deprecated and no longer needed before I had any time to return to it but I think there was some neat things.
param(
[string]$OU = $(throw "Sorry, you must supply an OU. Bye!")
)
#TODO don't pass the OU in a this way.
$SearchRoot = [adsi]"LDAP://OU=$OU"
$filter = "(&(&(&(&(mailnickname=*)(objectCategory=person)(objectClass=user)(msExchMailboxSecurityDescriptor=*)))))"
#$filter = "(&(&(&(&(mailnickname=cs)(objectCategory=person)(objectClass=user)(msExchMailboxSecurityDescriptor=*)))))"
$ds = New-Object DirectoryServices.DirectorySearcher($SearchRoot,$filter)
$ds.PageSize = 5000
$ds.PropertiesToLoad.Addrange(@("msExchMailboxSecurityDescriptor","userPrincipalName","objectSid"))
$ds.FindAll() | %{
$user = $_.GetDirectoryEntry()
Write-host -for Red "$($user.userPrincipalName)"
#here the DaclByte is a byte[] that is the secuirtydescriptor
[byte[]]$byteDACL = ($_.properties)["msexchmailboxsecuritydescriptor"][0]
###########################################################################
#using an activeDirectorySecurity to manipulate the securityDescriptor
#GetAccessRule is actually inherited from System.Security.AccessControl.DirectoryObjectSecurity, what's the point of using the child class?
#http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.directoryobjectsecurity.getaccessrules.aspx
#AuthorizationRuleCollection GetAccessRules(bool includeExplicit,bool includeInherited,Type targetType)
###########################################################################
$adDACL = new-object System.DirectoryServices.ActiveDirectorySecurity
#setSecurity...() takes a byte[] and will let us translate.
$adDACL.SetSecurityDescriptorBinaryForm($byteDACL)
$ACLs = $adDACL.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
$ACLs | %{
if(($_.IdentityReference).IsAccountSid()){
#translate will barf if the account does not exist (say from an old Domain trust). So I'm checking $? and not bothering to list permissions for missing accounts.
#and hiding errors by changing EAP
$oldEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
$name = ([System.Security.Principal.SecurityIdentifier]$_.IdentityReference).translate([System.Security.Principal.NTAccount])
$exist = $?
$ErrorActionPreference = $oldEAP
if($exist){
$permissions = @()
write-host -for CYAN "`t$name"
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::CreateChild){
$ar = "FullAccess"
"`t`t$ar"
$permissions += ,$ar
}
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::WriteOwner -ne 0){
$ar = "ChangeOwner"
"`t`t$ar"
$permissions += ,$ar
}
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::WriteDacl){
$ar = "Modify User Attributes"
"`t`t$ar"
$permissions += ,$ar
}
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ListChildren){
$ar = "Is mailbox primary owner of this object"
"`t`t$ar"
$permissions += ,$ar
}
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::Delete){
$ar = "DeleteItem"
"`t`t$ar"
$permissions += ,$ar
}
If ($_.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ReadControl){
$ar = "ReadPermission"
"`t`t$ar"
$permissions += ,$ar
}
}
}
}
#TODO fix checks on invalid SIDs
$sendAs = $user.psbase.get_objectSecurity().getAccessRules($true,$false, [System.Security.Principal.SecurityIdentifier]) | ?{$_.ObjectType -eq "ab721a54-1e2f-11d0-9819-00aa0040529b"}
$sendAs | %{
if(($_.IdentityReference).IsAccountSid()){
#translate will barf if the account does not exist (say from an old Domain trust). So I'm checking $? and not bothering to list permissions for missing accounts.
#$oldEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
$name = ([System.Security.Principal.SecurityIdentifier]$_.IdentityReference).translate([System.Security.Principal.NTAccount])
#$ErrorActionPreference =$oldEAP
"`t`tSEND AS: $name"
$permissions += ,$ar
}
}
$recvAs = $user.psbase.get_objectSecurity().getAccessRules($true,$false, [System.Security.Principal.SecurityIdentifier])| ?{$_.ObjectType -eq "ab721a56-1e2f-11d0-9819-00aa0040529b"}
$recvAs | %{
if(($_.IdentityReference).IsAccountSid()){
#translate will barf if the account does not exist (say from an old Domain trust). So I check $? and not bothering to list permissions for missing accounts.
#$oldEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
([System.Security.Principal.SecurityIdentifier]$_.IdentityReference).translate([System.Security.Principal.NTAccount])
#$ErrorActionPreference = $oldEAP
$permissions += ,$ar
"`t`tRECEIVE AS: $name"
}
}
#TODO return objects instead of tab formatted strings.
#Write-host -for Red "$($user.userPrincipalName)"
#"`t`t$permissions"
}
Exchange 2003 mailbox permissions.
Exchange 2007 provides different methods for these tasks, so I’m mainly preserving this for postarity as I close in on decomissioning my remaining 03 servers. At some point I might need some of the functions or casts in here so I’m publishing something I’m not proud of. If you like feel free to fix the issues, it’s bare minimum functionality at the moment, but like I said– it gets the job done. After running the script as an audit, it was clear that the number of permissions I had to deal with in this migration made it not worth my time to automate the migration of them across forests.
AD Schema, Indexed Attributes
I needed to optimize a query and make sure that I was only hitting indexed attributes.
So I’d better find out what’s being indexed. Each piece of the schema is stored as an LDAP object, and each of those has a bitwise property named SearchFlags where 1=Index.
#Connect to the rootDSE
$dse = [adsi]"LDAP://rootDSE"
#Look up the schema naming context
$schema = [adsi]"LDAP://$($dse.schemaNamingContext)"
#Build a directory searcher
$ds = new-ojbect DirectoryServices.DirectorySearcher
#LDAP queries can have a built-in bitwise AND
$bAND1 = "1.2.840.113556.1.4.803:=1"
$ds.SearchRoot = $schema
$ds.Filter = "(&(objectclass=attributeSchema)(searchFlags:$bAND1))"
$indexed = $ds.FindAll()
$indexed | ft {$_.properties.cn}
Of note here is the handy bitwise flags I can put in my filter. It looks like “attributename:ruleOID:=value” where ruleOID for Bitwise AND is 1.2.840.113556.1.4.803. The last line I’m not dealing with a DirectoryServices.DirectoryEntry object, but with a DirectoryServices.SearchResult, so I have to drill through the properties attribute to get the cn.
When I’m working with scripts interactively like this, I often chop up an array on the fly using a range operator so I don’t send thousands of objects down the pipeline like so: $array[0..4] | %. This is fine most of the time but does get a bit funky when dealing with children. Initially, I had been working with $schema.children. Trying to specify an index or range objects with “Unable to index into an object of type System.DirectoryServices.DirectoryEntries”. Not something that inherits the full collection interface, but no matter- it’s easy enough to just force it to an array. Found this gem from archived.
$children = $schema.children
#fails:
$children[0..4]
#forcing to an array: win
(@($children))[0..4] | Format-List
A client wanted a password expiration date check, and ANR is a way for me to be lazy.
get-mailbox *@example.com | %{
$ds = New-Object DirectoryServices.DirectorySearcher
$ds.Filter = "(UserPrincipalName=$($_.UserPrincipalName))"
$res = $ds.findall()
"$($_.UserPrincipalName)`n" + [datetime]::FromFileTime( $($res[0].Properties.pwdlastset))
}
This being a very fast answer to the problem has a few touches needed if I want to consider this ready to reuse:
I should not be using Exchange cmdlets when I’m building a new DirectorySearcher anyway. I need to add checks for the bitwise parameter to catch “password never expires” where set. But it’s the week of 2010 scripting games. I’ll add these to the list of things I need to revisit.
Take some Powershell Exchange inbox!
Raw snippet, but very cool. Will need to do a lot of cleanup and turn into a module, but just playing around I’m pretty excited about being able to get my email from Powershell… and without Outlook. More to come.
Install EWS managed API from here http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1
#default install path:
$dllpath = "C:Program FilesMicrosoftExchangeWeb Services1.0Microsoft.Exchange.WebServices.dll"
[Reflection.Assembly]::LoadFile($dllpath)
$s = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$s.Credentials = Net.NetworkCredential('myUserName', 'MyPassw0rd', 'netBIOSdomainName')
$s.AutodiscoverUrl("[email protected]")
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($s,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
$folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(25)
#show folders in inbox, with some interesting properties
$inbox.FindFolders($folderView) |ft DisplayName,TotalCount,ChildFolderCount -AutoSize
#load the rest of the items to use for the MAPI propertySet
$pSubject = [Microsoft.Exchange.WebServices.Data.ItemSchema]::subject
$pAttachments = [Microsoft.Exchange.WebServices.Data.ItemSchema]::HasAttachments
$pReceived = [Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived
#make a view (25 deep, no offset)
$ItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(25)
$ItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
$ItemView.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, $pSubject, $pAttachments, $pBody, $pReceived)
#find, show!
$res = $inbox.FindItems($itemView)
$res | ft DateTimeReceived,HasAttachments,Subject -AutoSize
Logon Scripting with .vbs, Deleting the Outlook Profile, Registry and AD
VBS? Is this still a thing? Grabbing the script, formatting prob won't work properly, but at least I have it in my DB now since I'm plucking stuff from archive.org.
groupDN = "CN=lescript,OU=lescript,OU=Department,DC=FABRICAM,DC=DOM"
Set objSysInfo = CreateObject("ADSystemInfo")
Set user = getObject("LDAP://" & objSysInfo.UserName)
on error resume next
memberOf = user.getex("memberOf")
If (Err.Number <> 0) then
'you are here if the user is a member of no groups.
'msgbox("you are member of no groups")
On Error GoTo 0
else
For Each objGroup in memberOf
'msgbox("objgroup,groupDN is: " & vbcr & objGroup & vbcr &groupDN)
If objGroup = groupDN Then
'Checks completed. "Work" code goes here.
End If
Next
End If
Assigning a logon script to a user or group.
Using AD groups from VBScript.
Getting the current AD user information from a logon script.
Enumerating AD Group membership from vbs.
A more familiar way to .NET programmers using DirectoryServices.DirectoryEntry (I found this after I had a working script)
Assigning a logon script to a user or group.
Using AD groups from VBScript.
Getting the current AD user information from a logon script.
Enumerating AD Group membership from vbs.
A more familiar way to .NET programmers using DirectoryServices.DirectoryEntry (I found this after I had a working script :( )
Next: Execution should be once, and only once. We don’t want to be deleting the Outlook profile every time a user logs on.
Set wmiLocator = CreateObject("WbemScripting.SWbemLocator") 'to get to stdRegProv
Set wshNetwork = CreateObject("WScript.Network") 'used to determine local machine name
'msgbox("name: " & WshNetwork.ComputerName"
set wmiNS = wmiLocator.ConnectServer(wshNetwork.ComputerName,"rootdefault") 'WMI namespace
set objReg = wmiNS.Get("StdRegProv")
In this snippet, I go through the steps required to get a connection to the local registry. More on working with the registry and keys from vbs. The comments are pretty self-explanatory, and the good news is that because I’m only looking at the HKCU hive, I’m do not need to use impersonation.
if objGroup = groupDN then
set wsh = createobject("wscript.shell")
on error resume next
key = wsh.RegRead("HKCUProfileDeleted")
'when err = (hex)80070002, key does not exist
if err.number <> 0 then
if hex(err.number) = 80070002 then
'msgBox("Script set to run: deleting profile and adding reg. key")
'wsh.sleep 14000
'old way: lRC = DeleteRegEntry(HKEY_CURRENT_USER, profile)
wsh.RegWrite"HKCUProfileDeleted", "True", "REG_SZ"
wsh.run("regDelete.bat")
End If
Else
if key = "True" then
'msgBox("Profile has already been deleted, nothing will be done.")
ElseIf key = "False" then
'external editing of the key is the only way to get here.
'Either the key does not exist, or it's set to true by the script.
msgBox("Please report this message to support" & vbcr & _
"""Profile deletion key was set to False, no action was taken.""" & vbcr & _
"HKCUProfileDeleted")
Else
msgbox("Please report this error to support" & vbcr & _
"""Key set to something other than True or False""" & _
vbcr & "HKCUProfileDeleted" & vbcr & "Key value: " & key)
End If
End If
In this section (executed after the group membership tests above) I check for registry key named “HKCUProfileDeleted”. If it does NOT exist (Line 6 checks for this read error), then I add it, set it to “True”, and run the batch file “regDelete.bat”. The rest of the snippet contains a do-nothing check if they key has already been set (lines 14-15) and error handling should the key be set to “False” (16-21) or some other value (22-25). Since these states should never happen, I’m only kicking an error to a message box, and asking the user to report it.
Now the “meat”: Deleting the MAPI profile. Because the profile is nothing more than a stack of registry keys, it should be simple to delete right? Wrong. VBS apparently has no way to recursively delete a registry key without writing the recursion yourself. If that wasn’t enough, for some reason I’ve been unable to completely isolate, when I tried to recursively delete keys (line 9) with the sample function (recursion handled via vbs logic) found here kb279847,, some keys remain. I tried the standard programmer “wait 10″ solution in case something was accessing the MAPI profile and adding keys as I tried to delete them (line 8 ) but that yielded only slightly better results. Less subkeys remained, but full recursive deletion still failed.
So what to do? Well there’s Wscript.shell.Run(foo.exe) and that gives us the ability to run a file. So why not take the easy way out and just do a “reg /delete”?
rem reg delete /va /f "HKCUSoftwareMicrosoftWIndows NTCurrentVersionWindows Messaging Subsystem"
regedit.exe /s \FABRICAM.DOMSYSVOLLCS.CORPscriptsprofileDelete.reg
Line 0 fails. So I’m just moving on, I’m really tired of jumping through hoops at this point, but trial and error proves that line 1 (using regedit to load a .reg file) works out just fine. I saved the “profileDelete.reg” to the System Volume and tested successfully.
Yes, it seems like this should have been easier, but soon enough it won’t matter and I’ll be able to use PowerShell
remove-item -path "HKCUSoftwareMicrosoftWIndows NTCurrentVersionWindows Messaging Subsystem"
Until then the .reg file looks like this:
Windows Registry Editor Version 5.00
[-HKEY_CURRENT_USERSoftwareMicrosoftWindows NTCurrentVersionWindows Messaging Subsystem]
Forget it, I’ll use a .reg file
Removing the Outlook profile registry keys using regedit
More recursive registry vbs (.reg too)
The “bonus” requirement of locking users out of their mailboxes I’ll handle in my next post. Until then, you need an updated token– any ACL changes won’t take effect until you log out/in. Remember mailbox ACLs are held in the LDAP property “msExchMailboxSecurityDescriptor”, and you’d need to go through the painful process of decrypting that.
Move-Mailbox -AllowMerge selects the wrong target
Fist things first, I’m looking for clues on how the lookup is done on the target account. Loading up the move-mailbox log & casting it to XML makes it easy to work with.
$log = [xml](gc C:logdirlogfile)
With that I can select the 2 mailboxes with the failed targets and see if there’s anything suspicious. Looking at the “Target” node, it has little of interest, basically target forest GC and some other migration settings that have little if anything to do with the target account lookup
$log."move-Mailbox".TaskDetails.Item | ?{$_.MailboxName -like "*foo*" -or $_.MailboxName -like "*bar*"} | %{$_.Source}
SourceDatabase
Identity
IsResourceMailbox
SourceDomainController
SourceDirectReports
SourcePublicDelegatesBL
SourceDeliverAndRedirect
SIDUsedInMatch
MatchedContactsDNList
Alias
SourceGlobalCatalog
SourceServer
SourceAltRecipientBL
SourcePublicDelegates
IsMatchedNTAccountMailboxEnabled
DistinguishName
DisplayName
PrimarySmtpAddress
LegacyExchangeDN
SourceManager
SMTPProxies
MatchedTargetNTAccountDN
SourceAltRecipient
Of interest, MatchedTargetNTAccountDN is incorrect, proving that the cmdlet lookup had the wrong account selected. But why? Looking at the SIDUsedInMatch I figured I should check the SID.
get-mailbox foo | %{[adsi]"LDAP://$($_.DistinguishedName)"} | %{
$sid = New-Object System.Security.Principal.SecurityIdentifier $($_.objectSid),0
$sid
$sid.Translate([System.Security.Principal.NTAccount])
}
Good news, nothing matched. So the confusion must be coming from somewhere else. I know that the ‘sn’ is used in ANR so this could be a possibility, but I doubt it since it would cause problems with all the “Smith” surnames on the system.
After checking all this… I’m waiting on a response from PSS, I’ll update when I have it :)
Mapping WMI mailbox object to its AD user account (the “right” way)
From following @shaylevey (blog) on twitter, I found his answer to this. Sure it works, but as he notes “I choosed to use LegacyDN since MailboxGUID needs to be formatted first (remove the hyphens and curly braces)”. Technet agrees saying of the MailboxGUID, “It is a unique value that distinguishes an individual mailbox from all others.” I needed to do this anyway for some of my own reporting and the following snippet does the trick:
I load up $ds as a DirectoryServices.DirectorySearcher object (plenty on that if you google), then load $users with DirectoryServices.DirectoryEntry objects. I haven’t played much with the Quest AD cmdlets because I do enough AD work that it was worth learning more about wheels. One thing of note is that you can make this use ANR (Ambigious Name Resolution) exactly like the Get-Mailbox cmdlet on Exchange 2007 by setting the LDAP filter like so
$ds.Filter = "(&(objectclass=user)(anr=$anr))"
Using the type-accelerator [adsi] would accomplish the same thing, and you can cast with [adsi]“LDAP://$($foo.DistinguishedName)” where $foo is anything that you have handy to get DN property out of. Say the results of Get-Mailbox on Exchange 2007.
$users = $ds.findall() | %{$_.getDirectoryEntry()}
$allMBOs = gwmi -computer $myExchangeServer -NameSpace rootmicrosoftexchangev2 Exchange_Mailbox
$users | %{
$guid = ([guid]$($_.msExchMailboxGuid)).tostring()
$wmiInfo = $allMBOs | ?{$_.MailboxGUID -like "{$guid}" -and !$_.DateDiscoveredAbsentInDS }
#do something with this!
}
What are my physical Exchange 2007 mailbox servers (not cluster names)?
I need to get some WMI stats from the underlying physical database servers in my Exchange 2007 org. Following guidelines public folder servers should not be housed on clusters so will be installed on standalone servers. Some fairly simple powershell makes it easy enough to get all my physical servers, but sadly Get-ClusteredMailboxServerStatus returns an object Microsoft.Exchange.Management.SystemConfigurationTasks.CMSStatusEntry returns the names of the physical nodes, but pollutes them a bit with the status information ”<Active, Quorum Owner>”. These attributes may be split among servers if the cluster group and Exchange groups are split between the nodes. This ‘tacked on’ information in the string is space delimited so it’s not tough to yank it out.
Get-MailboxDatabase | sort-object -Unique server | %{
$server = Get-ExchangeServer $_.Server
if($server.IsMemberOfCluster -eq "Yes"){
"$($_.server) is a cluster, with nodes:"
(Get-ClusteredMailboxServerStatus $server).OperationalMachines | %{($_.split(' '))[0]}
}else{
"$($_.Server) is a stand alone"
#$_.Server.name
}
}
Comment out the quoted lines, and remove the comment and this snippet will return the computer names of all mailbox servers and mailbox cluster nodes. Be aware that this enables WMI use against each cluster node (for things like CPU/disk monitering) but the physical nodes will not be recognized as Exchange servers. If you are looking for something like Get-MailboxDatabase | %{$_.EdbFilePath} you will still need to use the cluster name, not that of the nodes.