Tuesday, March 30, 2010

Tired of looking up security codes in events?

Hello, I have put together a few Powershell functions for this lockout tool I am working on, which help decode some of the various Kerberos and logon codes in security event log events. They are not full tested, but hopefully should work fine as is.
function decode-krbTktOpts([string]$code) {
 #code provided is in hex format
 if (-not($code -match "0x\d{8}")) {
   write-error "Invalid entry sent to decode-krbTktOpts function : value was : $code"
   return $null
 }
 
 
 ##########
 #Ticket options are 32 bits of flags.  Code comes in as hex.  Only bits 0-1,3-5,16-17,20,23,25-31 are used
 ##########
 $results = new-object collections.arraylist
 $code = [convert]::toint32($code.substring($code.indexof("x")+1), 16)
 if ($code -and 1) {
  $results.add("Validate") >$null
 } else if ($code -and 2) {
  $results.add("Renew") >$null
 } else if ($code -and 8) {
  $results.add("EncTktInSKey") >$null 
 } else if ($code -and 16) { 
  $results.add("RenewableOK") >$null
 } else if ($code -and 32) {
  $results.add("DisableTransitedCheck") >$null
 } else if ($code -and 65536) {
  $results.add("Canonicalize") >$null
 } else if ($code -and 131072) {
  $results.add("CNameInAddlTkt") >$null
 } else if ($code -and 1048576) {
  $results.add("OptHardwareAuth") >$null
 } else if ($code -and 8388608) {
  $results.add("Renewable") >$null
 } else if ($code -and 33554432) {
  $results.add("AllowPostDate") >$null
 } else if ($code -and 67108864) {
  $results.add("Proxy") > $null
 } else if ($code -and 134217728) {
  $results.add("Proxiable") > $null
 } else if ($code -and 268435456) {
  $results.add("Forwarded") > $null
 } else if ($code -and 536870912) {
  $results.add("Forwardable") > $null
 } 
 return $results

}

function decode-LogonErrorCode([string]$code) {
 #decode 32 bit microsoft logon error codes from Hex format (32 bit)
 if (-not($code -match "0x\d{8}")) {
   write-error "Invalid entry sent to decode-krbTktOpts function : value was : $code"
   return $null
 }
 
 switch($code.tolower()) {
  "0x0" { return  "Successful login" }
  "0xC0000064" { return "The specified user does not exist" }
  "0xC000006A" { return "The value provided as the current password is not correct" }
  "0xC000006C" { return "Password policy not met" }
  "0xC000006D" { return "The attempted logon is invalid due to a bad user name"}
  "0xC000006E" { return "User account restriction has prevented successful login"}
  "0xC000006F" { return "The user account has time restrictions and may not be logged onto at this time"}
  "0xC0000070" { return "The user is restricted and may not log on from the source workstation"}
  "0xC0000071" { return "The user account's password has expired"}
  "0xC0000072" { return "The user account is currently disabled"}
  "0xC000009A" { return "Insufficient system resources"}
  "0xC0000193" { return "The user's account has expired"}
  "0xC0000224" { return "User must change his password before he logs on the first time"}
  "0xC0000234" { return "The user account has been automatically locked" }
  default {return "Unknown code provided, unable to translate" }
 }
 
}

function decode-krbErrCode([string]$code) {
 #code provided is required to be in the hex format provided in the system event logs  ex: 0x2
 if (-not($code -match "x")) {
  #if we receive something in invalid format, try to convert to hex
  if ($code -match "\d+") {
   $code = "0x" + [string]::format("{0:x}",$code)
  } else {
   write-error "Invalid entry sent to decode-krbErrCode function : value was : $code"
   return $null
  }
 }
  
 switch($code.tolower()) {
  "0x0" { return ("KDC_ERR_NONE","No Error") }
  "0x1" { return ("KDC_ERR_NAME_EXP","Clients entry in Database has Expired") }
  "0x2" { return ("KDC_ERR_SERVICE_EXP","Servers entry in Database has Expired") }
  "0x3" { return ("KDC_ERR_BAD_PVNO","Request protocol version number not supported") }
  "0x4" { return ("KDC_ERR_C_OLD_MAST_KVNO","Client's key encrypted in old master key") }
  "0x5" { return ("KDC_ERR_S_OLD_MAST_KVNO","Servers key encrypted in old master key") }
  "0x6" { return ("KDC_ERR_C_PRINCIPAL_UNKNOWN","Client not found in Kerberos Database") }
  "0x7" { return ("KDC_ERR_S_PRINCIPAL_UNKNOWN","Server not found in Kerberos Database") }
  "0x8" { return ("KDC_ERR_PRINCIPAL_NOT_UNIQUE","Multiple principal entries in database") }
  "0x9" { return ("KDC_ERR_NULL_KEY", "The client or server has a null key") }
  "0xa" { return ("KDC_ERR_CANNOT_POSTDATE", "Ticket not eligible for postdating") }
  "0xb" { return ("KDC_ERR_NEVER_VALID","Requested start time is later than end time") }
  "0xc" { return ("KDC_ERR_POLICY","KDC policy rejects request") }
  "0xd" { return ("KDC_ERR_BADOPTION","KDC cannot accomodate requested option") }
  "0xe" { return ("KDC_ERR_ETYPE_NOSUPP","Kerberos server has no support for this encryption type") }
  "0xf" { return ("KDC_ERR_SUMTYPE_NOSUPP","Kerberos server has no support for checksum type") }
  "0x10" { return ("KDC_ERR_PADATA_TYPE_NOSUPP","Kerberos server has no support for PADATA type") }
  "0x11" { return ("KDC_ERR_TRTYPE_NOSUPP", "Kerberos server has no support for transited type") }
  "0x12" { return ("KDC_ERR_CLIENT_REVOKED","Clients credentials have been revoked") }
  "0x13" { return ("KDC_ERR_SERVICE_REVOKED","Credentials for server have been revoked") }
  "0x14" { return ("KDC_ERR_TGT_REVOKED","TGT has been revoked") }
  "0x15" { return ("KDC_ERR_CLIENT_NOTYET","Client not yet valid") }
  "0x16" { return ("KDC_ERR_SERVICE_NOTYET", "Server not yet valid") }
  "0x17" { return ("KDC_ERR_KEY_EXPIRED", "Password has expired - change password to reset") }
  "0x18" { return ("KDC_ERR_PREAUTH_FAILED","Preauthentication is invalid, bad password") }
  "0x19" { return ("KDC_ERR_PREAUTH_REQUIRED","Additional Preauthentication required") }
  "0x1f" { return ("KRB_AP_ERR_BAD_INTEGRITY","Integrity check on decrypted field failed") }
  "0x20" { return ("KRB_AP_ERR_TKT_EXPIRED","Ticket expired")}
  "0x21" { return ("KRB_AP_ERR_TKT_NYV","Ticket not yet valid")}
  "0x22" { return ("KRB_AP_ERR_REPEAT","Request is a replay")}
  "0x23" { return ("KRB_AP_ERR_NOT_US","The ticket isn't for us")}
  "0x24" { return ("KRB_AP_ERR_BADMATCH", "Ticket and authenticator do not match") }
  "0x25" { return ("KRB_AP_ERR_SKEW", "Clock skew is too big")}
  "0x26" { return ("KRB_AP_ERR_BADADDR", "Incorrect net address") }
  "0x27" { return ("KRB_AP_ERR_BADVERSION", "Protocol version mismatch") }
  "0x28" { return ("KRB_AP_ERR_MSG_TYPE", "Invalid message type") }
  "0x29" { return ("KRB_AP_ERR_MODIFIED", "Message stream modified") }
  "0x2a" { return ("KRB_AP_ERR_BADORDER", "Message out of order") }
  "0x2c" { return ("KRB_AP_ERR_BADKEYVER","Specified version of key is not available") }
  "0x2d" { return ("KRB_AP_ERR_NOKEY", "Service key not available") }
  "0x2e" { return ("KRB_AP_ERR_MUT_FAIL", "Mutual authentication failed") }
  "0x2f" { return ("KRB_AP_ERR_BADDIRECTION", "Incorrect message direction") }
  "0x30" { return ("KRB_AP_ERR_METHOD", "Alternative authentication method required") }
  "0x31" { return ("KRB_AP_ERR_BADSEQ", "Incorrect sequence number in message") }
  "0x32" { return ("KRB_AP_ERR_INAPP_CKSUM", "Inappropriate type of checksum in message") }
  "0x3c" { return ("KRB_ERR_GENERIC", "Generic error") }
  "0x3d" { return ("KRB_ERR_FIELD_TOOLONG","Field is too long for this implementation") }
  default { return ("Invalid code", "not in RFC") }
 }
  
}

Friday, March 26, 2010

Auditing Kerberos Delegation

Hello,

In the last few weeks I was doing some audits in my environment for accounts that were trusted for delegation. I thought I would share some LDAP searches with everyone, using Joe's great ADFIND tool. For those that are not too familiar with delegation, there are two different bits in the UserAccountControl attribute that are related to delegation. These are TRUSTED_FOR_DELEGATION (0x80000) which uses kerberos forwardable tickets, and TRUSTED_TO_AUTH_FOR_DELEGATION (0x1000000) which allows the delegated person/computer to request a ticket on a user's behalf. Both of these options should be used with extreme caution when the accounts are unconstrained, the second option even more so. If systems that are delegated in an unconstrained manner get compromised, anyone accessing them is basically giving up their account for any purpose to the compromised machine.



Finding all unconstrained delegated computer and user accounts, ignoring domain controllers.

adfind -h DCservername -b dc=mydomain,dc=com -s subtree -bit -f "(&(|(objectcategory=user)(objectcategory=computer))(|(userAccountControl:OR:=16777216)(userAccountControl:OR:=524288))(!(iscriticalsystemobject=TRUE))(!(msds-allowedtodelegateto=*)))" -t 9000 distinguishedname serviceprincipalname useraccountcontrol

Finding all constained delegated computer and user accounts, ignoring domain controllers.

adfind -h DCservername -b dc=mydomain,dc=com -s subtree -bit -f "(&(|(objectcategory=user)(objectcategory=computer))(|(userAccountControl:OR:=16777216)(userAccountControl:OR:=524288))(!(iscriticalsystemobject=TRUE))(msds-allowedtodelegateto=*))" -t 9000 distinguishedname serviceprincipalname useraccountcontrol

Thursday, March 25, 2010

.NET and NetBIOS name resolution

Recently I was looking at what it would take to throw together some code to chase failed logon events through multiple servers and workstations down to the source machine, source process, and/or source network connection. One of the problems that I encounter in security event logs, the source machines are often IP addresses instead of the system name. When dealing with reverse DNS records in dynamic environments, there are problem subnets in which hosts change IP's often. Reverse DNS allows multiple hosts to record the same record with a maximum per entry limit so high that name resolution is worthless. Even if you have a short scavenging period, your data may not be very accurate, or perhaps the source machine did not register a record. One idea that came to mind was doing a WMI lookup against the remote IP to pull the computer name, but that requires the appropriate level of access for remote WMI (if it is enabled), so it may not be as reliable. Being a sysadmin from the pre-windows 2000 period, I like to use nbtstat against the IP address to see the name of remote windows machines. I tried looking around the various classes of .NET and could find anything for netbios style name resolution, so to save myself the trouble of trying to parse through nbtstat command output strings, and dealing with that slowness (when using multiple NIC's and virtualization NIC's), I decided to roll my own solution. The code below was expanded more later for some additional functionality such as flag parsing. This is the simplified version that returns an array of PSObjects containing the various netbios records. For those not familiar with Netbios, it is the 15 character name format used in windows. The 16th byte of the name is a type value. Type 0x00 will give you the workstation name and domain name (group flag is enabled). The netbios query packet is pretty standard other than perhaps the transaction ID. The replied records are all fixed length and names are padded with 0x20 up to the 15 characters for the names. There is a number of records value that tells how many records were returned, so pulling the results is pretty basic.   (NOTE: the script opens a privileged port, so it requires admin rights on the local machine that you are running it on)


Function convert-netbiosType([byte]$val) {
 #note netbios type codes are usually in decimal, but .net likes to deal with bytes
 #as integers.
    
 $myval = [int]$val
 switch($myval) {
  0 { return "Workstation" }
  1 { return "Messenger service" }
  3 { return "Messenger" }
  6 { return "RAS" }
  32 { return "File Service" }
  27 { return "Domain Master Browser" }
  28 { return "Domain Controller" }
  29 { return "Master Browser" }
  30 { return "Browser election" }
  31 { return "NetDDE" }
  33 { return "RAS Client" }
  34 { return "Exchange MS mail connector" }
  35 { return "Exchange Store" }
  36 { return "Exchange Directory" }
  48 { return "Modem sharing service Server"}
  49 { return "Modem sharing service Client"}
  67 { return "SMS client remote control" }
  68 { return "SMS client remote transfer" }
  135 { return "Exchange MTA" }
  default { return "unk" }
 }
 
}



function get-netbios-name ([string]$ip) {
 #Function:  Get netbios name of the remote machine by IP address provided
 #  result:  Error = $null, positive result is hashtable of names
 if (-not ($ip -match "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")) {
  write-error "The Ip address provided: $ip  is not a valid IPv4 address format"
  return $null
 }
 #ping first for reachability check
 $po = New-Object net.NetworkInformation.PingOptions
 $po.set_ttl(64)
 $po.set_dontfragment($true)
#alexander ping
 [Byte[]] $pingbytes = (65,72,79,89)
 $ping = new-object Net.NetworkInformation.Ping
 $pingres = $ping.send($ip, 1000, $pingbytes, $po)
    
if ($pingres.status -eq "Success") {
    
#netbios name query  NBTNS
 $port=137
 $ipEP = new-object System.Net.IPEndPoint ([system.net.IPAddress]::parse($ip),$port)
 $udpconn = new-Object System.Net.Sockets.UdpClient
 [byte[]] $sendbytes = (0xf4,0x53,00,00,00,01,00,00,00,00,00,00,0x20,0x43,0x4b,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41 ,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,00,00,0x21,00,01)
 $udpconn.client.receivetimeout=1000
 $bytesSent = $udpconn.Send($sendbytes,50,$ipEP)
 $rcvbytes = $udpconn.Receive([ref]$ipEP)
 if ($? -eq $false -or $rcvbytes.length -lt 63) {
  write-error "System is not responding to netbios traffic on port 137, system is not a windows machine, or other error has occurred."
  return $null
    
 } else {
  [array]$nbnames = $null
  #nbtns query results have a number of returned records field at byte #56 of the returned
  #udp payload.  Read this value to find how many records we have
  $startptr = 56
  $numresults = [int]$rcvbytes[$startptr]
  $startptr++
  $namereclen = 18
  #loop through the number of results and get the names + data
  #  NETBIOS result =  15 byte of name (padded if shorted 0x20)
  #                     1 byte of type
  #                     2 byte of flags
    
    
  for ($i = 0; $i -lt $numresults; $i++) {
   $nbname = new-object PSObject
   $tempname = ""
   #read the 15 byte name and convert to human readable string
   for ($j = 0; $j -lt $namereclen -3; $j++) {
    $tempname += [char]$rcvbytes[$startptr + ($i * $namereclen) + $j]
       
   }
   add-member -input $nbname NoteProperty NetbiosName $tempname
   $rectype = convert-netbiosType $rcvbytes[$startptr + ($i * $namereclen) + 15]
   add-member -input $nbname NoteProperty  RecordType $rectype
   if (($rcvbytes[$startptr + ($i * $namereclen) + 16] -band 128) -eq 128 ) {
  #in the flags field, only the high order byte of the 2 is used
  #the left most bit is the Group name flag which can be used for domain
  #name type identification to differentiate the 0x00 type names
    $groupflag = 1
   } else { $groupflag = 0 }
    add-member -input $nbname NoteProperty IsGroupType $groupflag
    $nbnames += $nbname
   }
   return $nbnames
    
  }
 } else {
  write-error "System not pinging: $ip"
  #prompt for another ip to be inputted?
  return $null
 }
  
}

$ip = args[0]
if ($ip -eq $null -or $ip -eq "") {
  write-host "You need to provide an ip address to check"
  exit
}
get-netbios-name $ip

For those that use nbtstat, you will also know that it returns the MAC address, however not in the best or most efficient way. Since nbtstat tries to use every network adapter on the machine, if you have multiple nics and virtualization nics, then it is slow. The benefit of the powershell method above is that you only send out from one NIC. To get the MAC address, the code can be modified to pull it out of the received packets. MAC address is the last useful 6 bytes of the packet (which may be padded with extra 0's at the end). So you can use this to grab those bytes and format it to a mac address string.

$mac = (0,0,0,0,0,0)
$j = 5
for ($i = $rcvbytes.length - 1; $i -gt 0; $i--) {
   if ($rcvbytes[$i] -ne 0x0) {
      $mac[$j] = $rcvbytes[$i]
      $j--
      if ($j -eq -1) { $i = -1 }
   }
}
$macstring = ""
foreach ($byte in $mac) {
  $macstring += ("{0:X2}" -f $byte) + "-"
}
new-object psobject -property @{
   IP = $ip
   MacAddress = $macstring.trim("-")
}

Thursday, March 18, 2010

Fixing scattered non-permission inherting objects

Occasionally in our environment we come across problems with user management access due to inherited permissions not being enabled. This appears to be related to accounts that were once members of protected groups like server operators, backup operators, account operators, etc and the AdminSDHolder process unchecking inheritance. Over time as the users changed roles and were removed from these groups, inheritance was not manually enabled. There are scripts available that show how to edit this ACL attribute with vbscript for a single object, but I'm a Powershell guy and I want to hit a few thousand accounts at once. For the sake of simplicity, let us assume all of the accounts are in the same OU location: OU=admins,dc=mydomain,dc=com. Also note that OU=admins has inheritance disabled for security reasons and cannot be changed. If it could we could fix all objects in the OU with a single command.

If we are unconcerned with checking to see which accounts are set to not inherit, but we don't want to temporarily expose any current AdminSDHolder accounts to a short term reduction in security, we can do this:

$de = new-object DirectoryServices.DirectoryEntry("LDAP://ou=admins,dc=mydomain,dc=com")
$ds = new-object DirectoryServices.DirectorySearcher($de)
$ds.filter = "(&(objectclass=user)(!(admincount=1)))"
$ds.propertiestoload.add("distinguishedname")
$users = $ds.findall()

Now our variable (array) $users contains a list of LDAP results that can be used to provide distinguished names to the DSACLS command.

Loop through the results and enable permissions inheritance for all objects.

foreach ($user in $users) {
$dn = $user.properties.distinguishedname
dsacls $dn /P:n > $null
}

This will loop through all users found in the query and enable inheritance. If the top level OU did not need to be protected you could accomplish the same type of result with dsacls /T option to run the change on a whole tree of objects from the OU level down. The Powershell way allows more granular control and is helpful if you have several OU levels under the Admins OU that you don't want impacted. The DirectorySearcher class allows you to specify the search scope to limit the results.

In AD Powershell, the get-acl/set-acl options can provide similar capabilities, example here.

Thursday, March 11, 2010

Avoiding account lockout policy (pre Windows 2008)

Over the last year or so a few instances have come up with service accounts getting locked out and the owners being unhappy. Usually everyone wants to know if it is possible to set their account so it can't be locked out. Prior to Windows 2008 AD fine grained password policies and managed service accounts, the answer typically is No. The security guy in me says No way should this be done even if it is possible. But to be fair, lets look at technical feasibility.

Lockout policy/Password policy is a domain wide setting, for domain accounts it follows what policy is applying to the domain controllers. For domain member local accounts it follows whatever policy is pushed to the client system. But, there is in fact a way to avoid lockouts, though it is a bit unorthodox. Let me give some background then I will mention the how-to.

A few weeks ago, one morning I had this idea pop into my head that it would be interesting to explorer the security implications of computer accounts in the enterprise (This means computer objects...people like to mix "computer accounts" to mean user accounts). Since computer accounts have passwords and can access remote resources, I thought there may be some interesting things to find.

So on this mission, I first looked at what can you do with computer accounts. Normal tools, like ADUC, dsmod, etc don't let you set a computer account password manually. NET USER and admod will allow this however. So I created my object, reset the password and started messing around. I first found that you can't log in interactively with computer accounts and it gives you an error saying that a policy is blocking this. I granted all possible user rights to the account, and added to administrators group, but no help there. If you look at the UserAccountControl attribute, there are flags that indicate the type of account. There is also a SamAccountType attribute on users and computers. The value of this is slightly different, but you can't edit this attribute as it is controlled by the SAM. UserAccountControl however can be edited. There is some security implications with this related to computers that I will get into some other time.

Anyways, if you look at the values in the linked article above you can see these flags. You can't have both set at the same time, but you can change the computer object's useraccesscontrol level to that of a normal user. Coincidentally, this causes the SamAccountType value to be updated to match a user object as well.

Values:
0x1000 workstation trust account
0x0200 normal account

So I changed my test object's UAC to 0x0200, I retried local login and it worked fine. Also using it for scheduled tasks, services, etc were all working now as well. Previously I could only use the account to map network drives.

So this was an interesting discovery, but I took this a little further on login attempts. I noticed with repeat failed passwords my bad password count kept going up, well over the lockout threshold set in my test domain. When my normal user objects were getting locked, my computer account kept taking attempts over and over. Providing the correct password after multiple failures that should have resulted in lockout showed that I could still log in. So apparently the mechanism used to lock accounts ignores computer objects, doesn't look at UAC for them, and doesn't look at the SamAccountType. Given this, we can create an unlockable "user" by creating a computer object.

For those who may worry now about brute force password attempts on computer objects, given that most forms of logins are blocked for a standard account, and the auto generated password length is significantly high, the risk is low. For those that have pretty loose computer object creation policies, it may be time to start thinking in a different way. In the near future, I hope to provide some better details on the overall security implications of computer objects.

Monday, March 8, 2010

Converting time formats in Powershell

Since I am spending the morning looking at solutions to track down lockout events to source in a more automated fashion, I'm stuck looking at how to dig through event log events. Using the Active Directory values for bad password time helps to locate a domain controller with useful log events, but the format needs some adjustment. When looking at AD, the format can look like this:

lastlogontimestamp {129121380931028667}
badpasswordtime {129115363568077019}

In powershell it is very easy to convert this to a datetime object for further use. If you want to just display the value in a readable format, you can just use this command. If you want to work with it some more, assign it to a variable:

[datetime]::FromFileTime($mydatetime)

ex:

PS C:\> [datetime]::fromfiletime(129121380931028667)

Wednesday, March 03, 2010 7:01:33 PM


Now when looking at event logs with something such as WMI queries, the date time format is different.
Format: YYYYMMDDHHMMSS.000000-UUU
Reference

The System.Management.ManagementDateTimeConverter class can be used to convert between WMI's format and DateTime.

Once the value is in WMI format, into a wmi queries. If you are searching logs in a timewritten range you may want to adjust the seconds of this value +/- a few to help ensure that everything links up. You can also drop the milliseconds as the AD time stamp may not perfectly match up to a system event log time stamp. In any case, having a tight time range and very specific WMI query will help keep remote event log search queries fast and leave you will less results to process in any addition work you plan to do.

Some basic code using these conversions and providing a time range for event log searches:


function wmiTime-toDateTime([string]$wmitime){
#YYYYMMDDHHMMSS.000000-UUU where -UUU is the three-digit offset
return [system.management.managementdatetimeconverter]::todatetime($wmitime)

}

function dt-toWmiTime([system.datetime]$dt) {
#YYYYMMDDHHMMSS.000000-UUU where -UUU is the three-digit offset
$dt = $dt.touniversaltime()
$wmiformat = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($dt)
return $wmiformat

}

function adtime-toWMItime([string]$mydatetime) {
$dt = [datetime]::FromFileTime($mydatetime) 
$wmitime =  dt-toWmiTime $dt
return $wmitime
}


function adjust-wmitime ([string]$wmiOrigTime, [double]$secondOffset) {
#WMI time format: YYYYMMDDHHMMSS.000000-UUU where -UUU is the three-digit offset
#This function takes an original time and provides an array of two values, search start time and
#search end time

$mybasetime = wmiTime-toDateTime $wmiOrigTime
$myadjustedDT = $mybasetime.addseconds($secondoffset)
$myResultingWMI = dt-toWmiTime $myadjustedDT
return $myResultingWMI

}

function compare-wmiTimes($startTime,$endTime) {
 $dtS = wmiTime-toDateTime $startTime
 $dtE = wmiTime-toDateTime $endTime
 $timediff = new-timespan -start $dtS -end $dtE
 if ($timediff.TotalMilliseconds -gt 0) {
  #second date is later than the first
  return 1
 } elseif ($timediff.TotalMilliseconds -eq 0) {
  #time is the same
  return 0
 } else {
  #first date is later than the second
  return -1
 }

}

function wmitime-timerange ([string]$wmiOrigTime, $secondOffset) {
$secondoffset = [double]$secondoffset
$mystartWMI = adjust-wmitime $wmiOrigTime -$secondoffset
$myendWmi = adjust-wmitime $wmiOrigTime $secondoffset
$myResult = ($mystartWMI, $myendWMI)
return $myResult
}



Wednesday, March 3, 2010

Checking SSL certificate values with Powershell

For anyone that needs to check SSL certificates in a simple way from Powershell, I created something for this purpose a while back. It works for most SSL connections using .NET code and will throw exceptions if the name on the cert you provide is not valid, or the cert is expired.

Check-sslcert.ps1 (Updated Jan 15, 2013)


#Requires -version 2.0
 
param(
 [parameter(mandatory=$true,helpmessage="IP address or hostname to resolve remote system")][string]$ipaddr,
 [parameter(mandatory=$true,helpmessage="TCP port number that SSL application is listening on")][int]$port,
 [parameter(helpmessage="Hostname on certificate")][string]$myhostname=$ipaddr,
 [parameter(helpmessage="Verbose")][alias('fulldetail')][switch]$V
 
)


function stripcomma([string]$tempstring) {
 write-debug "In Function StripComma $($tempstring)"
 return $tempstring.replace(',',';') 
 
}

function convertoid([string]$oid) {
 write-debug "In function ConvertToOID: $($oid)"
 #strip off oid component common to all crypto types
 $oidstr = $oid.replace("1.2.840.113549.1.","")
 
 #pull out first number
 $firstval = $oidstr.substring(0,$oidstr.indexof('.'))
 
 #pull out second number for more detail
 $sub = $oidstr.substring(2)
 if ($sub.indexof('.') -gt 0) {
  $sub = $sub.substring(0,$sub.indexof('.')) 
 }
 
 if ($firstval -eq "1") {
  $format = "PKCS-1"
  switch ($sub) {
   "1" { return ($format + " RSA Encryption") }
   "2" { return ($format + " MD2 with RSA") }
   "3" { return ($format + " rsadsi md4 with RSA")}
   "4" { return ($format + " MD5 with RSA") }
   "5" { return ($format + " SHA-1 with RSA") }
   "6" { return ($format + " rsaOAEPEncryptionSet")}
   "11" { return ($format + " sha256 with RSA") }
  }
 } elseif ($firstval -eq "5") {
  $format = "RSA PKCS5"
  switch ($sub) {
   "1" { return ($format + " rsadsi pbe with MD2 DES-CBC")}
   "3" { return ($format + " rsadsi pbe with MD5 DES-CBC")}
   "4" { return ($format + " pbe with MD2 and RC2_CBC")}
   "6" { return ($format + " pbe with MD5 and RC2_CBC")}
   "9" { return ($format + " pbe with MD5 and XOR")}
   "10" { return ($format + " pbe with SHA1 and DES-CBC")}
   "11" { return ($format + " pbe with SHA1 and RC2_CBC")}
   "12" { return ($format + " id-PBKDF2 key derivation function")}
   "13" { return ($format + " id-PBES2  PBES2 encryption")}
   "14" { return ($format + " id-PBMAC1 message auth scheme")}
   
  }
 } elseif ($firstval -eq "7" ) {
  $format = "PKCS-7"
  switch ($sub) {
   "1" { return ($format + " data")}
   "2" { return ($format + " signed data")}
   "3" { return ($format + " enveloped data")}
   "4" { return ($format + " signed and enveloped data")}
   "5" { return ($format + " digested data")}
   "6" { return ($format + " encrypted data")}
  }
 } elseif ($firstval -eq "12") {
  return ("PKCS-12")
 } elseif ($firstval -eq "15") {
  return ("PKCS-15") 
 } else {
  return $oid 
 }
   
 
}

######
#MAIN#
######

#open TCP connection
try {
 $conn = new-object system.net.sockets.tcpclient($ipaddr,$port) 
 
 try {
  #create ssl stream on existing tcp connection
  $stream = new-object system.net.security.sslstream($conn.getstream())
  #send hostname on cert to try SSL negotiation
  $stream.authenticateasclient($myhostname) 
  
  $cert = $stream.get_remotecertificate()
  $cert2 = New-Object system.security.cryptography.x509certificates.x509certificate2($cert)    #can get much more information with this class    

  $validto = [datetime]::Parse($cert.getexpirationdatestring())
  $validfrom = [datetime]::Parse($cert.geteffectivedatestring())
  
  if ($V) {
   new-object psobject -property @{ 
    Connection = "Success"
    Machine = $ipaddr
    CertFormat = ($cert.getformat())
    CertExpiration = $validto
    CertIssueDate = $validfrom
    CertIssuer = ($cert.get_issuer())
    SerialNumber = ($cert.getserialnumberstring())
    CertSubject = (stripcomma $cert.get_subject())
    CertType =  (convertoid $cert.getkeyalgorithm())
   }
  } else {
   #non verbose
   New-Object psobject -Property @{
    Connection = "Success"
    Machine = $ipaddr
    CertExpiration = $validto
   }
  }

 } catch {
  #if SSL connection failed, cert may be invalid or name on cert didn't match, fails either way
  throw $_
 } finally {
  Write-Debug "In finally: closing connection"
  $conn.close()
 }
} catch {
 Write-Verbose "Error occurred connecting to $($ipaddr)"
 New-Object PSObject -Property @{
  Machine = $ipaddr
  Connection = "Failure"
  Status = $_.exception.innerexception.message
 }
 
}

Tuesday, March 2, 2010

System.security.principal.windowsprincipal IsInRole() Trust relationship problem

Hello everyone,

I recently ran across a problem with an application using .NET's System.Security.Principal.WindowsPrincipal's IsInRole() function failing with error "The trust relationship between the primary domain and the trusted domain failed". The application was looking for local group memberships on the workstations to validate access to the application. To give a quick background, I'm not a developer and its the first time I have seen this object in use. A simplified idea of the domain structure can be shown as:


The application was receiving this error in child domain #2, but working in child 1. Legacy domain dc's had just be decommissioned.

When digging through the history of the machines, user accounts involved, group object metadata, etc...the users were found to have not been a member of any of the local groups the code was looking for. Tracing the exception with some basic Powershell tests from the effected domain...


$me = [system.security.principal.windowsidentity]::getcurrent()

$up = new-object system.security.principal.windowsprincipal($me)

$up.isinrole("Administrators") **can be local or domain group, not specified, account is both**
True

$up.isinrole("blah") **can be local or domain group, group does not exist
Exception calling "IsInRole" with "1" argument(s): "The trust relationship between the primary domain and the trusted domain failed."


Here we have success, or failure depending on perspective. As we have not specified the domain in any of our code, the code is checking local and trusted groups. I'm not digging deeply into finding out how it does it or what order. Netmon traces suggest some system calls for group membership to domain controllers, and showed name resolution attempts to the decomissioned trusted domain.


If we try the same test from Child domain 1


$up.isinrole("blah")
False


If we try the same test from child domain 2 with domain specified

$up.isinrole("Child1\blah")
False


No exceptions now. How do we find local groups when the machine name is not known?

$up.isinrole("\users")
True

$up.isinrole("\blah")
False

This seems to work for me for specifying a local group. Eventually a fix for this type of problem involves code being able to handle intermittent domain trust problems , but also removing any legacy trusts along with a decommissioning of domain. Having code that better specifies the scope of the group or domain of the group would also improve reliability. I hope this post helps for anyone that encounters this error with this particular .NET class.