Another permissions related gotcha is when performing the task under run as different user, I have notice the same failure when the user performing the action has sufficient rights via group membership on the target OU. When directly adding that user to the OU permissions, it allowed the creation of the computer object. Possibly some run-as activity isn't passing a full access token on the connections and the group membership is ignored?
Showing posts with label powershell. Show all posts
Showing posts with label powershell. Show all posts
Wednesday, March 20, 2024
New-ADComputer : A required attribute is missing
When trying to create a computer with this commandlet and you get this error, it may be a poorly worded exception. This error can come up if you don't have permissions to create computer objects on the OU you provided. If you follow the commandlet examples and provide what is required: samaccountname, name, and path; go check the OU permissions. If you had tried doing it with the older dsadd command, it will give an error that the modification was not permitted for security reasons. Once permissions are fixed, or a different set of properly delegated credentials are passed, it should work fine.
Tuesday, September 28, 2021
Capturing unique simple bind or unsigned ldap queries from a domain controller
Using get-winevent in powershell with XML filter, you can grab the 2889 events from the directory services log. These contain the username, and source IP. With some custom defined attributes within select-object along with an array, you can filter this down to unique connections.
$query = @"
<QueryList>
<Query Id="0" Path="Directory Service">
<Select Path="Directory Service">*[System[(EventID=2889)]]</Select>
</Query>
</QueryList>
"@
$somelistofdomaincontrollers | %{
$serv = $_
$hashes = @();
get-winevent -filterxml $query | select @{n="dc";e={$_.machinename}},
@{n="source";e={($_.properties.value[0].split(":"))[0]}},
@{n="user";e={$_.properties.value[1]}},
@{n='connhash';e={$str = ($_.machinename +
$_.properties.value[0].split(":"))[0] +
$_.properties.value[1]; $str.gethashcode()}} | %{
if ($hashes.contains($_.connhash)) {} else {$hashes += $_.connhash; $_|
select dc,source,user}
}
Wednesday, March 4, 2020
End to end network latency
When it comes to testing connectivity and latency, I've noticed that many IT technicians don't seem to have any tools in their skill set that go beyond a ping. While that works in many situations, there are often situations where ICMP traffic (including ping) is blocked. At this point, connectivity testing skill set often falls down to a telnet command to the port to see if its open, instead of using many already available tools like the powershell test-netconnection cmdlet. Unfortunately, that commandlet and telnet only show that you can connect to a port, and it doesn't tell you how long it takes to get to it. If you're on a windows machine, you can use the Test-PortLatency function that I've written below. This will give a rough idea of the time to connect in milliseconds to a remote tcp port. If you're on a linux machine, the nmap suite of tools has several programs that give latency information, like nping, or just nmap itself. There are other options as well, and typically some programming languages like python or perl available, which should have some capability to create a simple script to provide this information.
While these tools provide latency from one source point to another, you may find that you need to run tests from multiple points. Connections can get complicated with multiple layers and applications that give you a full end to end experience through multiple servers and protocols. In these cases, you will need to work through to determine what your various points in the connection are, and at what points you need to test from. For example, you may be doing remote desktop through a jump server (bastion host). Your workstation doesn't have direct access to the final remote desktop server. Testing connectivity from the jump server to the final destination only gives you part of the total round trip end to end connectivity. You will need to test from the workstation to the jump server, and then add the latency of the jump server to the final destination to get a rough idea of your end to end latency. If you can run ping's at the different layers, it will help give an idea of packet loss as well.
While these tools provide latency from one source point to another, you may find that you need to run tests from multiple points. Connections can get complicated with multiple layers and applications that give you a full end to end experience through multiple servers and protocols. In these cases, you will need to work through to determine what your various points in the connection are, and at what points you need to test from. For example, you may be doing remote desktop through a jump server (bastion host). Your workstation doesn't have direct access to the final remote desktop server. Testing connectivity from the jump server to the final destination only gives you part of the total round trip end to end connectivity. You will need to test from the workstation to the jump server, and then add the latency of the jump server to the final destination to get a rough idea of your end to end latency. If you can run ping's at the different layers, it will help give an idea of packet loss as well.
function Test-PortLatency {
param ( [parameter(mandatory=$true)][string]$Computer,
[parameter(mandatory=$true)][int]$Port,
[parameter(helpmessage="Timeout in milliseconds")]$timeout=10000
)
$starttime = get-date
$Testconn = New-Object Net.Sockets.TcpClient
$Testconn.BeginConnect( $computer, $Port, $Null, $Null ) | Out-Null
$MaxTimeout = ( Get-Date ).AddMilliseconds( $timeout)
$millisec = 0
While ( -not $Testconn.Connected -and ( Get-Date ) -lt $maxTimeout )
{
Sleep -Milliseconds 10
$ms += 10
}
$endtime = get-date
$result = new-object psobject
add-member -input $result NoteProperty Connected $($testconn.connected)
add-member -input $result NoteProperty Milliseconds $(($endtime - $starttime).totalmilliseconds)
if ($testconn.client.remoteendpoint -eq $null) {
$resultstr = "Connection_Refused"
} elseif ($result.milliseconds -gt $timeout) {
$resultstr = "Connection_TimedOut"
} elseif ($result.connected) {
$resultstr = "Successful_connection"
} else {
$resultstr = "status_unknown"
}
add-member -input $result NoteProperty Result $resultstr
$Testconn.Close()
$result
}
Wednesday, May 8, 2019
AD groups - setting group owner and delegating permission with powershell
Given that permissions delegation is only a simple checkbox in the AD users and computers tool under the manager's name, it would be nice if set-adgroup had a similar functionality. There are several steps involved in delegating rights, and if the owner is being changed, typically the old owner's rights should be removed. With this script below, it should accomplish this at least on a single domain environment. If the owner and group are in multiple domains, it can get a bit more complicated and some adjustments would be needed. This script takes either get-adgroup pipeline input, or a single group name that would work as an "identity" value in get-adgroup.
[CmdletBinding()]
param(
[parameter(helpmessage="samaccountname of group owner",mandatory=$true)]$owner,
#defaults to current user's domain if not specified
[parameter(helpmessage="netbios domain name")]$domain=$env:userdomain,
[parameter(parametersetname='pipe', valuefrompipeline=$true)]
[Microsoft.ActiveDirectory.Management.ADGroup]$group,
[parameter(parametersetname='normal',valuefrompipeline=$false, helpmessage="AD group name")]$identity,
#for cross domain? needs testing and what if the group and owner are in different domains?
[parameter(helpmessage="domain controller to do object lookups")]$server
)
begin{
import-module activedirectory
if ($server -eq $null) { $server = (get-addomaincontroller -Discover -Writable).hostname}
}
process {
#allow for both a single group name to be passed as $identity, or take pipeline get-adgroup output
if ($pscmdlet.parametersetname -eq 'normal') {
try {
[array]$group = get-adgroup $identity -property managedby -ea stop -server $server
} catch {
throw "Unable to find AD group"
}
}
#cast the group as an array for the foreach loop
if (!($group -is [array])) { $group = [array]$group}
#hardcode guid for the write members permission
$guid =[guid]'bf9679c0-0de6-11d0-a285-00aa003049e2'
#get the group owner's sid - this could be replaced with get-aduser
$user = New-Object System.Security.Principal.NTAccount("$domain\$owner")
$sid =$user.translate([System.Security.Principal.SecurityIdentifier])
$ctrl =[System.Security.AccessControl.AccessControlType]::Allow
$rights =[System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
$intype =[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
try {
try {
$aduserobj = get-aduser $owner -server $server
} catch {
#if no user was found try looking for a group owner
$aduserobj = get-aduser $owner -server $server
}
} catch {
throw "Unable to find owner's account"
}
foreach ($groupobj in $group) {
$acl = Get-Acl ad:"$($groupobj.distinguishedname)"
#if the group was passed without the managedby property, look it up again
if (($groupobj |gm |where {$_.name -eq "managedby"}) -eq $null) {
#if there was a previous manager listed that doesn't match the one being passed to this script
# remove any existing rights for that user
$groupobj = get-adgroup $groupobj.distinguishedname -property managedby -server $server
}
if ($groupobj.managedby -ne $null) {
try {
$previousownersid = (get-aduser $groupobj.managedby -property sid -server $server).sid } catch { $previousownersid = (get-adgroup $groupobj.managedby -property sid -server $server).sid
if ($previousownersid.value -ne $sid.value) {
#$prevownerID = new-object system.security.principal.securityidentifier($previousownersid)
$previousownerID = $previousownersid.Translate([System.security.principal.ntaccount]).Value
$acl.access | where {$_.identityreference -eq $previousownerID} | % {
$acl.RemoveAccessRule($_)
}
}
}
#set the ManagedBy property to the new owner
set-adgroup $groupobj -managedby $aduserobj -server $server
#add new owner's right to manage members
$rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($sid,$rights,$ctrl,$guid)
$acl.AddAccessRule($rule)
Set-Acl -acl $acl -path ad:"$($groupobj.distinguishedname)"
}
}
Thursday, April 5, 2018
FIM / MIM checking PCNS events for a specific user
The following script can be used along with some previous functions that I have written, AD object meta data check, and time functions. This will look at the user's last password set time and the domain controller that the change was recorded on. It will take the change time from the AD metadata for that last password reset, and use it to remotely search the domain controller's application log for the PCNS (password change notification service) events that match the user's SamAccountName. Of course it will need to be run with an account that has remote WMI permissions to the domain controller, which will typically be domain admin unless you made some wmi permissions modifications to the cimv2 portion of the wmi namespace.
param (
$samaccountname
)
#put some . link here for the time functions and meta data check if its not already in your profile
function get-PCNSEvents-inrange([string]$server,$time,$seconds,$username) {
$myTimeRange = wmitime-timerange $time $seconds
$filter = "logfile='application' and timegenerated>='" + $mytimerange[0] + "' and timegenerated <= '" + $mytimerange[1] + "' and sourcename='PCNSSVC'"
$results = gwmi -computer $server win32_ntlogevent -filter $filter |
where {$_.message -match $username} |select -last 1 -exp message
return $results
}
try {
$pwdChangeEvent = show-adobjmeta -type user -name $samaccountname | where {$_.attribute -eq "unicodePwd"}
if ($pwdChangeEvent -eq $null) {Throw "Cannot find user in active directory"}
$eventtime = dt-toWMITime $pwdChangeEvent.ChangeTime
$server = $pwdChangeEvent.originator.split(",")[0].replace("CN=","")
$event = get-PCNSEvents-inrange $server $eventtime 10 $samaccountname
if ($event -eq $null) {throw "No events found on domain controller around the time of the last password change."}
$event
} catch { $_}
Thursday, September 21, 2017
Reverse CNAME lookup with dns cmdlets
In case you ever get the request to find any alias that points to a server (or list of servers), you can use the DNS commandlets to build a list of results on a zone by zone basis to further dig through. This command will give you a rough list with 3 attributes:
Hostname = name of the dns record
ShortAlias = non-fqdn of the DNS record data (where the CNAME points to)
Alias = full DNS record data
I put the short name in there just in case the information provided to you is a short server name.
$zone = "contoso.com"
$recs = get-DnsServerResourceRecord -zonename $zone -rrtype cname |
select @{name="shortalias"; expr={
$_.recorddata.hostnamealias -replace "\..*",""}}, @{name="alias";
expr={$_.recorddata.hostnamealias}},hostname
This will give you the full list of cname data for the zone in an array of objects. If what you are searching for is an array, just run it through a loop in one of two ways [example of matching short names against an array of names to search for]
foreach ($name in $list) { $recs | where {$_.shortalias -match $name} }
or
foreach ($entry in $recs) { if ($list -contains $entry.shortalias) { $entry } }
Its not super clean, but it will display the records. You can modify the loops to collect the data in an array. You could even run an extra outer loop to hit multiple zones. The $list can just be a copy and paste into powershell from excel or whatever the list comes in.
$list = "
".split("`n")
Make sure when you paste, you don't end up with the " on a new line at the end like it shows above. If you do that, the first loop example will dump out the whole $recs array on the last entry in $list.
If you don't have access to the Dns cmdlets, but you have rights to pull the zone with dnscmd, you can do something like this:
dnscmd /zoneprint | where {$_ -match "CNAME"} |
% {$resline = $_ -split "\s+"; ($resline[0], $resline[3]) }
You'll have to do something with the two values at the end, which are record name and record data.
Hostname = name of the dns record
ShortAlias = non-fqdn of the DNS record data (where the CNAME points to)
Alias = full DNS record data
I put the short name in there just in case the information provided to you is a short server name.
$zone = "contoso.com"
$recs = get-DnsServerResourceRecord -zonename $zone -rrtype cname |
select @{name="shortalias"; expr={
$_.recorddata.hostnamealias -replace "\..*",""}}, @{name="alias";
expr={$_.recorddata.hostnamealias}},hostname
This will give you the full list of cname data for the zone in an array of objects. If what you are searching for is an array, just run it through a loop in one of two ways [example of matching short names against an array of names to search for]
foreach ($name in $list) { $recs | where {$_.shortalias -match $name} }
or
foreach ($entry in $recs) { if ($list -contains $entry.shortalias) { $entry } }
Its not super clean, but it will display the records. You can modify the loops to collect the data in an array. You could even run an extra outer loop to hit multiple zones. The $list can just be a copy and paste into powershell from excel or whatever the list comes in.
$list = "
".split("`n")
Make sure when you paste, you don't end up with the " on a new line at the end like it shows above. If you do that, the first loop example will dump out the whole $recs array on the last entry in $list.
If you don't have access to the Dns cmdlets, but you have rights to pull the zone with dnscmd, you can do something like this:
dnscmd
% {$resline = $_ -split "\s+"; ($resline[0], $resline[3]) }
You'll have to do something with the two values at the end, which are record name and record data.
Monday, July 31, 2017
Testing connectivity to your domain controller
In the distant past there was a useful client side tool for checking connectivity between clients and domain controllers (netdiag.exe). According to microsoft's command line reference guide, it is available in windows 8 and 2012, but in reality the command does not exist on any windows machine I have checked beyond 2003. Trying to run an older version won't work either due to some incompatibility. So, alternatives are required to do checks. One thing you would typically want to check between a client and a domain controller is port connectivity. Below, I will show a simple script that tests most of the ports. Some may not be open in your environment (like 636,3269 for ldaps). Some ports are dynamic, so I haven't included trying to check these.
To begin with, you should know what domain controller your workstation has logged into. This machine logon establishes the "secure channel" between your machine and the domain. You can use an old tool that is still around called nltest.
C:\Windows>nltest /sc_query:contoso.com
Flags: 30 HAS_IP HAS_TIMESERV
Trusted DC Name \\DC1.contoso.com
Trusted DC Connection Status Status = 0 0x0 NERR_Success
The command completed successfully
This output shows the status of your secure channel, and the name of the domain controller you are querying. You will need to provide the name of the domain you are connected to. FQDN domain name or NETBIOS domain name should work fine.
This script will provide two functions, one port checker and one function to run to test your connection. Run Test-DomainControllerPorts with your domain name (or leave it blank for auto detect). The script returns the name of the DC that you are connected to, along with 2 arrays of ports that are open and another of ports that aren't responding.
function tcpt ([string]$serv, [string]$p) {
$result = $false
try {
$conn = new-object system.net.sockets.tcpclient($serv,$p)
if ($conn.connected) { $result = $true } else { result = $false }
$conn.close()
} catch {
$result = $false
}
$conn = $null
return $result
}
function test-DomainControllerPorts {
param (
$domainname = (gwmi win32_computersystem).domain
)
$secureChannelDC = (nltest /sc_query:$domainname |
where {$_ -match "Trusted DC Name"}).split("\\") |
where {$_ -match $domainname}
$secureChanneldc = $securechanneldc.trim()
$functionalports = @()
$nonFunctionalPorts = @()
$portsToCheck = ("53", "88", "135", "137", "139", "389", "445", "464", "3268", "636", "3269")
foreach ($port in $portsToCheck) {
$portstat = tcpt $secureChannelDC $port
if ($portstat) {
$functionalports += $port
} else {
$nonfunctionalPorts += $port
}
}
$result = new-object PSObject
add-member -inp $result NoteProperty DomainController $secureChannelDC
add-member -inp $result NoteProperty OpenPorts $functionalports
add-member -inp $result NoteProperty UnOpenPorts $nonfunctionalports
out-default -inp $result
}
To begin with, you should know what domain controller your workstation has logged into. This machine logon establishes the "secure channel" between your machine and the domain. You can use an old tool that is still around called nltest.
C:\Windows>nltest /sc_query:contoso.com
Flags: 30 HAS_IP HAS_TIMESERV
Trusted DC Name \\DC1.contoso.com
Trusted DC Connection Status Status = 0 0x0 NERR_Success
The command completed successfully
This output shows the status of your secure channel, and the name of the domain controller you are querying. You will need to provide the name of the domain you are connected to. FQDN domain name or NETBIOS domain name should work fine.
This script will provide two functions, one port checker and one function to run to test your connection. Run Test-DomainControllerPorts with your domain name (or leave it blank for auto detect). The script returns the name of the DC that you are connected to, along with 2 arrays of ports that are open and another of ports that aren't responding.
function tcpt ([string]$serv, [string]$p) {
$result = $false
try {
$conn = new-object system.net.sockets.tcpclient($serv,$p)
if ($conn.connected) { $result = $true } else { result = $false }
$conn.close()
} catch {
$result = $false
}
$conn = $null
return $result
}
function test-DomainControllerPorts {
param (
$domainname = (gwmi win32_computersystem).domain
)
$secureChannelDC = (nltest /sc_query:$domainname |
where {$_ -match "Trusted DC Name"}).split("\\") |
where {$_ -match $domainname}
$secureChanneldc = $securechanneldc.trim()
$functionalports = @()
$nonFunctionalPorts = @()
$portsToCheck = ("53", "88", "135", "137", "139", "389", "445", "464", "3268", "636", "3269")
foreach ($port in $portsToCheck) {
$portstat = tcpt $secureChannelDC $port
if ($portstat) {
$functionalports += $port
} else {
$nonfunctionalPorts += $port
}
}
$result = new-object PSObject
add-member -inp $result NoteProperty DomainController $secureChannelDC
add-member -inp $result NoteProperty OpenPorts $functionalports
add-member -inp $result NoteProperty UnOpenPorts $nonfunctionalports
out-default -inp $result
}
Update for later OS's (high than win 2008), some of the ports above are legacy and wouldn't be open on many domain controllers (such as 137, 139)
Sunday, July 2, 2017
AD: Simple way to remove all members of a group
No loops required, use the -clear parameter in set-adgroup.
Set-adgroup -identity "name of group" -clear member
The time required to execute will vary depending on number of people in the group.
Set-adgroup -identity "name of group" -clear member
The time required to execute will vary depending on number of people in the group.
Wednesday, April 19, 2017
Download all enterprise CA crl's from active directory
This script will look for all published crl's in the configuration partition, download them, and write them to binary files. To further examine the files, you can open them up in windows (standard certificate viewing tools), or use the PSPKI module to dig into the data.
$debase = new-object directoryservices.directoryentry("LDAP://RootDSE")
$configpartition = $debase.configurationNamingContext[0]
$de = new-object directoryservices.directoryentry(`
"LDAP://CN=CDP,CN=Public Key Services,CN=Services," + $configpartition)
$ds = new-object directoryservices.directorysearcher($de)
$ds.filter = "(objectclass=cRLDistributionPoint)"
$ds.propertiestoload.add("certificaterevocationlist")|out-null
$crls = $ds.findall()
foreach ($crl in $crls) {
$CAcert = $crl.path.replace("LDAP://CN=","")
$CAcert = $CAcert.substring(0,$CAcert.indexof(","))
$file = $CACert + ".crl"
set-content $file ([byte[]]($crl.properties.certificaterevocationlist[0])) `
-encoding Byte
}
Download all files from IIS web directory listing (non-recursive)
This simple code should be able to dig out all file names from inside the A HREF tags where the file name consists of letters, numbers, a few special characters, spaces, file path forward slashes and periods; and ends with an extension of 2-4 characters. Each entry will be downloaded, however, take note that the A HREF data will contain a relative path to the item, including the directory structure. The webclient downloadfile method's second parameter wants a path name, including file name, for the destination. If the full path doesn't exist, the file may just get put in the current directory.
$wc = new-object net.webclient
$sitename = "http://somesite/somedirectory"
$weblisting = $wc.downloadstring($sitename)
$items = select-string '"[a-zA-Z0-9/._-() ]*\.[a-zA-Z0-9]{2,4}"' `
-input $weblisting -allmatches|
foreach {$_.matches.value.replace('"','')}
foreach ($item in $items) {
$wc.downloadfile($sitename + $item, ".\" + $item)
}
Tuesday, January 17, 2017
powershell: filtering for unique lines of csv
Scenario: You have a large csv file (several hundred meg or more) representing username to computer name mappings. The data contains a lot of duplicates as it represents activity over a period of time. The data is already sorted by time, so how do you get the most recent activity per computer while ignoring the rest?
Pipeline method with commandlets:
import-csv .\data.csv| select -Unique computername|ConvertTo-Csv -NoTypeInformation
|out-file .\filtered-data.csv
This ran for hours, hit several hundred MB of ram usage and eventually had to be cancelled as it was taking too long. Unfortunately for the unique filtering on select, it had to do csv conversions to get the attribute that I wanted to filter on.
Hackish method with hash table:
$ht = new-object hashtable
function selective-add {
[CmdletBinding()]
param ( [Parameter(Mandatory=$True,ValueFromPipeline=$True)]$line )
begin {}
process {
$data = $line.split(',')
if ($ht.contains($data[1])) {} else {
$ht.add($data[1],$data[0])
}
}
}
get-content .\data.csv | selective-add
$ht.keys | % { add-content -path filtered-data.csv -value $("{0},{1}" -f $_, $ht.Item($_)) }
This only took about 15 minutes, however it sucked up twice as much ram as the previous method in a very short period of time.
Pipeline method with commandlets:
import-csv .\data.csv| select -Unique computername|ConvertTo-Csv -NoTypeInformation
|out-file .\filtered-data.csv
This ran for hours, hit several hundred MB of ram usage and eventually had to be cancelled as it was taking too long. Unfortunately for the unique filtering on select, it had to do csv conversions to get the attribute that I wanted to filter on.
Hackish method with hash table:
$ht = new-object hashtable
function selective-add {
[CmdletBinding()]
param ( [Parameter(Mandatory=$True,ValueFromPipeline=$True)]$line )
begin {}
process {
$data = $line.split(',')
if ($ht.contains($data[1])) {} else {
$ht.add($data[1],$data[0])
}
}
}
get-content .\data.csv | selective-add
$ht.keys | % { add-content -path filtered-data.csv -value $("{0},{1}" -f $_, $ht.Item($_)) }
This only took about 15 minutes, however it sucked up twice as much ram as the previous method in a very short period of time.
Tuesday, December 27, 2016
Fixing dns record permissions for dynamic dns
In case you have dns records that need to be changed from static to dynamic, or the machine/clusters that will be updating them have changed, you can modify the dns record permissions to allow updates. Doing this through the gui is fine for a few records, but if you need something a bit more simple and automated, you can try this script. It takes both the computer name (doesn't have to match the dns record, just needs to be a computer object), and the dns record (fqdn) that you are updating. This script will work for AD dns only, and would be limited to the current domain, and likely any forest wide partitions. If you check my article on managing other domains in powershell, you can probably get edits to other domains working on this with a few modifications and extra parameters.
param (
$computername,
$dnsrecord
)
$script:computernameSam = $computername + "$"
try {
import-module activedirectory
} catch {
write-error "This script requires the AD powershell module"
exit
}
while ( (test-path -path Ad:) -ne $true )
{
start-sleep -seconds 2
}
#Standard ACL for a dynamic dns entry
#ActiveDirectoryRights : CreateChild, DeleteChild, ListChildren, ReadProperty, DeleteTree, ExtendedRight, Delete,
# GenericWrite, WriteDacl, WriteOwner
#InheritanceType : None
#ObjectType : 00000000-0000-0000-0000-000000000000
#InheritedObjectType : 00000000-0000-0000-0000-000000000000
#ObjectFlags : None
#AccessControlType : Allow
#IdentityReference : Domain\machine$
#IsInherited : False
#InheritanceFlags : None
#PropagationFlags : None
#
function get-partition {
param ( $record )
#need to split off everything after first name to find longest zone match
#using get-dnsserverzone ($name)
$dnsrecordparts = $record.split(".")
for ($i = 1; $i -lt $dnsrecordparts.length; $i++) {
$zonenameTest = $dnsrecordparts[$i..($dnsrecordparts.length -1)] -join "."
$zoneObj = get-dnsserverzone $zonenameTest -ea 0
if ($zoneObj -ne $null) {
write-output -inputobject $zoneobj
$i = $dnsrecordparts.length + 1
}
}
}
function get-dnsobject {
param ($record)
$zoneObject = get-partition -record $record
if ($zoneObject -ne $null) {
$zonename = $zoneObject.zonename
$record -match "(.*)(\.$zonename)"
$dnsRecordDN = "dc=" + $matches[1] + "," + $zoneobject.distinguishedname
try {
get-adobject $dnsRecordDN
} catch { Throw "Unable to find dns record for this machine"}
} else { throw "DNS zone not found"}
}
try {
try {
$guid = [guid]'00000000-0000-0000-0000-000000000000'
$adcomputer = get-adcomputer $computername -property objectsid
$sid = $adcomputer.objectsid
$ctrl = [System.Security.AccessControl.AccessControlType]::Allow
$rights = 983423
$intype =[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
$rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($sid,$rights,$ctrl,$guid)
} catch { throw "Unable to get computer account SID" }
try {
#find record
$dnsDN = get-dnsObject -record $dnsrecord
} catch { throw $_ }
try {
$acl = get-acl ad:"$($dnsDN.distinguishedname)"
$acl.setowner([system.security.principal.ntaccount]"$script:computernameSam")
$acl.AddAccessRule($rule)
Set-Acl -acl $acl -path ad:"$($dnsDN.distinguishedname)"
} catch { throw $_ }
} catch { $_}
Tuesday, June 21, 2016
Tips for AD group membership managment in powershell
Managing large groups can fail due to limits in Active Directory Web Services when too many members are in a group.
Fails: Get-adgroupmember "LargeGroup"
error: Get-ADGroupMember : The size limit for this request was exceeded
Works: Add-adgroupmember and remove-adgroupmember
Work Around: get-adgroup "LargeGroup" -properties members | select -expand members
This will get the distinguishednames of all members as an array.
-----------------------------------------------
Piping groups or users into a group membership cmdlet to change the group memberships.
1) When you are piping groups into a cmdlet where the user(s) are static. Pipe to Add-ADGroupMember.
Ex: get-adgroup -filter {name -like "HelpDesk*"}| add-adgroupmember -members $userdn
2) When you are piping users into a cmdlet where the group(s) are static. Pipe to Add-ADPrincipalGroupMembership
Ex: get-aduser bob | Add-ADPrincipalGroupMembership -memberof $groupdn
NOTE: Add-ADPrincipalGroupMembership will generate successful security audit events (Directory Service Change) for the addition of the group member, even if they were already a member of the group
-----------------------------------------------
When using Add-ADGroupMember with an array of members, if any of them are part of the group already, the whole operation will fail. Its best to try adding one at a time.
Fails: Get-adgroupmember "LargeGroup"
error: Get-ADGroupMember : The size limit for this request was exceeded
Works: Add-adgroupmember and remove-adgroupmember
Work Around: get-adgroup "LargeGroup" -properties members | select -expand members
This will get the distinguishednames of all members as an array.
-----------------------------------------------
Piping groups or users into a group membership cmdlet to change the group memberships.
1) When you are piping groups into a cmdlet where the user(s) are static. Pipe to Add-ADGroupMember.
Ex: get-adgroup -filter {name -like "HelpDesk*"}| add-adgroupmember -members $userdn
2) When you are piping users into a cmdlet where the group(s) are static. Pipe to Add-ADPrincipalGroupMembership
Ex: get-aduser bob | Add-ADPrincipalGroupMembership -memberof $groupdn
NOTE: Add-ADPrincipalGroupMembership will generate successful security audit events (Directory Service Change) for the addition of the group member, even if they were already a member of the group
-----------------------------------------------
When using Add-ADGroupMember with an array of members, if any of them are part of the group already, the whole operation will fail. Its best to try adding one at a time.
Thursday, June 16, 2016
Piping get-aduser output through several custom powershell functions
For people who write scripts to process large amounts of AD objects, you may find that use of pipelines will be more memory efficient that variables and foreach. In one case, I was working through a problem with a script in powershell v2, where lots of strange failures were occurring, such as failing to assign values to a variable. A statement like: $a = 1, may just randomly not work. As the script was doing some heavy processing of large numbers of objects, I assumed memory consumption was the problem. I wanted to convert the script to use of pipeline. Since the script had several functions that handled different aspects of what was needed, I thought it would be good to try multiple pipelines. In the end, the change to pipelines fixed all the failures in the script.
What I wanted:
Take an OU, run get-aduser on the OU -> Pipe to an analysis function to check password expiration for different types of accounts and password policies, then decide if an email notice needed to be sent -> Pipe (if needed) to an email function -> Pipe the results of all of the above to logging function.
At each stage, different bits of calculated data or additional properties needed to be added to the original get-aduser object. This was possible by using custom PSObjects after the initial analysis function. The basics of the code is below:
function process-OU {
param(
[parameter(mandatory=$true)][string]$searchbase,
[string]$type="standard"
)
Get-ADUser -Filter {(enabled -eq $True) -and (mail -like "*") } `
-SearchBase $SearchBase `
-Properties mail, PasswordLastSet, sn, PasswordNeverExpires |
analyze-user -type $type |email-user |log-result
}
function Analyze-User{
[CmdletBinding()]
param (
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[Microsoft.ActiveDirectory.Management.ADAccount]$user,
[string]$Type
)
begin {}
process {
#do some analysis and decide if you want to
#continue with write-output $user
#
#Add any additional pieces of information to the user object with
# add-member -input $user -force NoteProperty Expired $False
if ($proceedtoEmail) { write-output $user }
}
}
function Email-User {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[PSobject]$emailuser
)
#Notice the parameter type is a generic
#[psobject] as it is no long conforming
#to the [Microsoft.ActiveDirectory.Management.ADAccount] type
Begin{}
Process {
#handle email creation and sending.
#Check if it was sent without error,
#add email status as another property
}
}
function log-result {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline=$True)]
[PSObject]$user
)
begin {}
process {
#do some logging here
}
}
process-OU -searchbase "ou=myusers,dc=contoso,dc=com" -type "regular"
Tuesday, April 12, 2016
Active Directory ACL's explained
In a previous post on decoding AD ACL's, I provided some code which took BSonPosh's get-adacl output and decoded the SID's and GUID's to help provide more readable output. The example of this is below, however you may find some of the other fields to be a bit confusing. So I created a few different types of test permissions on an OU to show how they are reflected in the Powershell output of these two commands.
Permission set in GUI: "Apply to: All Descendant objects, create/delete Conference Site objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : Descendents
ObjectType : msExchConferenceContainer
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : InheritOnly
Permission set in GUI: "Apply to: This object and all descendant objects, create/delete Contact objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : All
ObjectType : contact
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : None
Permission set in GUI: "Apply to: This object only, create/delete Computer Objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : None
ObjectType : computer
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : None
PropagationFlags : None
Permission set in GUI: "Apply to: Descendent Computer objects, Modify Owner"
ActiveDirectoryRights : WriteOwner
InheritanceType : Descendents
ObjectType : 00000000-0000-0000-0000-000000000000
InheritedObjectType : computer
ObjectFlags : InheritedObjectAceTypePresent
AccessControlType : Allow
IdentityReference : BHI-MASTER\adminlinlnat
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : InheritOnly
InheritedObjectType: Notice this will be all zero's when the permission is for creating a child object in a container. When it is permissions being set on a specific type of child objects, then it will be set that that object type, and the ObjectType value will be all zero's. When setting a permission on a specific property of a specific type of child object, you will get both fields filled in with the ObjectType being the specified property, and InheritedObjectType being the AD object's type.
PropagationFlags: InheritOnly exists when applying to something other than the current OU. (https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags(v=vs.110).aspx)
InheritenceFlags: ContainerInherit when applying to anything below the current level, ObjectInherit when applying to child objects (https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags(v=vs.110).aspx)
InheritanceType: All (everything from this level down), Descendents (children and descendants, not the current object), None (current level only) (https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectorysecurityinheritance(v=vs.110).aspx)
Permission set in GUI: "Apply to: All Descendant objects, create/delete Conference Site objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : Descendents
ObjectType : msExchConferenceContainer
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : InheritOnly
Permission set in GUI: "Apply to: This object and all descendant objects, create/delete Contact objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : All
ObjectType : contact
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : None
Permission set in GUI: "Apply to: This object only, create/delete Computer Objects"
ActiveDirectoryRights : CreateChild, DeleteChild
InheritanceType : None
ObjectType : computer
InheritedObjectType : 00000000-0000-0000-0000-000000000000
ObjectFlags : ObjectAceTypePresent
AccessControlType : Allow
IdentityReference : TEST.LOCAL\Nathan
IsInherited : False
InheritanceFlags : None
PropagationFlags : None
Permission set in GUI: "Apply to: Descendent Computer objects, Modify Owner"
ActiveDirectoryRights : WriteOwner
InheritanceType : Descendents
ObjectType : 00000000-0000-0000-0000-000000000000
InheritedObjectType : computer
ObjectFlags : InheritedObjectAceTypePresent
AccessControlType : Allow
IdentityReference : BHI-MASTER\adminlinlnat
IsInherited : False
InheritanceFlags : ContainerInherit
PropagationFlags : InheritOnly
InheritedObjectType: Notice this will be all zero's when the permission is for creating a child object in a container. When it is permissions being set on a specific type of child objects, then it will be set that that object type, and the ObjectType value will be all zero's. When setting a permission on a specific property of a specific type of child object, you will get both fields filled in with the ObjectType being the specified property, and InheritedObjectType being the AD object's type.
PropagationFlags: InheritOnly exists when applying to something other than the current OU. (https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags(v=vs.110).aspx)
InheritenceFlags: ContainerInherit when applying to anything below the current level, ObjectInherit when applying to child objects (https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags(v=vs.110).aspx)
InheritanceType: All (everything from this level down), Descendents (children and descendants, not the current object), None (current level only) (https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectorysecurityinheritance(v=vs.110).aspx)
Wednesday, December 16, 2015
Tis the season for vacation clearing (and password expiration)
As we approach the end of the year, along with its holidays, its common for many employees to take leave for long periods of time. So as a gift to helpdesks everywhere, often they would request to know who will have their password expire during the peak holiday times (to prepare for the support calls). To build a list like this is quite easy with powershell. This assumes you don't have fine grain password policies. In this example, we look at expiring passwords between Dec 21 and Jan 4 given the working days and anticipated return dates around the Christmas and New Years holidays:
import-module activedirectory
#grab the domain wide password policy and extract a # of days integer
$passwordage = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge |select -exp days
#define your start and end filter dates and subtract the max Password age value.
#We need to calculate using passwordlastset timestamps
$startdate = ([datetime]"12-21-2015").adddays(-$passwordage)
$enddate = ([datetime]"1-4-2016").adddays(-$passwordage)
#Filter as much as possible on the LDAP side with the date ranges.
#The Select statement includes a calculated
#expression to convert the passwordlastset value to an actual expiration date.
#Convert to CSV and output to file.
#Zip it and mail it out.
get-aduser -filter {(enabled -eq $true) -and (passwordlastset -ge $startdate) -and (passwordlastset -le $enddate)} -Properties passwordlastset, mail | select samaccountname, name, mail, @{name="ExpirationDate"; exp={$_.passwordlastset.adddays($passwordage)}} | convertto-csv -notypeinfo | out-file .\expiringholidays.csv
import-module activedirectory
#grab the domain wide password policy and extract a # of days integer
$passwordage = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge |select -exp days
#define your start and end filter dates and subtract the max Password age value.
#We need to calculate using passwordlastset timestamps
$startdate = ([datetime]"12-21-2015").adddays(-$passwordage)
$enddate = ([datetime]"1-4-2016").adddays(-$passwordage)
#Filter as much as possible on the LDAP side with the date ranges.
#The Select statement includes a calculated
#expression to convert the passwordlastset value to an actual expiration date.
#Convert to CSV and output to file.
#Zip it and mail it out.
get-aduser -filter {(enabled -eq $true) -and (passwordlastset -ge $startdate) -and (passwordlastset -le $enddate)} -Properties passwordlastset, mail | select samaccountname, name, mail, @{name="ExpirationDate"; exp={$_.passwordlastset.adddays($passwordage)}} | convertto-csv -notypeinfo | out-file .\expiringholidays.csv
Tuesday, December 15, 2015
Checking Mcafee DAT version information remotely with powershell
This script can be used to remotely check a Mcafee client's datversion number and the date of the DAT file's release. It uses basic remote registry reading techniques, so remote registry access (usually administrator on the remote machine) will be required for it to work. You can use this script as a template for other registry reading operations that you may need. It is a basic script that only accepts single computer input. You can change parameters and turn this into a function to handle pipeline or array input.
Update July 2018. Added a secondary registry key in due to some differences I'm seeing in my environment lately. With different product versions and EPO use, this location may move around a bit. If you find this code doesn't work for you, check the current dat version in the taskbar icon, then search the registry for the dat version number. This should get you to the location that your product is storing this information in.
Update July 2018. Added a secondary registry key in due to some differences I'm seeing in my environment lately. With different product versions and EPO use, this location may move around a bit. If you find this code doesn't work for you, check the current dat version in the taskbar icon, then search the registry for the dat version number. This should get you to the location that your product is storing this information in.
param (
[parameter(mandatory=$true)][string]$computername
)
function ping-host([string]$computername) {
#This function will perform a simple, small size single packet ping of a machine and return true/false for the result
if ([string]::IsNullOrEmpty($computername) ) {return $false}
#ping first for reachability check
$po = New-Object net.NetworkInformation.PingOptions
$po.set_ttl(64)
$po.set_dontfragment($true)
[Byte[]] $pingbytes = (65,72,79,89)
$ping = new-object Net.NetworkInformation.Ping
$savedEA = $Erroractionpreference
$ErrorActionPreference = "silentlycontinue"
$pingres = $ping.send($computername, 1000, $pingbytes, $po)
if (-not $?) {return $false}
$ErrorActionPreference = $savedEA
if ($pingres.status -eq "Success") { return $true } else {return $false}
}
if ((ping-host $computername) -eq $false) {
New-Object PSobject -Property @{
Computername = $computername
DATVersion = "System Not Online"
Datdate = $null
}
} else {
try {
#Set up the key that needs to be accessed and what registry tree it is under
$key = "Software\McAfee\AVEngine"
$type = [Microsoft.Win32.RegistryHive]::LocalMachine
#open up the registry on the remote machine and read out the TOE related registry values
$regkey = [Microsoft.win32.registrykey]::OpenRemoteBaseKey($type,$computername)
$regkey = $regkey.opensubkey($key)
$status = $regkey.getvalue("AVDatVersion")
$datdate = $regkey.getvalue("AVDatDate")
} catch {
try {
$key = "Software\Wow6432Node\McAfee\AVEngine"
$type = [Microsoft.Win32.RegistryHive]::LocalMachine
#open up the registry on the remote machine and read out the TOE related registry values
$regkey = [Microsoft.win32.registrykey]::OpenRemoteBaseKey($type,$computername)
$regkey = $regkey.opensubkey($key)
$status = $regkey.getvalue("AVDatVersion")
$datdate = $regkey.getvalue("AVDatDate")
} catch {
#try newer registry location
try {
$key = "Software\Wow5432Node\Network Associates\ePolicy Orchestrator\Application Plugins\VIRUSCAN880"
$regkey = [Microsoft.win32.registrykey]::OpenRemoteBaseKey($type,$computername)
$regkey = $regkey.opensubkey($key)
$status = $regkey.getvalue("DATVersion")
$datdate = $regkey.getvalue("DatDate")
} catch {
$status = "Cannot read regkey"
}
}
}
New-Object PSobject -Property @{
Computername = $computername
DATVersion = $status
DatDate = $datdate
} |select Computername,DatVersion,DatDate
}
Thursday, December 3, 2015
Powershell Switch for a numeric range
There are several ways of handling a range of values in a Switch statement in powershell. In this example, I will show a simple conversion of a numeric month value to a quarter [string].
First example:
The range operator returns an array of the numbers in that specified range, so we use -contains to test our value. If we left out -contains, all switch values would match as no comparison is being made, yet the range is being evaluated as true.
Second example:
The key to this example is the Break statement. Without this, everything would match as the switch operator continues to evaluate for all possible matches (not just the first one). Break will cause it to stop evaluating.
Third example:
The more "wordy" example with multiple conditions and the -And. This may be more readable, however the first example does a pretty good job with this. The functionality is far more obvious than example #2. In any case, using conditions to check a range of values will save time and code. Alternatively (though not very elegant or practical), you can use a regex switch for this:
First example:
switch($month){
{1..3 -contains $_}{"Q1"}
{4..6 -contains $_}{"Q2"}
{7..9 -contains $_}{"Q3"}
{10..12 -contains $_}{"Q4"}
}
The range operator returns an array of the numbers in that specified range, so we use -contains to test our value. If we left out -contains, all switch values would match as no comparison is being made, yet the range is being evaluated as true.
Second example:
switch($month) {
{$_ -le 3} {"Q1";break;}
{$_ -le 6} {"Q2";break;}
{$_ -le 9} {"Q3";break;}
{$_ -le 12} {"Q4"; break;}
}
The key to this example is the Break statement. Without this, everything would match as the switch operator continues to evaluate for all possible matches (not just the first one). Break will cause it to stop evaluating.
Third example:
switch($month) {
{$_ -ge 1 -and $_ -le 3} {"Q1"}
{$_ -ge 4 -and $_ -le 6} {"Q2"}
{$_ -ge 7 -and $_ -le 9} {"Q3"}
{$_ -ge 10 -and $_ -le 12} {"Q4"}
}
The more "wordy" example with multiple conditions and the -And. This may be more readable, however the first example does a pretty good job with this. The functionality is far more obvious than example #2. In any case, using conditions to check a range of values will save time and code. Alternatively (though not very elegant or practical), you can use a regex switch for this:
switch -regex ([string]$month) {
"^[1-3]$" {"Q1"}
"^[4-6]$" {"Q2"}
"^[7-9]$" {"Q3"}
"^1[0-2]$" {"Q4"}
}
Monday, August 3, 2015
Encouraging domain controller to advertise time
param(
[parameter(mandatory=$true)]$computer
)
$sb = {
set-itemproperty -path `
HKLM:\system\currentcontrolset\services\w32time\timeproviders\ntpserver `
-name "Enabled" -value "1" restart-service w32time w32tm /resync } invoke-command -script $sb -computer $computer
2023 - Update to this article. It is possible for a server to have a value of 1 on the Enabled dword
for ntserver and the server still reports that it is not advertising. There is a group policy setting
that can prevent the machine from acting as an NTP server which will block the service from working and won't
present anything obvious in the registry.
Checking NTP time offsets with powershell - parsing dos utility output
Recently I ran into a problem with domain controllers being out of sync with each other. Most of the DC's were sync'd with each other, but they had falling out of sync with the root PDC. Since SCOM's time monitoring only tests between the local system clock and the PDC of that domain, it will not detect all time problems in a multi-domain environment. In the past, this may have been fine as the windows time service had servers sync'ing to the PDC. Now there is some difference in how the time service finds a sync partner. It can be configured to use the PDC, or the new default which is based on sites (CrossSiteSyncFlags registry setting, time service reference). So, due to this problem I went through an old solution I had come up with using w32tm /monitor to look at all domain controllers. The problem with this is the output is really messy, and apparently may not be the same from one OS version to another. I had it working on 2003, and 2012R2, but when running on 2008R2 the old parsing rules I had fell apart. So using the same w32tm utility, I started over with the stripchart command to do a test of a single machine with the function below. Using this function, you can test any ntp server that is accessible (domain controller or otherwise). This way you can wrap the function into a monitoring tool that looks at all domain controllers in all domains, as well as the higher level NTP source that the domain sync's to.
The number of samples in the stripchart command will greatly impact the run time if you are running it against a large number of computers. I put it at 3 due to false non-responsive alerts when it was set to 1. If your network is repsonsive, 1 may be sufficient.
The number of samples in the stripchart command will greatly impact the run time if you are running it against a large number of computers. I put it at 3 due to false non-responsive alerts when it was set to 1. If your network is repsonsive, 1 may be sufficient.
function get-timeoffset {
param([parameter(mandatory=$true)]$computer)
write-verbose "working on server $computer"
$resultval = new-object PSobject
add-member -input $resultval NoteProperty Computer $computer
$a = w32tm /stripchart /computer:$computer /dataonly /samples:3 /ipprotocol:4
if (-not ($a -is [array])) {
add-member -input $resultval NoteProperty Status "Offline"
add-member -input $resultval NoteProperty Offset $null
} else {
$foundtime=$false
#go through the 5 samples to find a response with timeoffset.
for ($i = 3; $i -lt 8; $i++) {
if (-not $foundtime) {
if ($a[$i] -match ", ([-+]\d+\.\d+)s") {
$offset = [float]$matches[1]
add-member -input $resultval NoteProperty Status "Online"
add-member -input $resultval NoteProperty Offset $offset
$foundtime=$true
}
}
}
#if no time samples were found, check for error
if (-not $foundtime) {
if ($a[3] -match "error") {
#0x800705B4 is not advertising/responding
add-member -input $resultval NoteProperty Status "NTP not responding"
} else {
add-member -input $resultval NoteProperty Status $a[3]
}
add-member -input $resultval NoteProperty Offset $null
}
}
$resultval
}
Subscribe to:
Posts (Atom)