The following PowerShell script will create a list of supernets that are outside of the United States. The networks created by this script are intended to be used to restrict network traffic from foreign countries. The results of this script aren't perfect and aren't intended to be perfect. There is trade off between the size of the list and accuracy, and I chose to err on the side of a shorter list of networks so it would not add and extra burden to the firewall.
Here is the script:
$debug = 0
# Filter for records that aren't in the US or run by ARIN
$records = ([xml]((New-Object System.Net.WebClient).DownloadString("http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml"))).registry.record | ? {
$_.designation -notlike "*ARIN*" -and
$_.status -ne "LEGACY"
}
# Create array for holding supernets
$supernets = @()
# Add a property for the Binary representation of the first octet
# Add a property for holding the masked bits, used for finding the supernets
$records | % { $_ |
Add-Member NoteProperty -Name "Bits" -Value ([Convert]::ToString($_.prefix.Split("/")[0],2)).PadLeft(8,"0") -PassThru |
Add-Member NoteProperty -Name "MaskedBits" -Value ""
}
# $i is the current number of mask bits used for finding a supernet
for ($i=1; $i -le 8; $i++) {
# apply the mask, set the masked bits property
# this get the left most $i of bits
$records | % {
$_.MaskedBits = $_.Bits.SubString(0,$i)
}
if ($debug) { $numrecords = $records.count }
# how many /$i networks does it take to fill the current supernet, /1 is 128, /2 is 64 ...
$supernetsize = [Math]::pow(2,8-$i)
if ($debug) { "Supernet Size: $supernetsize" }
# if a full supernet is found, then ...
# a "full" supernet contains all of the /8 networks to fill the supernet
$records | group MaskedBits | ? { $_.Count -eq $supernetsize} | % {
$group = $_
# create the supernet object and set the properties
$supernet = "" | Select Prefix,Bits,MaskBits,CIDR
$supernet.Bits = $group.Name.PadRight(8, "0")
$supernet.Prefix = [Convert]::ToByte($supernet.Bits, 2)
$supernet.MaskBits = $i
$supernet.CIDR = "$($supernet.Prefix).0.0.0/$($supernet.MaskBits)"
# add the supernet to the collection of supernets
$supernets += $supernet
# remove the networks from the full network collection if they were just added as a supernet
$records = $records | ? { $_.Bits.SubString(0,$i) -ne $group.Name }
}
if ($debug) {
"Matching Supernets Found: $(($numrecords - $records.count)/$supernetsize)"
$supernets | ? { $_.MaskBits -eq $i }
"—————————————————————————————————————-"
}
}
#output the results
$supernets | Sort Bits | Select Prefix,Bits,MaskBits,CIDR
Now for a bit of explanation...
As you may already know, most IPv4 addresses are controlled by ARIN (North America), APNIC (Asia Pacific), LACNIC (Latin America and Caribbean), and other similar regional internet registries (RIR). Each one controls the ip addresses for a specific portion of the world. We want to create a list of all networks that are not in the United States, and the closest approximation is the ARIN RIR. The problem is, prior to the establishment of the RIRs, some blocks of IP addresses were handed out directly to organizations (and their status is "legacy"). Most of the companies that have the legacy address spaces are US based and we will assume they are allowed.
First, we get the list of networks from IANA (Internet Assigned Numbers Authority). Our black list will contain all of the networks NOT in the US, so we filter out all of the ARIN controlled IP addresses and the addresses with a "legacy" status. The list is imported in XML format. The nice feature of the xml format is that imports the xml entities as objects, and PowerShell works best with objects. The properties of the objects are prefix, designation, date, status and xref, but we only use the status and the prefix. With these properties defined we can easily use the Where-Object cmdlet (alias ?) to filter out the "safe" address. If you wanted to block a different set of RIRs this is where you could make one simple change to better suit you.
$records = ([xml]((New-Object System.Net.WebClient).DownloadString("http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml"))).registry.record | ? {
$_.designation -notlike "*ARIN*" -and
$_.status -ne "LEGACY"
}
After the import and the filter, the $records variable contains all of the addresses blocks we want to blacklist. We could just quit now, but it would be nice to shorten the list. We can combine the address blocks into supernets. We start by creating a variable to hold the array of supernets.
# Create array for holding supernets
$supernets = @()
To make things easier, we can extend the object given to us from xml. Two properties are added, one to hold the bit representation of the first octet, and the second will be used for storing a masked version of those bits.
# Add a property for the Binary representation of the first octet
# Add a property for holding the masked bits, used for finding the supernets
$records | % { $_ |
Add-Member NoteProperty -Name "Bits" -Value ([Convert]::ToString($_.prefix.Split("/")[0],2)).PadLeft(8,"0") -PassThru |
Add-Member NoteProperty -Name "MaskedBits" -Value ""
}
The $records variable (as previously stated, contains all the networks to be blocked) is piped into the ForEach-Object cmdlet (alias %). Inside the loop we create the properties and set the Bits property. The initial value for the Masked Bits is blank since we will set that later.
Now that the object is created the way we want it, we start a For loop. The loop will go from 1 to 8, representing each bit in the octet, and it will be used for masking.
# $i is the current number of mask bits used for finding a supernet
for ($i=1; $i -le 8; $i++) {
Now to use our newly created property and set the Mask Bits on each object.
# apply the mask, set the masked bits property
# this get the left most $i of bits
$records | % {
$_.MaskedBits = $_.Bits.SubString(0,$i)
}
The $records variable is piped into a ForEach-Object loop. Inside the loop we apply the mask. The mask takes the left most bits. When $i is one, we only look at the leftmost bit. When $i is 2, we take the two leftmost bits, and so on.
Now we need to calculate how many networks it takes to "fill" a supernet with our mask. A mask of 1 will require 128 networks to be full, a mask of 2 will require 64 networks, 3 requires 32, and so on.
# how many /$i networks does it take to fill the current supernet, /1 is 128, /2 is 64 ...
$supernetsize = [Math]::pow(2,8-$i)
Now we have the supernet size, so let's see if we have any full supernets.
# if a full supernet is found, then ...
# a "full" supernet contains all of the /8 networks to fill the supernet
$records | group MaskedBits | ? { $_.Count -eq $supernetsize} | % {
We pipe the $records variable into Group-Object, where the grouping is done in the Masked Bits. All of the networks with matching Mask Bits will be put in a group. We then filter all groups that are full by using the Where-Object cmdlet to filter groups that have the required number of elements. If any make it through the filter they are piped into the ForEach-Object cmdlet, where we create the supernet.
$group = $_
# create the supernet object and set the properties
$supernet = "" | Select Prefix,Bits,MaskBits,CIDR
$supernet.Bits = $group.Name.PadRight(8, "0")
$supernet.Prefix = [Convert]::ToByte($supernet.Bits, 2)
$supernet.MaskBits = $i
$supernet.CIDR = "$($supernet.Prefix).0.0.0/$($supernet.MaskBits)"
# add the supernet to the collection of supernets
$supernets += $supernet
# remove the networks from the full network collection if they were just added as a supernet
$records = $records | ? { $_.Bits.SubString(0,$i) -ne $group.Name }
}
}
First we set a the $group variable equal to the current group, $_ represents the group passed into the ForEach-Object cmdlet. Next, we create a variable to hold the supernet and set the properties of the supernet. The Bits property is the masked bits, the Prefix is the decimal equivalent of the masked bits, the Mask Bits is the number of bits used in the mask, and CIDR is just a pretty version of the Prefix and the Mask Bits. For example, the fourth pass through the For loop uses four bits for the mask. It finds a full supernet matching the four leftmost bits 0101. The supernet Bits are 01010000, Prefix is 80, Masked Bits is 4, and the CIDR is 80.0.0.0/4.
The supernet is added to the collection of supernets. We then need to remove the networks in the supernet from the $records variable so we don't use them again. This is done by filtering the $records variable for all networks that match our masked bits. In the case above, we would remove all networks that start with 0101xxxx.
The loop then starts over with a slightly bigger mask which looks from smaller supernets.
All we have left to do is output the results.
#output the results
$supernets | Sort Bits | Select Prefix,Bits,MaskBits,CIDR
Results:
Prefix Bits MaskBits CIDR
——— —— ———— ——
0 00000000 7 0.0.0.0/7
2 00000010 8 2.0.0.0/8
5 00000101 8 5.0.0.0/8
10 00001010 8 10.0.0.0/8
14 00001110 8 14.0.0.0/8
23 00010111 8 23.0.0.0/8
27 00011011 8 27.0.0.0/8
31 00011111 8 31.0.0.0/8
36 00100100 7 36.0.0.0/7
39 00100111 8 39.0.0.0/8
41 00101001 8 41.0.0.0/8
42 00101010 8 42.0.0.0/8
46 00101110 8 46.0.0.0/8
49 00110001 8 49.0.0.0/8
58 00111010 7 58.0.0.0/7
60 00111100 7 60.0.0.0/7
62 00111110 8 62.0.0.0/8
77 01001101 8 77.0.0.0/8
78 01001110 7 78.0.0.0/7
80 01010000 4 80.0.0.0/4
100 01100100 6 100.0.0.0/6
104 01101000 7 104.0.0.0/7
106 01101010 8 106.0.0.0/8
109 01101101 8 109.0.0.0/8
110 01101110 7 110.0.0.0/7
112 01110000 4 112.0.0.0/4
175 10101111 8 175.0.0.0/8
176 10110000 5 176.0.0.0/5
185 10111001 8 185.0.0.0/8
186 10111010 7 186.0.0.0/7
189 10111101 8 189.0.0.0/8
190 10111110 8 190.0.0.0/8
193 11000001 8 193.0.0.0/8
194 11000010 7 194.0.0.0/7
197 11000101 8 197.0.0.0/8
200 11001000 6 200.0.0.0/6
210 11010010 7 210.0.0.0/7
212 11010100 7 212.0.0.0/7
217 11011001 8 217.0.0.0/8
218 11011010 7 218.0.0.0/7
220 11011100 6 220.0.0.0/6
224 11100000 3 224.0.0.0/3
I hope that helps.
All registry keys have a value associated with called the Last Write Time. This is analogous to the last modification time for a file. When ever the registry key or one if its values has been created, modified, or deleted the value is updated to the current local system time. Unfortunately, there is no Last Write Time associated with a registry value, but it can be infered from the Last Write Time of the key.
Here is a PowerShell script to read the Last Write Time for a registry key.
Usage:
Get-RegKeyLastWriteTime.ps1 <Key> <SubKey>
Example:
Get-RegKeyLastWriteTime.ps1 HKLM SOFTWARE\Microsoft\Windows\CurrentVersion
Output:
Key LastWriteTime —- ——————- AdminDebug 10/28/2009 7:50:51 PM App Management 7/14/2009 4:41:12 AM App Paths 1/22/2010 2:07:18 PM Applets 7/14/2009 4:41:12 AM Audio 7/14/2009 4:41:12 AM Authentication 7/14/2009 4:41:12 AM BitLocker 7/14/2009 4:41:12 AM ...
Get-RegKeyLastWriteTime.ps1 Script:
param ( [string] $Key, [string] $SubKey )
switch ($Key) {
"HKCR" { $searchKey = 0x80000000} #HK Classes Root
"HKCU" { $searchKey = 0x80000001} #HK Current User
"HKLM" { $searchKey = 0x80000002} #HK Local Machine
"HKU" { $searchKey = 0x80000003} #HK Users
"HKCC" { $searchKey = 0x80000005} #HK Current Config
default {
#throw "Invalid Key. Use one of the following options HKCR, HKCU, HKLM, HKU, HKCC"
}
}
$KEYQUERYVALUE = 0x1
$KEYREAD = 0x19
$KEYALLACCESS = 0x3F
$sig1 = @'
[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int RegOpenKeyEx(
int hKey,
string subKey,
int ulOptions,
int samDesired,
out int hkResult);
'@
$type1 = Add-Type -MemberDefinition $sig1 -Name Win32Utils `
-Namespace RegOpenKeyEx -Using System.Text -PassThru
$sig2 = @'
[DllImport("advapi32.dll", EntryPoint = "RegEnumKeyEx")]
extern public static int RegEnumKeyEx(
int hkey,
int index,
StringBuilder lpName,
ref int lpcbName,
int reserved,
int lpClass,
int lpcbClass,
out long lpftLastWriteTime);
'@
$type2 = Add-Type -MemberDefinition $sig2 -Name Win32Utils `
-Namespace RegEnumKeyEx -Using System.Text -PassThru
$sig3 = @'
[DllImport("advapi32.dll", SetLastError=true)]
public static extern int RegCloseKey(
int hKey);
'@
$type3 = Add-Type -MemberDefinition $sig3 -Name Win32Utils `
-Namespace RegCloseKey -Using System.Text -PassThru
$hKey = new-object int
$result = $type1::RegOpenKeyEx($searchKey, $SubKey, 0, $KEYREAD, [ref] $hKey)
#initialize variables
$builder = New-Object System.Text.StringBuilder 1024
$index = 0
$length = [int] 1024
$time = New-Object Long
#234 means more info, 0 means success. Either way, keep reading
while ( 0,234 -contains $type2::RegEnumKeyEx($hKey, $index++, `
$builder, [ref] $length, $null, $null, $null, [ref] $time) )
{
#create output object
$o = "" | Select Key, LastWriteTime
$o.Key = $builder.ToString()
$o.LastWriteTime = (Get-Date $time).AddYears(1600)
$o
#reinitialize for next time through the loop
$length = [int] 1024
$builder = New-Object System.Text.StringBuilder 1024
}
$result = $type3::RegCloseKey($hKey);
C:\> tasklist /FI "modules eq metsrv.dll"PowerShell
PS C:\> Get-Process | ? { $_.Modules -like "*(metsrv.dll)*" }In version MetaSploit 3.3, and presumably future versions, the metsrv.dll is not visible due to Reflective DLL injection. It does work on v2 and v3.0-3.2. However, there are still footprints of meterpreter in v3.3. Two other dll's are loaded with meterpreter that many processes don't load.C:\WINDOWS\system32\rsaenh.dllWe can look for processes that have these two dll's loaded using either of these two commands.
C:\WINDOWS\system32\IPHLPAPI.DLL
C:\> tasklist /fi "MODULES eq rsaenh.dll" /fi "MODULES eq iphlpapi.dll"PowerShell
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*"
-and $_.Modules -like "*(iphlpapi.dll)*"}The problem is, some processes load these dll's so it isn't a 100% sign of pwnage. The processes include:explorer.exeIf IE were compromised it wouldn't be obvious, but it is obvious if Icecast was.
iexplore.exe
lsass.exe
svchost.exe
winlogon.exe
PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*"
-and $_.Modules -like "*(iphlpapi.dll)*"} | select ProcessName
ProcessName
—————-
explorer
Icecast2
IEXPLORE
lsass
svchost
svchost
svchost
winlogonIt is also apparent if meterpreter has been migrated to a process that doesn't normally load the dll's. In my testing I migrated to calc. Here are the results now.PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*"
-and $_.Modules -like "*(iphlpapi.dll)*"} | select ProcessName
ProcessName
—————-
calc
explorer
Icecast2
IEXPLORE
lsass
svchost
svchost
svchost
winlogonIf we had a baseline of processes that load these dll's then we can use PowerShell to filter out processes that don't normally load the dll's.PS C:\> Get-Process | ? { $_.Modules -like "*(rsaenh.dll)*"
-and $_.Modules -like "*(iphlpapi.dll)*" -and
"explorer","iexplore","lsass","svchost","winlogon" -notcontains $_.ProcessName }
ProcessName
—————-
calc
Icecast2
In this example Icecast2 was the initial point of compromise and meterpreter has migrated to calc.
While this isn't a perfect way to find meterpreter it is better than nothing.
UPDATE:
According to Stephen Fewer, one of the MetaSploit developers:
iphlpapi.dll is imported by the meterpreters stdapi extension for the route and ipconfig commands.
rsaenh.dll (The Microsoft Enhanced Cryptographic Provider DLL) is being loaded via advapi32.dll after a call from the openssl subsystem within meterpreter calling advapi32!CryptAcquireContext[1]
Ed Skoudis used the for loop to create an ftp script for the ftp command in order to do a port scan. I did an modification to it so that it didn't require the script file and no files were written to the file system. You can find that posting here:
http://blog.securitywhole.com/2009/02/28/ftp-port-scanning.aspx
In my quest to port the Kung Fu of Mr. Skoudis in to powershell I came up with this command:
1..1024 | % { echo ((new-object Net.Sockets.TcpClient).Connect("10.10.10.10",$_)) "$_ is open" } 2>out-null
If you have been following the previous entries there isn't anything fancy here, except one handy little trick that has to do with the output from the echo command. If you look closely you see that the command attempts to write the output of the connection as well as the string at the end. If the first portion throws an error, then the second part isn't output. Here is a simple example with the output.PS C:\> echo (1+1) (2+2)
2
4
PS C:\> echo (1/0) (2+2) 2>Out-Null
(No Output)
We can use this to our advantage. If our connection fails, an error is raised and we don't output the "$_ is open" portion. If the connection works then the "$_ is open" is displayed.
Unfortunately, there is no easy way to change the connection timeout so this process is slow. We can do it with asynchronous calls, but that is a lot of work and is no longer a one liner. I'll put that in a future version.
Stealing two other commands from Mr. Skoudis we can do an nslookup of each host in a range.
for /L %i in (1,1,255) do @echo 10.10.10.%i: & @nslookup 10.10.10.%i 2>nul | find "Name"
10.10.10.1
10.10.10.2
10.10.10.3
Name: server.blah.com
10.10.10.4
for /L %i in (1,1,255) do @nslookup 10.10.10.%i 2>nul | find "Name" && echo 10.10.10.%i
Name: server.blah.com
Here is the powershell version and it's output:
1..255 | % { [System.Net.Dns]::GetHostByAddress("10.10.10.$_") } 2> Out-Null | Format-List
HostName : server.blah.com
Aliases : {loadbalancer.blah.com, service.blah.com, service2.blah.com, service3.blah.com}
AddressList : {10.10.10.3}
Using the [System.Net.Dns]::GetHostByAddress() method gives us more power, plus we can send the objects we want down the pipeline for further actions. We use the 2> Out-Null so that the error messages for the unresolvable IP addresses aren't shown.
Ed Skoudis came up with some fantastic Command Line Kung Fu for Windows to do some basic scanning. Powershell is becoming more and more common so I decided to port these commands to powershell. I think Ed would agree that the standard windows commands can be rather painful and aren't easily extensible (blasted windows) and I hoped to make it slightly less agonizing. In order to make it easier to understand, I won't use the shortcuts in my examples for the foreach-object cmdlet (%) or where-object cmdlet (?).
The first CLKF I thought I would tackle was the ping sweep. You can check out the great write-up over at the Command Line Kung Fu Blog.
http://blog.commandlinekungfu.com/2009/03/episode-6-command-line-ping-sweeper.html
Taken from the blog, here is the Windows command to do ping sweep at the command line and its associated output:
C:\>for /L %i in (1,1,255) do @ping -n 1 10.10.10.%i | find "Reply"
Reply from 192.168.1.1: bytes=32 time=4ms TTL=64
Reply from 192.168.1.3: bytes=32 time=5ms TTL=64
Reply from 192.168.1.37: bytes=32 time=4ms TTL=64
Here is the powershell version and its output:
PS C:\>1..255 | foreach-object { (new-object System.Net.Networkinformation.Ping).Send("10.10.10.$_") } | where-object {$_.Status -eq "success"} | select Address
Address
———-
10.10.10.1
10.10.10.3
10.10.10.37
In the above command the range operator (..) generates a list of the numbers 1 through 255. The cool thing is you don't have to use just a single range, you can string them together like this (1..5),7,(9..10) which would give you the numbers 1-10 skipping 6 and 8.
foreach-object { (new-object System.Net.Networkinformation.Ping).Send("10.10.10.$_") }
The foreach-object takes the numbers fed into the pipeline and operates on them one at a time. First, it creates a new ping object and then calls the send method. The parameter given to the send method is a string concatenation of 10.10.10. and the number from $_, which is the "current pipeline object." The $_ variable in our example will contain the numbers 1-255.where-object {$_.Status -eq "success"}
The output of the send method is the PingReply object which contains a status. We can filter the results only successful pings reply objects will be sent further down the pipeline.Select Address
Finally, all we care about is the address so that is the only piece we have displayed.
Now that we know how it works, let's pimp out our powershell version.
First, we don't have to just use a contiguous set of numbers. If we wanted to scan all ip address before 10.10.10.100, after 10.10.10.200 and 10.10.10.155 we could use this:
(1..99),(200..255),155 | foreach-object ....
We can use the results to feed into other commands. You can ping sweep an entire subnet and have it automatically do an nslookup, attempt to list the contents of the c$ share, and tell you that you are doing a good job (a little positive reinforcement never hurts).
PS C:\>1..255 | foreach-object { (new-object System.Net.Networkinformation.Ping).Send("10.10.10.$_") } | where-object {$_.Status -eq "success"} | foreach-object { nslookup $_; gci "\\$($_.Address)\c$"; echo "Good Job" }
The ping sweep can be sped up by setting a timeout value (in milliseconds). In the example below we set the timeout value to 100ms.... (new-object System.Net.Networkinformation.Ping).Send("10.10.10.$_", 100) ...
Next time we'll look into using the powershell version of nslookup and the brute force reverse dns lookup.Active Directory Authentication
Paste these lines into the CLI. The first two lines can be added via the GUI. VIC -> Configuration -> Security Profile -> Properties -> Add activeDirectorKerberos [sic] (NOT Kerberos).
esxcfg-firewall —openPort 88,tcp,out,KerberosClient
esxcfg-firewall —openPort 464,tcp,out,KerberosPasswordChange
esxcfg-auth —enablead —addomain agstar.local —addc mydc.mycdomain.blah
esxcfg-auth —enablekrb5 —krb5realm=agstar.local —krb5kdc=mydc.mycdomain.blah-–krb5adminserver=mydc.mycdomain.blah
Edit the VMWare Authentication deamon config located at /etc/pam.d/vmware-authd and add this line to the top:
auth sufficient /lib/security/pam_unix_auth.so shadow nullok
Prevent users’ password from expiring since that is taken care of in AD.
esxcfg-auth —passmaxdays=-1
Add users using the username found in AD
adduser jdoe
adduser ymomma
adduser bdover
Here is a version that was 200 times faster in my environment. Depending on the number of domain controllers it could be even faster for you. It does one big query for each domain controller and then compiles the results. The original script took 45 minutes, this version took 13 seconds.
This script returns a list with all users and their last logon date/time. You can then filter by logon's older than a certain date/time, sort, or export it.
$dcs = [System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain().DomainControllers | select name
$startdate = get-date('1/1/1601')
$lst = new-Object System.Collections.ArrayList
foreach ($dc in $dcs) {
$root = [ADSI] "LDAP://$($dc.Name):389"
$searcher = New-Object System.DirectoryServices.DirectorySearcher $root
$searcher.filter = "(&(objectCategory=person)(objectClass=user))"
$searcher.PropertiesToLoad.Add("name") | out-null
$searcher.PropertiesToLoad.Add("LastLogon") | out-null
$searcher.PropertiesToLoad.Add("displayName") | out-null
$searcher.PropertiesToLoad.Add("userAccountControl") | out-null
$searcher.PropertiesToLoad.Add("canonicalName") | out-null
$searcher.PropertiesToLoad.Add("title") | out-null
$searcher.PropertiesToLoad.Add("sAMAccountName") | out-null
$searcher.PropertiesToLoad.Add("sn") | out-null
$searcher.PropertiesToLoad.Add("givenName") | out-null
$results = $searcher.FindAll()
foreach ($result in $results)
{
$user = $result.Properties;
$usr = $user | select -property @{name="Name"; expression={$_.name}},
@{name="LastLogon"; expression={$_.lastlogon}},
@{name="DisplayName"; expression={$_.displayname}},
@{name="Disabled"; expression={(($_.useraccountcontrol[0]) -band 2) -eq 2}},
@{name="CanonicalName"; expression={$_.canonicalname}},
@{name="Title"; expression={$_.title}},
@{name="sAMAccountName"; expression={$_.samaccountname}},
@{name="LastName"; expression={$_.sn}},
@{name="FirstName"; expression={$_.givenname}}
$lst.Add($usr) | out-null
}
}
$lst | group name | select-object Name,
@{Expression={ ($_.Group | Measure-Object -property LastLogon -max).Maximum }; Name="LastLogon" },
@{Expression={ ($_.Group | select-object -first 1).DisplayName}; Name="DisplayName" },
@{Expression={ ($_.Group | select-object -first 1).CanonicalName}; Name="CanonicalName" },
@{Expression={ ($_.Group | select-object -first 1).Title}; Name="Title" },
@{Expression={ ($_.Group | select-object -first 1).sAMAccountName}; Name="sAMAccountName" },
@{Expression={ ($_.Group | select-object -first 1).LastName}; Name="LastName" },
@{Expression={ ($_.Group | select-object -first 1).FirstName}; Name="FirstName" },
@{Expression={ ($_.Group | select-object -first 1).Disabled}; Name="Disabled" } |
select-object Name, DisplayName, CanonicalName, Title, sAMAccountName, LastName, FirstName, Disabled,
@{Expression={ $startdate.adddays(($_.LastLogon / (60 * 10000000)) / 1440) }; Name="LastLogon" }