From dac80958c5ef3b139fb157dadbe6f5a761b84d5e Mon Sep 17 00:00:00 2001 From: vletoux Date: Fri, 12 Jul 2019 08:31:23 +0200 Subject: [PATCH] PingCastle 2.7.0.0 --- ADWS/ADDomainInfo.cs | 1 - ADWS/ADItem.cs | 1156 +++++++++-------- ADWS/ADWSConnection.cs | 10 +- ADWS/ADWebService.cs | 17 +- ADWS/LDAPConnection.cs | 48 +- Compatibility.cs | 9 + ConsoleMenu.cs | 71 +- Data/CompromiseGraphData.cs | 4 +- Data/DomainKey.cs | 14 +- Data/HealthcheckData.cs | 64 +- Graph/Database/IDataStorage.cs | 21 +- Graph/Database/LiveDataStorage.cs | 129 +- Graph/Database/RelationType.cs | 84 +- .../ExportDataFromActiveDirectoryLive.cs | 139 +- Graph/Export/IRelationFactory.cs | 15 + Graph/Export/RelationFactory.cs | 380 +++--- ...CompromiseGraphPrivilegedOperatorsEmpty.cs | 1 + Healthcheck/ADModel.cs | 40 +- Healthcheck/HealthcheckAnalyzer.cs | 804 +++++++++--- ...lcheckRuleAnomalyAnonymousAuthorizedGPO.cs | 1 + ...eatlcheckRuleAnomalyCertMD2Intermediate.cs | 1 + .../Rules/HeatlcheckRuleAnomalyCertMD2Root.cs | 1 + ...eatlcheckRuleAnomalyCertMD4Intermediate.cs | 1 + .../Rules/HeatlcheckRuleAnomalyCertMD4Root.cs | 1 + ...eatlcheckRuleAnomalyCertMD5Intermediate.cs | 1 + .../Rules/HeatlcheckRuleAnomalyCertMD5Root.cs | 1 + ...atlcheckRuleAnomalyCertSHA0Intermediate.cs | 1 + .../HeatlcheckRuleAnomalyCertSHA0Root.cs | 1 + ...atlcheckRuleAnomalyCertSHA1Intermediate.cs | 1 + .../HeatlcheckRuleAnomalyCertSHA1Root.cs | 1 + .../Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs | 1 + ...eckRuleAnomalyDCRefuseComputerPwdChange.cs | 39 + .../Rules/HeatlcheckRuleAnomalyDCSpooler.cs | 3 +- ...tlcheckRuleAnomalyDsHeuristicsAnonymous.cs | 2 +- ...eatlcheckRuleAnomalyLDAPSigningDisabled.cs | 41 + .../Rules/HeatlcheckRuleAnomalyLMHash.cs | 5 +- ...HeatlcheckRuleAnomalyMembershipEveryone.cs | 2 +- .../Rules/HeatlcheckRuleAnomalyNoGPOLLMNR.cs | 41 + .../Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs | 1 + .../Rules/HeatlcheckRuleAnomalyNullSession.cs | 1 + ...checkRuleAnomalySMB2SignatureNotEnabled.cs | 3 +- ...heckRuleAnomalySMB2SignatureNotRequired.cs | 3 +- .../HeatlcheckRulePrivilegedDelegated.cs | 2 +- ...tlcheckRulePrivilegedDelegationEveryone.cs | 5 +- ...eckRulePrivilegedDelegationFileDeployed.cs | 36 + ...atlcheckRulePrivilegedDelegationGPOData.cs | 2 + ...tlcheckRulePrivilegedDelegationKeyAdmin.cs | 3 +- ...heckRulePrivilegedDelegationLoginScript.cs | 2 + ...ilegedDsHeuristicsAdminSDExMaskModified.cs | 28 + ...kRulePrivilegedDsHeuristicsDoListObject.cs | 28 + ...heckRulePrivilegedExchangeAdminSDHolder.cs | 3 +- ...HeatlcheckRulePrivilegedExchangePrivEsc.cs | 27 + .../HeatlcheckRulePrivilegedKerberoasting.cs | 44 + ...HeatlcheckRulePrivilegedLoginDCEveryone.cs | 41 + ...atlcheckRulePrivilegedPrivilegeEveryone.cs | 5 +- ...ckRulePrivilegedRecoveryModeUnprotected.cs | 39 + .../HeatlcheckRulePrivilegedRecycleBin.cs | 28 + .../HeatlcheckRulePrivilegedSchemaAdmins.cs | 2 +- ...ckRulePrivilegedUnconstrainedDelegation.cs | 3 +- ...atlcheckRulePrivilegedUnknownDelegation.cs | 1 + ...eatlcheckRuleStaleADRegistrationEnabled.cs | 5 +- .../HeatlcheckRuleStaledDCSubnetMissing.cs | 50 +- .../Rules/HeatlcheckRuleStaledSMBv1.cs | 2 +- ...tlcheckRuleTrustFileDeployedOutOfDomain.cs | 41 + ...atlcheckRuleTrustLoginScriptOutOfDomain.cs | 14 +- .../Rules/HeatlcheckRuleTrustTGTDelegation.cs | 57 + Healthcheck/Rules/RuleDescription.resx | 732 +++++++++-- Healthcheck/TrustAnalyzer.cs | 28 +- NativeMethods.cs | 63 +- PingCastle.csproj | 36 +- PingCastle.sln | 20 + PingCastleAutoUpdater/App.config | 6 + .../PingCastleAutoUpdater.csproj | 69 + PingCastleAutoUpdater/Program.cs | 273 ++++ .../Properties/AssemblyInfo.cs | 36 + PingCastleAutoUpdater/pingcastle.ico | Bin 0 -> 34063 bytes Program.cs | 118 +- Properties/AssemblyInfo.cs | 4 +- README.md | 3 + Report/ReportBase.cs | 308 ++++- Report/ReportCompromiseGraph.cs | 72 +- Report/ReportCompromiseGraphConsolidation.cs | 21 +- Report/ReportHealthCheckConsolidation.cs | 64 +- Report/ReportHealthCheckRules.cs | 375 ++++++ Report/ReportHealthCheckSingle.cs | 441 +++++-- Report/ReportHealthCheckSingleCompared.cs | 74 ++ Report/ReportMapBuilder.cs | 130 +- Report/ReportNetworkMap.cs | 1021 +++++++++++++++ Report/ReportRiskControls.cs | 50 +- Rules/RiskModelCategory.cs | 10 +- Rules/RuleAttribute.cs | 66 +- Scanners/ACLScanner.cs | 25 +- Scanners/AntivirusScanner.cs | 268 ++++ Scanners/ConsistencyScanner.cs | 4 +- Scanners/ForeignUsersScanner.cs | 6 +- Scanners/LAPSBitLocker.cs | 140 ++ Scanners/ReplicationScanner.cs | 2 + Scanners/ScannerBase.cs | 24 +- Scanners/Smb2Protocol.cs | 11 +- Scanners/SmbScanner.cs | 4 +- Tasks.cs | 28 +- changelog.txt | 33 + misc/RegistryPolReader.cs | 16 +- misc/Subnet.cs | 84 ++ shares/ShareEnumerator.cs | 7 +- template/bootstrap.min.css.gz | Bin 21023 -> 21302 bytes template/bootstrap.min.js.gz | Bin 14057 -> 14929 bytes template/responsivetemplate.html.gz | Bin 342 -> 327 bytes 108 files changed, 6594 insertions(+), 1706 deletions(-) create mode 100644 Graph/Export/IRelationFactory.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleAnomalyDCRefuseComputerPwdChange.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleAnomalyLDAPSigningDisabled.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleAnomalyNoGPOLLMNR.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationFileDeployed.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsAdminSDExMaskModified.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsDoListObject.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedExchangePrivEsc.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedKerberoasting.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedLoginDCEveryone.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedRecoveryModeUnprotected.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedRecycleBin.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleTrustFileDeployedOutOfDomain.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleTrustTGTDelegation.cs create mode 100644 PingCastleAutoUpdater/App.config create mode 100644 PingCastleAutoUpdater/PingCastleAutoUpdater.csproj create mode 100644 PingCastleAutoUpdater/Program.cs create mode 100644 PingCastleAutoUpdater/Properties/AssemblyInfo.cs create mode 100644 PingCastleAutoUpdater/pingcastle.ico create mode 100644 Report/ReportHealthCheckRules.cs create mode 100644 Report/ReportHealthCheckSingleCompared.cs create mode 100644 Report/ReportNetworkMap.cs create mode 100644 Scanners/AntivirusScanner.cs create mode 100644 Scanners/LAPSBitLocker.cs create mode 100644 misc/Subnet.cs diff --git a/ADWS/ADDomainInfo.cs b/ADWS/ADDomainInfo.cs index ff40136..b944c17 100644 --- a/ADWS/ADDomainInfo.cs +++ b/ADWS/ADDomainInfo.cs @@ -90,7 +90,6 @@ private static int ExtractIntValue(XmlNode item) public static ADDomainInfo Create(DirectoryEntry rootDSE) { ADDomainInfo info = new ADDomainInfo(); - Trace.WriteLine("rootDse property count: " + rootDSE.Properties.Count); info.DefaultNamingContext = rootDSE.Properties["defaultNamingContext"].Value as string; info.ConfigurationNamingContext = rootDSE.Properties["configurationNamingContext"].Value as string; info.DnsHostName = rootDSE.Properties["dnsHostName"].Value as string; diff --git a/ADWS/ADItem.cs b/ADWS/ADItem.cs index 72824fb..f1f8948 100644 --- a/ADWS/ADItem.cs +++ b/ADWS/ADItem.cs @@ -20,39 +20,43 @@ namespace PingCastle.ADWS { [DebuggerDisplay("{DistinguishedName}")] - public class ADItem - { - public class ReplPropertyMetaDataItem - { - public int AttrType { get; set; } - public int Version { get; set; } - public DateTime LastOriginatingChange { get; set; } - public Guid LastOriginatingDsaInvocationID { get; set; } - public long UsnOriginatingChange { get; set; } + public class ADItem + { + public class ReplPropertyMetaDataItem + { + public int AttrType { get; set; } + public string AttrName { get; set; } + public int Version { get; set; } + public DateTime LastOriginatingChange { get; set; } + public Guid LastOriginatingDsaInvocationID { get; set; } + public long UsnOriginatingChange { get; set; } public long UsnLocalChange { get; set; } - } + } - public int AdminCount { get; set; } + public int AdminCount { get; set; } public string AttributeID { get; set; } - public X509Certificate2Collection CACertificate { get; set; } - public string Class { get; set; } - public string Description { get; set; } - public string DistinguishedName { get; set; } - public string DisplayName { get; set; } - public string DnsRoot { get; set; } - public string DNSHostName { get; set; } - public string DSHeuristics { get; set; } - public int DSMachineAccountQuota { get; set; } - public int Flags { get; set; } - public string GPLink { get; set; } - public string GPCFileSysPath { get; set; } - public DateTime LastLogonTimestamp { get; set; } + public X509Certificate2Collection CACertificate { get; set; } + public string Class { get; set; } + public string Description { get; set; } + public string DistinguishedName { get; set; } + public string DisplayName { get; set; } + public string DnsRoot { get; set; } + public string DNSHostName { get; set; } + public string DSHeuristics { get; set; } + public int DSMachineAccountQuota { get; set; } + public int Flags { get; set; } + public string fSMORoleOwner { get; set; } + public string GPLink { get; set; } + public string GPCFileSysPath { get; set; } + public DateTime LastLogonTimestamp { get; set; } public string lDAPDisplayName { get; set; } - public string Location { get; set; } - public string[] Member { get; set; } - public string[] MemberOf { get; set; } - public int msDSSupportedEncryptionTypes { get; set; } + public string Location { get; set; } + public string[] Member { get; set; } + public string[] MemberOf { get; set; } + public ActiveDirectorySecurity msDSAllowedToActOnBehalfOfOtherIdentity { get; set; } + public string[] msDSEnabledFeature { get; set; } + public int msDSSupportedEncryptionTypes { get; set; } public long msDSMinimumPasswordAge { get; set; } public long msDSMaximumPasswordAge { get; set; } public int msDSMinimumPasswordLength { get; set; } @@ -62,60 +66,63 @@ public class ReplPropertyMetaDataItem public long msDSLockoutObservationWindow { get; set; } public long msDSLockoutDuration { get; set; } public bool msDSPasswordReversibleEncryptionEnabled { get; set; } - public List msDSTrustForestTrustInfo { get; set; } - public string Name { get; set; } - public string NetBIOSName { get; set; } - public ActiveDirectorySecurity NTSecurityDescriptor { get; set; } - public SecurityIdentifier ObjectSid { get; set; } + public Dictionary msDSReplAttributeMetaData { get; set; } + public List msDSTrustForestTrustInfo { get; set; } + public string[] msiFileList { get; set; } + public string Name { get; set; } + public string NetBIOSName { get; set; } + public ActiveDirectorySecurity NTSecurityDescriptor { get; set; } + public SecurityIdentifier ObjectSid { get; set; } public int ObjectVersion { get; set; } - public string OperatingSystem { get; set; } - public int PrimaryGroupID { get; set; } - public DateTime PwdLastSet { get; set; } - public string SAMAccountName { get; set; } + public string OperatingSystem { get; set; } + public int PrimaryGroupID { get; set; } + public DateTime PwdLastSet { get; set; } + public string SAMAccountName { get; set; } + public Guid SchemaIDGUID { get; set; } public byte[] SchemaInfo { get; set; } - public string ScriptPath { get; set; } - public SecurityIdentifier SecurityIdentifier { get; set; } + public string ScriptPath { get; set; } + public SecurityIdentifier SecurityIdentifier { get; set; } public string[] ServicePrincipalName { get; set; } - public SecurityIdentifier[] SIDHistory { get; set; } - public string[] SiteObjectBL { get; set; } - public int TrustAttributes { get; set; } - public int TrustDirection { get; set; } - public string TrustPartner { get; set; } - public int TrustType { get; set; } - public int UserAccountControl { get; set; } - public DateTime WhenCreated { get; set; } - public DateTime WhenChanged { get; set; } - public Dictionary ReplPropertyMetaData { get; set; } + public SecurityIdentifier[] SIDHistory { get; set; } + public string[] SiteObjectBL { get; set; } + public int TrustAttributes { get; set; } + public int TrustDirection { get; set; } + public string TrustPartner { get; set; } + public int TrustType { get; set; } + public int UserAccountControl { get; set; } + public DateTime WhenCreated { get; set; } + public DateTime WhenChanged { get; set; } + public Dictionary ReplPropertyMetaData { get; set; } - private static string StripNamespace(string input) - { - int index = input.IndexOf(':'); - if (index >= 0) - { - return input.Substring(index + 1); - } - return input; - } + private static string StripNamespace(string input) + { + int index = input.IndexOf(':'); + if (index >= 0) + { + return input.Substring(index + 1); + } + return input; + } - private static string ExtractStringValue(XmlNode item) - { - XmlNode child = item.FirstChild; - if (child != null && item.FirstChild != null) - { - return child.InnerText; - } - return String.Empty; - } + private static string ExtractStringValue(XmlNode item) + { + XmlNode child = item.FirstChild; + if (child != null && item.FirstChild != null) + { + return child.InnerText; + } + return String.Empty; + } - private static int ExtractIntValue(XmlNode item) - { - XmlNode child = item.FirstChild; - if (child != null && item.FirstChild != null) - { - return int.Parse(child.InnerText); - } - return 0; - } + private static int ExtractIntValue(XmlNode item) + { + XmlNode child = item.FirstChild; + if (child != null && item.FirstChild != null) + { + return int.Parse(child.InnerText); + } + return 0; + } private static long ExtractLongValue(XmlNode item) { @@ -137,242 +144,294 @@ private static bool ExtractBoolValue(XmlNode item) return false; } - private static DateTime ExtractDateValue(XmlNode item) - { - XmlNode child = item.FirstChild; - if (child != null && item.FirstChild != null) - { + private static DateTime ExtractDateValue(XmlNode item) + { + XmlNode child = item.FirstChild; + if (child != null && item.FirstChild != null) + { return SafeExtractDateTimeFromLong(long.Parse(child.InnerText)); - } - return DateTime.MinValue; - } + } + return DateTime.MinValue; + } - private static ActiveDirectorySecurity ExtractSDValue(XmlNode child) - { - string value = ExtractStringValue(child); - byte[] data = Convert.FromBase64String(value); - ActiveDirectorySecurity sd = new ActiveDirectorySecurity(); - sd.SetSecurityDescriptorBinaryForm(data); - return sd; - } + private static ActiveDirectorySecurity ExtractSDValue(XmlNode child) + { + string value = ExtractStringValue(child); + byte[] data = Convert.FromBase64String(value); + ActiveDirectorySecurity sd = new ActiveDirectorySecurity(); + sd.SetSecurityDescriptorBinaryForm(data); + return sd; + } - private static SecurityIdentifier ExtractSIDValue(XmlNode child) - { - string value = ExtractStringValue(child); - byte[] data = Convert.FromBase64String(value); - return new SecurityIdentifier(data, 0); - } + private static SecurityIdentifier ExtractSIDValue(XmlNode child) + { + string value = ExtractStringValue(child); + byte[] data = Convert.FromBase64String(value); + return new SecurityIdentifier(data, 0); + } - // see https://msdn.microsoft.com/en-us/library/cc223786.aspx - private static List ConvertByteToTrustInfo(byte[] data) - { - List output = new List(); - Trace.WriteLine("Beginning to analyze a forestinfo data " + Convert.ToBase64String(data)); - int version = BitConverter.ToInt32(data, 0); - if (version != 1) - { - Trace.WriteLine("trust info version incompatible : " + version); - return output; - } - int recordcount = BitConverter.ToInt32(data, 4); - Trace.WriteLine("Number of records to analyze: " + recordcount); - int pointer = 8; - for (int i = 0; i < recordcount; i++) - { - int recordSize = 17; - int recordLen = BitConverter.ToInt32(data, pointer); - byte recordType = data[pointer + 16]; + // see https://msdn.microsoft.com/en-us/library/cc223786.aspx + private static List ConvertByteToTrustInfo(byte[] data) + { + List output = new List(); + Trace.WriteLine("Beginning to analyze a forestinfo data " + Convert.ToBase64String(data)); + int version = BitConverter.ToInt32(data, 0); + if (version != 1) + { + Trace.WriteLine("trust info version incompatible : " + version); + return output; + } + int recordcount = BitConverter.ToInt32(data, 4); + Trace.WriteLine("Number of records to analyze: " + recordcount); + int pointer = 8; + for (int i = 0; i < recordcount; i++) + { + int recordSize = 17; + int recordLen = BitConverter.ToInt32(data, pointer); + byte recordType = data[pointer + 16]; DateTime dt = SafeExtractDateTimeFromLong((((long)BitConverter.ToInt32(data, pointer + 8)) << 32) + BitConverter.ToInt32(data, pointer + 12)); - if (recordType == 0 || recordType == 1) - { - int nameLen = BitConverter.ToInt32(data, pointer + recordSize); - string name = UnicodeEncoding.UTF8.GetString(data, pointer + recordSize + 4, nameLen); - Trace.WriteLine("RecordType 0 or 1: name=" + name); - } - else if (recordType == 2) - { - Trace.WriteLine("RecordType 2"); - int tempPointer = pointer + recordSize; - int sidLen = BitConverter.ToInt32(data, tempPointer); - tempPointer += 4; - SecurityIdentifier sid = new SecurityIdentifier(data, tempPointer); - tempPointer += sidLen; - int DnsNameLen = BitConverter.ToInt32(data, tempPointer); - tempPointer += 4; - string DnsName = UnicodeEncoding.UTF8.GetString(data, tempPointer, DnsNameLen); - tempPointer += DnsNameLen; - int NetbiosNameLen = BitConverter.ToInt32(data, tempPointer); - tempPointer += 4; - string NetbiosName = UnicodeEncoding.UTF8.GetString(data, tempPointer, NetbiosNameLen); - tempPointer += NetbiosNameLen; + if (recordType == 0 || recordType == 1) + { + int nameLen = BitConverter.ToInt32(data, pointer + recordSize); + string name = UnicodeEncoding.UTF8.GetString(data, pointer + recordSize + 4, nameLen); + Trace.WriteLine("RecordType 0 or 1: name=" + name); + } + else if (recordType == 2) + { + Trace.WriteLine("RecordType 2"); + int tempPointer = pointer + recordSize; + int sidLen = BitConverter.ToInt32(data, tempPointer); + tempPointer += 4; + SecurityIdentifier sid = new SecurityIdentifier(data, tempPointer); + tempPointer += sidLen; + int DnsNameLen = BitConverter.ToInt32(data, tempPointer); + tempPointer += 4; + string DnsName = UnicodeEncoding.UTF8.GetString(data, tempPointer, DnsNameLen); + tempPointer += DnsNameLen; + int NetbiosNameLen = BitConverter.ToInt32(data, tempPointer); + tempPointer += 4; + string NetbiosName = UnicodeEncoding.UTF8.GetString(data, tempPointer, NetbiosNameLen); + tempPointer += NetbiosNameLen; - HealthCheckTrustDomainInfoData domaininfoc = new HealthCheckTrustDomainInfoData(); - domaininfoc.CreationDate = dt; - domaininfoc.DnsName = DnsName.ToLowerInvariant(); - domaininfoc.NetbiosName = NetbiosName; - domaininfoc.Sid = sid.Value; - output.Add(domaininfoc); - } - pointer += 4 + recordLen; - } - return output; - } + HealthCheckTrustDomainInfoData domaininfoc = new HealthCheckTrustDomainInfoData(); + domaininfoc.CreationDate = dt; + domaininfoc.DnsName = DnsName.ToLowerInvariant(); + domaininfoc.NetbiosName = NetbiosName; + domaininfoc.Sid = sid.Value; + output.Add(domaininfoc); + } + pointer += 4 + recordLen; + } + return output; + } - private static Dictionary ConvertByteToMetaDataInfo(byte[] data) - { - var output = new Dictionary(); - //Trace.WriteLine("Beginning to analyze a replpropertymetadata data " + Convert.ToBase64String(data)); - int version = BitConverter.ToInt32(data, 0); - if (version != 1) - { - Trace.WriteLine("trust info version incompatible : " + version); - return output; - } - int recordcount = BitConverter.ToInt32(data, 8); - //Trace.WriteLine("Number of records to analyze: " + recordcount); - int pointer = 16; - for (int i = 0; i < recordcount; i++) - { - var item = new ReplPropertyMetaDataItem(); - item.AttrType = BitConverter.ToInt32(data, pointer); - item.Version = BitConverter.ToInt32(data, pointer + 4); + private static Dictionary ConvertByteToMetaDataInfo(byte[] data) + { + var output = new Dictionary(); + //Trace.WriteLine("Beginning to analyze a replpropertymetadata data " + Convert.ToBase64String(data)); + int version = BitConverter.ToInt32(data, 0); + if (version != 1) + { + Trace.WriteLine("trust info version incompatible : " + version); + return output; + } + int recordcount = BitConverter.ToInt32(data, 8); + //Trace.WriteLine("Number of records to analyze: " + recordcount); + int pointer = 16; + for (int i = 0; i < recordcount; i++) + { + var item = new ReplPropertyMetaDataItem(); + item.AttrType = BitConverter.ToInt32(data, pointer); + item.Version = BitConverter.ToInt32(data, pointer + 4); long filetime = BitConverter.ToInt64(data, pointer + 8) * 10000000; - item.LastOriginatingChange = DateTime.FromFileTime(filetime); + item.LastOriginatingChange = DateTime.FromFileTime(filetime); byte[] guid = new byte[16]; Array.Copy(data, pointer + 16, guid, 0, 16); item.LastOriginatingDsaInvocationID = new Guid(guid); - item.UsnOriginatingChange = BitConverter.ToInt64(data, pointer + 32); - item.UsnLocalChange = BitConverter.ToInt64(data, pointer + 40); - pointer += 48; - output[item.AttrType] = item; - } - return output; - } + item.UsnOriginatingChange = BitConverter.ToInt64(data, pointer + 32); + item.UsnLocalChange = BitConverter.ToInt64(data, pointer + 40); + pointer += 48; + output[item.AttrType] = item; + } + return output; + } - private static List ExtractTrustForestInfo(XmlNode child) - { - string value = ExtractStringValue(child); - return ConvertByteToTrustInfo(Convert.FromBase64String(value)); - } + private static Dictionary ConvertStringArrayToMetaDataInfo(IEnumerable data) + { + var output = new Dictionary(); + foreach (var xml in data) + { + var metaData = new ReplPropertyMetaDataItem(); + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml.Replace('\0',' ')); + foreach (XmlNode child in doc.DocumentElement.ChildNodes) + { + switch (child.Name) + { + case "pszAttributeName": + metaData.AttrName = child.InnerText; + break; + case "dwVersion": + metaData.Version = int.Parse(child.InnerText); + break; + case "ftimeLastOriginatingChange": + metaData.LastOriginatingChange = DateTime.Parse(child.InnerText); + break; + case "uuidLastOriginatingDsaInvocationID": + metaData.LastOriginatingDsaInvocationID = new Guid(child.InnerText); + break; + case "usnOriginatingChange": + metaData.UsnOriginatingChange = long.Parse(child.InnerText); + break; + case "usnLocalChange": + metaData.UsnLocalChange = long.Parse(child.InnerText); + break; + case "pszLastOriginatingDsaDN": + //metaData.LastOriginatingDsaInvocationID = child.InnerText; + break; + } + } + if (!String.IsNullOrEmpty(metaData.AttrName)) + { + output[metaData.AttrName] = metaData; + } + } + return output; + } - private static Dictionary ExtractReplPropertyMetadata(XmlNode child) - { - string value = ExtractStringValue(child); - return ConvertByteToMetaDataInfo(Convert.FromBase64String(value)); - } + private static List ExtractTrustForestInfo(XmlNode child) + { + string value = ExtractStringValue(child); + return ConvertByteToTrustInfo(Convert.FromBase64String(value)); + } - private static string[] ExtractStringArrayValue(XmlNode item) - { - XmlNode child = item.FirstChild; - List list = new List(); - while (child != null) - { - list.Add(child.InnerText); - child = child.NextSibling; - } - return list.ToArray(); - } + private static Dictionary ExtractReplPropertyMetadata(XmlNode child) + { + string value = ExtractStringValue(child); + return ConvertByteToMetaDataInfo(Convert.FromBase64String(value)); + } + + private static string[] ExtractStringArrayValue(XmlNode item) + { + XmlNode child = item.FirstChild; + List list = new List(); + while (child != null) + { + list.Add(child.InnerText); + child = child.NextSibling; + } + return list.ToArray(); + } - private static X509Certificate2Collection ExtractCertificateStore(XmlNode item) - { - XmlNode child = item.FirstChild; - X509Certificate2Collection store = new X509Certificate2Collection(); - while (child != null) - { - store.Add(new X509Certificate2(Convert.FromBase64String(child.InnerText))); - child = child.NextSibling; - } - return store; - } + private static X509Certificate2Collection ExtractCertificateStore(XmlNode item) + { + XmlNode child = item.FirstChild; + X509Certificate2Collection store = new X509Certificate2Collection(); + while (child != null) + { + store.Add(new X509Certificate2(Convert.FromBase64String(child.InnerText))); + child = child.NextSibling; + } + return store; + } - private static SecurityIdentifier[] ExtractSIDArrayValue(XmlNode item) - { - XmlNode child = item.FirstChild; - List list = new List(); - while (child != null) - { - byte[] data = Convert.FromBase64String(child.InnerText); - list.Add(new SecurityIdentifier(data, 0)); - child = child.NextSibling; - } - return list.ToArray(); - } + private static SecurityIdentifier[] ExtractSIDArrayValue(XmlNode item) + { + XmlNode child = item.FirstChild; + List list = new List(); + while (child != null) + { + byte[] data = Convert.FromBase64String(child.InnerText); + list.Add(new SecurityIdentifier(data, 0)); + child = child.NextSibling; + } + return list.ToArray(); + } - public static ADItem Create(XmlElement item) - { - ADItem aditem = new ADItem(); - aditem.Class = StripNamespace(item.Name).ToLowerInvariant(); - XmlNode child = item.FirstChild; + public static ADItem Create(XmlElement item) + { + ADItem aditem = new ADItem(); + aditem.Class = StripNamespace(item.Name).ToLowerInvariant(); + XmlNode child = item.FirstChild; - while (child != null && child is XmlElement) - { - string name = StripNamespace(child.Name); - switch(name) - { - case "adminCount": - aditem.AdminCount = ExtractIntValue(child); - break; + while (child != null && child is XmlElement) + { + string name = StripNamespace(child.Name); + switch(name) + { + case "adminCount": + aditem.AdminCount = ExtractIntValue(child); + break; case "attributeID": aditem.AttributeID = ExtractStringValue(child); break; - case "cACertificate": - aditem.CACertificate = ExtractCertificateStore(child); - break; - case "description": - aditem.Description = ExtractStringValue(child); - break; - case "displayName": - aditem.DisplayName = ExtractStringValue(child); - break; - case "distinguishedName": - aditem.DistinguishedName = ExtractStringValue(child); - break; - case "dNSHostName": - aditem.DNSHostName = ExtractStringValue(child); - break; - case "dnsRoot": - aditem.DnsRoot = ExtractStringValue(child).ToLowerInvariant(); - break; - case "dSHeuristics": - aditem.DSHeuristics = ExtractStringValue(child); - break; - case "flags": - aditem.Flags = ExtractIntValue(child); - break; - case "gPCFileSysPath": - aditem.GPCFileSysPath = ExtractStringValue(child); - break; - case "gPLink": - aditem.GPLink = ExtractStringValue(child); - break; - case "lastLogonTimestamp": - aditem.LastLogonTimestamp = ExtractDateValue(child); - break; + case "cACertificate": + aditem.CACertificate = ExtractCertificateStore(child); + break; + case "description": + aditem.Description = ExtractStringValue(child); + break; + case "displayName": + aditem.DisplayName = ExtractStringValue(child); + break; + case "distinguishedName": + aditem.DistinguishedName = ExtractStringValue(child); + break; + case "dNSHostName": + aditem.DNSHostName = ExtractStringValue(child); + break; + case "dnsRoot": + aditem.DnsRoot = ExtractStringValue(child).ToLowerInvariant(); + break; + case "dSHeuristics": + aditem.DSHeuristics = ExtractStringValue(child); + break; + case "flags": + aditem.Flags = ExtractIntValue(child); + break; + case "fSMORoleOwner": + aditem.fSMORoleOwner = ExtractStringValue(child); + break; + case "gPCFileSysPath": + aditem.GPCFileSysPath = ExtractStringValue(child); + break; + case "gPLink": + aditem.GPLink = ExtractStringValue(child); + break; + case "lastLogonTimestamp": + aditem.LastLogonTimestamp = ExtractDateValue(child); + break; case "lDAPDisplayName": aditem.lDAPDisplayName = ExtractStringValue(child); break; - case "location": - aditem.Location = ExtractStringValue(child); - break; - case "memberOf": - aditem.MemberOf = ExtractStringArrayValue(child); - break; - case "member": - aditem.Member = ExtractStringArrayValue(child); - break; - case "name": - aditem.Name = ExtractStringValue(child); - break; - case "ms-DS-MachineAccountQuota": - aditem.DSMachineAccountQuota = ExtractIntValue(child); - break; - case "msDS-SupportedEncryptionTypes": - aditem.msDSSupportedEncryptionTypes = ExtractIntValue(child); - break; - case "msDS-TrustForestTrustInfo": - aditem.msDSTrustForestTrustInfo = ExtractTrustForestInfo(child); - break; + case "location": + aditem.Location = ExtractStringValue(child); + break; + case "memberOf": + aditem.MemberOf = ExtractStringArrayValue(child); + break; + case "member": + aditem.Member = ExtractStringArrayValue(child); + break; + case "name": + aditem.Name = ExtractStringValue(child); + break; + case "msDS-AllowedToActOnBehalfOfOtherIdentity": + aditem.msDSAllowedToActOnBehalfOfOtherIdentity = ExtractSDValue(child); + break; + case "msDS-EnabledFeature": + aditem.msDSEnabledFeature = ExtractStringArrayValue(child); + break; + case "ms-DS-MachineAccountQuota": + aditem.DSMachineAccountQuota = ExtractIntValue(child); + break; + case "msDS-SupportedEncryptionTypes": + aditem.msDSSupportedEncryptionTypes = ExtractIntValue(child); + break; + case "msDS-TrustForestTrustInfo": + aditem.msDSTrustForestTrustInfo = ExtractTrustForestInfo(child); + break; case "msDS-MinimumPasswordAge": aditem.msDSMinimumPasswordAge = ExtractLongValue(child); break; @@ -400,77 +459,86 @@ public static ADItem Create(XmlElement item) case "msDS-PasswordReversibleEncryptionEnabled": aditem.msDSPasswordReversibleEncryptionEnabled = ExtractBoolValue(child); break; - case "nETBIOSName": - aditem.NetBIOSName = ExtractStringValue(child); - break; - case "nTSecurityDescriptor": - aditem.NTSecurityDescriptor = ExtractSDValue(child); - break; - case "objectSid": - aditem.ObjectSid = ExtractSIDValue(child); - break; + case "msDS-ReplAttributeMetaData": + aditem.msDSReplAttributeMetaData=ConvertStringArrayToMetaDataInfo(ExtractStringArrayValue(child)); + break; + case "msiFileList": + aditem.msiFileList = ExtractStringArrayValue(child); + break; + case "nETBIOSName": + aditem.NetBIOSName = ExtractStringValue(child); + break; + case "nTSecurityDescriptor": + aditem.NTSecurityDescriptor = ExtractSDValue(child); + break; + case "objectSid": + aditem.ObjectSid = ExtractSIDValue(child); + break; case "objectVersion": aditem.ObjectVersion = ExtractIntValue(child); break; - case "operatingSystem": - aditem.OperatingSystem = ExtractStringValue(child); - break; - case "primaryGroupID": - aditem.PrimaryGroupID = ExtractIntValue(child); - break; - case "pwdLastSet": - aditem.PwdLastSet = ExtractDateValue(child); - break; - case "replPropertyMetaData": - aditem.ReplPropertyMetaData = ExtractReplPropertyMetadata(child); - break; - case "sAMAccountName": - aditem.SAMAccountName = ExtractStringValue(child); - break; + case "operatingSystem": + aditem.OperatingSystem = ExtractStringValue(child); + break; + case "primaryGroupID": + aditem.PrimaryGroupID = ExtractIntValue(child); + break; + case "pwdLastSet": + aditem.PwdLastSet = ExtractDateValue(child); + break; + case "replPropertyMetaData": + aditem.ReplPropertyMetaData = ExtractReplPropertyMetadata(child); + break; + case "sAMAccountName": + aditem.SAMAccountName = ExtractStringValue(child); + break; + case "schemaIDGUID": + aditem.SchemaIDGUID = new Guid(Convert.FromBase64String(ExtractStringValue(child))); + break; case "schemaInfo": aditem.SchemaInfo = Convert.FromBase64String(ExtractStringValue(child)); break; - case "scriptPath": - aditem.ScriptPath = ExtractStringValue(child); - break; - case "securityIdentifier": - aditem.SecurityIdentifier = ExtractSIDValue(child); - break; + case "scriptPath": + aditem.ScriptPath = ExtractStringValue(child); + break; + case "securityIdentifier": + aditem.SecurityIdentifier = ExtractSIDValue(child); + break; case "servicePrincipalName": aditem.ServicePrincipalName = ExtractStringArrayValue(child); break; - case "sIDHistory": - aditem.SIDHistory = ExtractSIDArrayValue(child); - break; - case "siteObjectBL": - aditem.SiteObjectBL = ExtractStringArrayValue(child); - break; - case "trustAttributes": - aditem.TrustAttributes = ExtractIntValue(child); - break; - case "trustDirection": - aditem.TrustDirection = ExtractIntValue(child); - break; - case "trustPartner": - aditem.TrustPartner = ExtractStringValue(child).ToLowerInvariant(); - break; - case "trustType": - aditem.TrustType = ExtractIntValue(child); - break; - case "userAccountControl": - aditem.UserAccountControl = ExtractIntValue(child); - break; - case "whenCreated": - aditem.WhenCreated = DateTime.ParseExact(ExtractStringValue(child), "yyyyMMddHHmmss.f'Z'", CultureInfo.InvariantCulture); - break; - case "whenChanged": - aditem.WhenChanged = DateTime.ParseExact(ExtractStringValue(child), "yyyyMMddHHmmss.f'Z'", CultureInfo.InvariantCulture); - break; - } - child = child.NextSibling; - } - return aditem; - } + case "sIDHistory": + aditem.SIDHistory = ExtractSIDArrayValue(child); + break; + case "siteObjectBL": + aditem.SiteObjectBL = ExtractStringArrayValue(child); + break; + case "trustAttributes": + aditem.TrustAttributes = ExtractIntValue(child); + break; + case "trustDirection": + aditem.TrustDirection = ExtractIntValue(child); + break; + case "trustPartner": + aditem.TrustPartner = ExtractStringValue(child).ToLowerInvariant(); + break; + case "trustType": + aditem.TrustType = ExtractIntValue(child); + break; + case "userAccountControl": + aditem.UserAccountControl = ExtractIntValue(child); + break; + case "whenCreated": + aditem.WhenCreated = DateTime.ParseExact(ExtractStringValue(child), "yyyyMMddHHmmss.f'Z'", CultureInfo.InvariantCulture); + break; + case "whenChanged": + aditem.WhenChanged = DateTime.ParseExact(ExtractStringValue(child), "yyyyMMddHHmmss.f'Z'", CultureInfo.InvariantCulture); + break; + } + child = child.NextSibling; + } + return aditem; + } // the AD is supposed to store Filetime as long. // Samba can return an out of range value @@ -486,104 +554,123 @@ private static DateTime SafeExtractDateTimeFromLong(long value) } } - [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] - public static ADItem Create(SearchResult sr, bool nTSecurityDescriptor) - { - ADItem aditem = new ADItem(); - // note: nTSecurityDescriptor is not present in the property except when run under admin (because allowed to read it) - // this workaround is here when running under lower permission - if (nTSecurityDescriptor) - { - aditem.NTSecurityDescriptor = sr.GetDirectoryEntry().ObjectSecurity; - } - foreach (string name in sr.Properties.PropertyNames) - { - switch (name) - { - case "admincount": - aditem.AdminCount = (int) sr.Properties[name][0]; - break; + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public static ADItem Create(SearchResult sr, bool nTSecurityDescriptor) + { + ADItem aditem = new ADItem(); + // note: nTSecurityDescriptor is not present in the property except when run under admin (because allowed to read it) + // this workaround is here when running under lower permission + var directoryEntry = sr.GetDirectoryEntry(); + if (nTSecurityDescriptor) + { + aditem.NTSecurityDescriptor = directoryEntry.ObjectSecurity; + } + aditem.Class = directoryEntry.SchemaClassName; + foreach (string name in sr.Properties.PropertyNames) + { + switch (name) + { + case "admincount": + aditem.AdminCount = (int) sr.Properties[name][0]; + break; case "attributeid": aditem.AttributeID = sr.Properties[name][0] as string; - break; - case "adspath": - break; - case "cacertificate": - X509Certificate2Collection store = new X509Certificate2Collection(); - foreach( byte[] data in sr.Properties["cACertificate"]) - { - store.Add(new X509Certificate2(data)); - } - aditem.CACertificate = store; - break; - case "description": - aditem.Description = sr.Properties[name][0] as string; - break; - case "displayname": - aditem.DisplayName = sr.Properties[name][0] as string; - break; - case "distinguishedname": - aditem.DistinguishedName = sr.Properties[name][0] as string; - break; - case "dnshostname": - aditem.DNSHostName = sr.Properties[name][0] as string; - break; - case "dnsroot": - aditem.DnsRoot = (sr.Properties[name][0] as string).ToLowerInvariant(); - break; - case "dsheuristics": - aditem.DSHeuristics = sr.Properties[name][0] as string; - break; - case "flags": - aditem.Flags = (int)sr.Properties[name][0]; - break; - case "gpcfilesyspath": - aditem.GPCFileSysPath = sr.Properties[name][0] as string; - break; - case "gplink": - aditem.GPLink = sr.Properties[name][0] as string; - break; - case "lastlogontimestamp": + break; + case "adspath": + break; + case "cacertificate": + X509Certificate2Collection store = new X509Certificate2Collection(); + foreach( byte[] data in sr.Properties["cACertificate"]) + { + store.Add(new X509Certificate2(data)); + } + aditem.CACertificate = store; + break; + case "description": + aditem.Description = sr.Properties[name][0] as string; + break; + case "displayname": + aditem.DisplayName = sr.Properties[name][0] as string; + break; + case "distinguishedname": + aditem.DistinguishedName = sr.Properties[name][0] as string; + break; + case "dnshostname": + aditem.DNSHostName = sr.Properties[name][0] as string; + break; + case "dnsroot": + aditem.DnsRoot = (sr.Properties[name][0] as string).ToLowerInvariant(); + break; + case "dsheuristics": + aditem.DSHeuristics = sr.Properties[name][0] as string; + break; + case "flags": + aditem.Flags = (int)sr.Properties[name][0]; + break; + case "fsmoroleowner": + aditem.fSMORoleOwner = sr.Properties[name][0] as string; + break; + case "gpcfilesyspath": + aditem.GPCFileSysPath = sr.Properties[name][0] as string; + break; + case "gplink": + aditem.GPLink = sr.Properties[name][0] as string; + break; + case "lastlogontimestamp": aditem.LastLogonTimestamp = SafeExtractDateTimeFromLong((long)sr.Properties[name][0]); - break; + break; case "ldapdisplayname": aditem.lDAPDisplayName = sr.Properties[name][0] as string; break; - case "location": - aditem.Location = sr.Properties[name][0] as string; - break; - case "memberof": - { - List list = new List(); - foreach (string data in sr.Properties[name]) - { - list.Add(data); - } - aditem.MemberOf = list.ToArray(); - } - break; - case "member": - { - List list = new List(); - foreach (string data in sr.Properties[name]) - { - list.Add(data); - } - aditem.Member = list.ToArray(); - } - break; - case "name": - aditem.Name = sr.Properties[name][0] as string; - break; - case "ms-ds-machineaccountquota": - aditem.DSMachineAccountQuota = (int)sr.Properties[name][0]; - break; - case "msds-supportedencryptiontypes": - aditem.msDSSupportedEncryptionTypes = (int)sr.Properties[name][0]; - break; - case "msds-trustforesttrustinfo": - aditem.msDSTrustForestTrustInfo = ConvertByteToTrustInfo((byte[])sr.Properties[name][0]); - break; + case "location": + aditem.Location = sr.Properties[name][0] as string; + break; + case "memberof": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.MemberOf = list.ToArray(); + } + break; + case "member": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.Member = list.ToArray(); + } + break; + case "name": + aditem.Name = sr.Properties[name][0] as string; + break; + case "msds-allowedtoactonbehalfofotheridentity": + aditem.msDSAllowedToActOnBehalfOfOtherIdentity = new ActiveDirectorySecurity(); + aditem.msDSAllowedToActOnBehalfOfOtherIdentity.SetSecurityDescriptorBinaryForm((byte[])sr.Properties[name][0], AccessControlSections.Access); + break; + case "msds-enabledfeature": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.msDSEnabledFeature = list.ToArray(); + } + break; + case "ms-ds-machineaccountquota": + aditem.DSMachineAccountQuota = (int)sr.Properties[name][0]; + break; + case "msds-supportedencryptiontypes": + aditem.msDSSupportedEncryptionTypes = (int)sr.Properties[name][0]; + break; + case "msds-trustforesttrustinfo": + aditem.msDSTrustForestTrustInfo = ConvertByteToTrustInfo((byte[])sr.Properties[name][0]); + break; case "msds-minimumpasswordage": aditem.msDSMinimumPasswordAge = (long)sr.Properties[name][0]; break; @@ -611,45 +698,65 @@ public static ADItem Create(SearchResult sr, bool nTSecurityDescriptor) case "msds-passwordreversibleencryptionenabled": aditem.msDSPasswordReversibleEncryptionEnabled = (bool)sr.Properties[name][0]; break; - case "netbiosname": - aditem.NetBIOSName = sr.Properties[name][0] as string; - break; - case "ntsecuritydescriptor": - // ignored - break; - case "objectclass": - aditem.Class = sr.Properties[name][sr.Properties[name].Count-1] as string; - break; - case "objectsid": - aditem.ObjectSid = new SecurityIdentifier((byte[])sr.Properties[name][0],0); - break; + case "msds-replattributemetadata": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.msDSReplAttributeMetaData = ConvertStringArrayToMetaDataInfo(list); + } + break; + case "msifilelist": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.msiFileList = list.ToArray(); + } + break; + case "netbiosname": + aditem.NetBIOSName = sr.Properties[name][0] as string; + break; + case "ntsecuritydescriptor": + // ignored + break; + case "objectsid": + aditem.ObjectSid = new SecurityIdentifier((byte[])sr.Properties[name][0],0); + break; case "objectversion": aditem.ObjectVersion = (int)sr.Properties[name][0]; break; - case "operatingsystem": - aditem.OperatingSystem = sr.Properties[name][0] as string; - break; - case "primarygroupid": - aditem.PrimaryGroupID = (int)sr.Properties[name][0]; - break; - case "pwdlastset": + case "operatingsystem": + aditem.OperatingSystem = sr.Properties[name][0] as string; + break; + case "primarygroupid": + aditem.PrimaryGroupID = (int)sr.Properties[name][0]; + break; + case "pwdlastset": aditem.PwdLastSet = SafeExtractDateTimeFromLong((long)sr.Properties[name][0]); - break; + break; case "replpropertymetadata": - aditem.ReplPropertyMetaData = ConvertByteToMetaDataInfo((byte[])sr.Properties[name][0]); - break; - case "samaccountname": - aditem.SAMAccountName = sr.Properties[name][0] as string; - break; + aditem.ReplPropertyMetaData = ConvertByteToMetaDataInfo((byte[])sr.Properties[name][0]); + break; + case "samaccountname": + aditem.SAMAccountName = sr.Properties[name][0] as string; + break; + case "schemaidguid": + aditem.SchemaIDGUID = new Guid((byte[])sr.Properties[name][0]); + break; case "schemainfo": aditem.SchemaInfo = (byte[])sr.Properties[name][0]; break; - case "scriptpath": - aditem.ScriptPath = sr.Properties[name][0] as string; - break; - case "securityidentifier": - aditem.SecurityIdentifier = new SecurityIdentifier((byte[])sr.Properties[name][0], 0); - break; + case "scriptpath": + aditem.ScriptPath = sr.Properties[name][0] as string; + break; + case "securityidentifier": + aditem.SecurityIdentifier = new SecurityIdentifier((byte[])sr.Properties[name][0], 0); + break; case "serviceprincipalname": { List list = new List(); @@ -660,53 +767,62 @@ public static ADItem Create(SearchResult sr, bool nTSecurityDescriptor) aditem.ServicePrincipalName = list.ToArray(); } break; - case "sidhistory": - { - List list = new List(); - foreach (byte[] data in sr.Properties[name]) - { - list.Add(new SecurityIdentifier(data, 0)); - } - aditem.SIDHistory = list.ToArray(); - } - break; - case "siteobjectbl": - { - List list = new List(); - foreach (string data in sr.Properties[name]) - { - list.Add(data); - } - aditem.SiteObjectBL = list.ToArray(); - } - break; - case "trustattributes": - aditem.TrustAttributes = (int)sr.Properties[name][0]; - break; - case "trustdirection": - aditem.TrustDirection = (int)sr.Properties[name][0]; - break; - case "trustpartner": - aditem.TrustPartner = ((string)sr.Properties[name][0]).ToLowerInvariant(); - break; - case "trusttype": - aditem.TrustType = (int)sr.Properties[name][0]; - break; - case "useraccountcontrol": - aditem.UserAccountControl = (int)sr.Properties[name][0]; - break; - case "whencreated": - aditem.WhenCreated = (DateTime)sr.Properties[name][0]; - break; - case "whenchanged": - aditem.WhenChanged = (DateTime)sr.Properties[name][0]; - break; - default: - Trace.WriteLine("Unknown attribute: " + name); - break; - } - } - return aditem; - } + case "sidhistory": + { + List list = new List(); + foreach (byte[] data in sr.Properties[name]) + { + list.Add(new SecurityIdentifier(data, 0)); + } + aditem.SIDHistory = list.ToArray(); + } + break; + case "siteobjectbl": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.SiteObjectBL = list.ToArray(); + } + break; + case "trustattributes": + aditem.TrustAttributes = (int)sr.Properties[name][0]; + break; + case "trustdirection": + aditem.TrustDirection = (int)sr.Properties[name][0]; + break; + case "trustpartner": + aditem.TrustPartner = ((string)sr.Properties[name][0]).ToLowerInvariant(); + break; + case "trusttype": + aditem.TrustType = (int)sr.Properties[name][0]; + break; + case "useraccountcontrol": + aditem.UserAccountControl = (int)sr.Properties[name][0]; + break; + case "whencreated": + aditem.WhenCreated = (DateTime)sr.Properties[name][0]; + break; + case "whenchanged": + aditem.WhenChanged = (DateTime)sr.Properties[name][0]; + break; + default: + Trace.WriteLine("Unknown attribute: " + name); + break; + } + } + if (String.IsNullOrEmpty(aditem.DistinguishedName)) + { + string path = (string) sr.Properties["adspath"][0]; + int i = path.IndexOf('/', 7); + if (i > 0) + { + aditem.DistinguishedName = path.Substring(i + 1); + } + } + return aditem; + } } } diff --git a/ADWS/ADWSConnection.cs b/ADWS/ADWSConnection.cs index a9b197f..d926cb1 100644 --- a/ADWS/ADWSConnection.cs +++ b/ADWS/ADWSConnection.cs @@ -363,7 +363,7 @@ XmlQualifiedName[] BuildProperties(List properties) private void EnumerateInternalWithADWS(string distinguishedName, string filter, string[] properties, string scope, ReceiveItems callback) { bool nTSecurityDescriptor = false; - List listproperties = new List(properties); + List listproperties = new List(); Enumerate enumerate = new Enumerate(); enumerate.Filter = new FilterType(); @@ -374,10 +374,14 @@ private void EnumerateInternalWithADWS(string distinguishedName, string filter, enumerate.Filter.LdapQuery.Scope = scope; enumerate.Filter.LdapQuery.Filter = filter; Trace.WriteLine("LdapQuery.Filter=" + enumerate.Filter.LdapQuery.Filter); - enumerate.Selection = new Selection(); - enumerate.Selection.SelectionProperty = BuildProperties(listproperties); + if (properties != null) + { + listproperties.AddRange(properties); + enumerate.Selection = new Selection(); + enumerate.Selection.SelectionProperty = BuildProperties(listproperties); + } EnumerateResponse enumerateResponse = null; Trace.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] Running enumeration"); diff --git a/ADWS/ADWebService.cs b/ADWS/ADWebService.cs index 861e155..b8cb9c0 100644 --- a/ADWS/ADWebService.cs +++ b/ADWS/ADWebService.cs @@ -13,6 +13,7 @@ using System.DirectoryServices.ActiveDirectory; using System.IO; using System.Net; +using System.Runtime.InteropServices; using System.Security.Permissions; using System.ServiceModel; using System.ServiceModel.Channels; @@ -35,7 +36,7 @@ public enum ADConnectionType LDAPThenADWS = 3, } - internal class ADWebService : IDisposable + internal class ADWebService : IDisposable, IADConnection { public ADWebService(string server, int port, NetworkCredential credential) @@ -105,7 +106,12 @@ private void EstablishConnection() } catch(Exception ex2) { - Trace.WriteLine("LDAP exception: " + ex2.Message); + Trace.WriteLine("LDAP exception: " + ex2.Message + "(" + ex2.GetType() + ")"); + if (ex2 as COMException != null) + { + COMException ex3 = (COMException)ex2; + Trace.WriteLine("COMException: " + ex3.ErrorCode); + } Trace.WriteLine(ex2.StackTrace); Trace.WriteLine("Throwing ADWS Exception again"); throw new ActiveDirectoryServerDownException(ex.Message); @@ -132,7 +138,7 @@ private void EstablishConnection() } catch (Exception ex2) { - Trace.WriteLine("ADWS exception: " + ex2.Message); + Trace.WriteLine("ADWS exception: " + ex2.Message + "(" + ex2.GetType() + ")"); Trace.WriteLine(ex2.StackTrace); Trace.WriteLine("Throwing LDAP Exception again"); throw new ActiveDirectoryServerDownException(ex.Message); @@ -164,6 +170,11 @@ public ADDomainInfo DomainInfo } } + public ADDomainInfo GetDomainInfo() + { + return DomainInfo; + } + public class OUExploration: IComparable { public string OU { get; set; } diff --git a/ADWS/LDAPConnection.cs b/ADWS/LDAPConnection.cs index 2fc1283..674c571 100644 --- a/ADWS/LDAPConnection.cs +++ b/ADWS/LDAPConnection.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.DirectoryServices; using System.Net; +using System.Runtime.InteropServices; using System.Security.Permissions; using System.Text; @@ -65,13 +66,16 @@ private void EnumerateInternalWithLDAP(string distinguishedName, string filter, } bool nTSecurityDescriptor = false; - foreach (string property in properties) + if (properties != null) { - clsDS.PropertiesToLoad.Add(property); - // prepare the flag for the ntsecuritydescriptor - if (String.Compare("nTSecurityDescriptor", property, true) == 0) + foreach (string property in properties) { - nTSecurityDescriptor = true; + clsDS.PropertiesToLoad.Add(property); + // prepare the flag for the ntsecuritydescriptor + if (String.Compare("nTSecurityDescriptor", property, true) == 0) + { + nTSecurityDescriptor = true; + } } } Trace.WriteLine("[" + DateTime.Now.ToLongTimeString() + "]Calling FindAll"); @@ -119,14 +123,38 @@ protected override ADDomainInfo GetDomainInfoInternal() [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] private ADDomainInfo GetLDAPDomainInfo() { - DirectoryEntry rootDse = new DirectoryEntry("LDAP://" + Server + "/RootDSE"); - if (Credential == null) + DirectoryEntry rootDse; + try { - rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", null, null, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + if (Credential == null) + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", null, null, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } + else + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", Credential.UserName, Credential.Password, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } + // force the connection to the LDAP server via an access to the "properties" property + Trace.WriteLine("rootDse property count: " + rootDse.Properties.Count); } - else + catch (COMException ex) { - rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", Credential.UserName, Credential.Password, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + // Windows 2000 does not support a bind to the rootDse and returns "The server is not operational" (0x8007203A) + if (ex.ErrorCode == -2147016646) + { + if (Credential == null) + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", null, null, AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } + else + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", Credential.UserName, Credential.Password, AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } + } + else + { + throw; + } } return ADDomainInfo.Create(rootDse); } diff --git a/Compatibility.cs b/Compatibility.cs index c5b9569..484c288 100644 --- a/Compatibility.cs +++ b/Compatibility.cs @@ -42,4 +42,13 @@ public ContractNamespaceAttribute(string contractNamespace) this.contractNamespace = contractNamespace; } } + + // available in dotnet 3 but not on dotnet 2 which is needed for Windows 2000 + [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false, Inherited=false)] + internal sealed class IgnoreDataMemberAttribute : Attribute + { + public IgnoreDataMemberAttribute() + { + } + } } diff --git a/ConsoleMenu.cs b/ConsoleMenu.cs index 7f0dbfd..38f6923 100644 --- a/ConsoleMenu.cs +++ b/ConsoleMenu.cs @@ -7,6 +7,26 @@ namespace PingCastle { + + public class ConsoleMenuItem + { + public string Choice { get; set; } + public string ShortDescription { get; set; } + public string LongDescription { get; set; } + + public ConsoleMenuItem(string choice, string shortDescription) + : this(choice, shortDescription, null) + { + } + + public ConsoleMenuItem(string choice, string shortDescription, string longDescription) + { + Choice = choice; + ShortDescription = shortDescription; + LongDescription = longDescription; + } + } + public class ConsoleMenu { @@ -15,15 +35,17 @@ public class ConsoleMenu public static string Notice { get; set; } public static string Information { get; set; } - static void printSelectMenuStyle0(List> items, int currentIndex, int top, int left) + static void printSelectMenuStyle0(List items, int currentIndex, int top, int left) { bool hasDescription = false; + string description = null; int largerChoice = 0; + int maxDescription = 0; for (int i = 0; i < items.Count; i++) { - if (!String.IsNullOrEmpty(items[i].Value)) + if (!String.IsNullOrEmpty(items[i].ShortDescription)) hasDescription = true; - int l = items[i].Key.Length; + int l = items[i].Choice.Length; if (l > largerChoice) largerChoice = l; } @@ -34,15 +56,18 @@ static void printSelectMenuStyle0(List> items, int { Console.BackgroundColor = ConsoleColor.Gray; Console.ForegroundColor = ConsoleColor.Black; + description = items[i].LongDescription; } - Console.Write(" " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Key); + if (!String.IsNullOrEmpty(items[i].LongDescription) && maxDescription < items[i].LongDescription.Length) + maxDescription = items[i].LongDescription.Length; + Console.Write(" " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Choice); if (hasDescription) { - int diff = largerChoice - items[i].Key.Length; + int diff = largerChoice - items[i].Choice.Length; if (diff > 0) Console.Write(new String(' ', diff)); - if (!String.IsNullOrEmpty(items[i].Value)) - Console.Write("-" + items[i].Value); + if (!String.IsNullOrEmpty(items[i].ShortDescription)) + Console.Write("-" + items[i].ShortDescription); } Console.WriteLine(); Console.ResetColor(); @@ -54,9 +79,24 @@ static void printSelectMenuStyle0(List> items, int } Console.WriteLine(" 0-Exit"); Console.ResetColor(); + if (!String.IsNullOrEmpty(description)) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("=============================="); + Console.ResetColor(); + int currentLineCursor = Console.CursorTop; + Console.WriteLine(new string(' ', maxDescription)); + Console.SetCursorPosition(0, currentLineCursor); + Console.WriteLine(description); + } + else + { + Console.WriteLine(new string(' ', Console.WindowWidth - 1)); + Console.WriteLine(new string(' ', maxDescription)); + } } - static void printSelectMenuStyle1(List> items, int currentIndex, int top, int left) + static void printSelectMenuStyle1(List items, int currentIndex, int top, int left) { string description = null; Console.SetCursorPosition(left, top); @@ -68,12 +108,12 @@ static void printSelectMenuStyle1(List> items, int { Console.BackgroundColor = ConsoleColor.Gray; Console.ForegroundColor = ConsoleColor.Black; - description = items[i].Value; + description = items[i].ShortDescription; } - if (!String.IsNullOrEmpty(items[i].Value) && maxDescription < items[i].Value.Length) - maxDescription = items[i].Value.Length; + if (!String.IsNullOrEmpty(items[i].ShortDescription) && maxDescription < items[i].ShortDescription.Length) + maxDescription = items[i].ShortDescription.Length; - item = " " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Key; + item = " " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Choice; Console.SetCursorPosition(left + (i < (items.Count + 1) / 2 ? 0 : Console.WindowWidth / 2), top + i + (i < (items.Count + 1) / 2 ? 0 : -(items.Count + 1) / 2)); Console.Write(item + new string(' ',Console.WindowWidth / 2 - item.Length - 1)); Console.ResetColor(); @@ -92,7 +132,6 @@ static void printSelectMenuStyle1(List> items, int Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("=============================="); Console.ResetColor(); - Console.WriteLine("Description:"); int currentLineCursor = Console.CursorTop; Console.WriteLine(new string(' ', maxDescription)); Console.SetCursorPosition(0, currentLineCursor); @@ -155,21 +194,21 @@ public static List AskForListString() return list; } - public static int SelectMenu(List> items, int defaultIndex = 1) + public static int SelectMenu(List items, int defaultIndex = 1) { DisplayHeader(); ClearTopic(); return SelectMenu(items, defaultIndex, 0); } - public static int SelectMenuCompact(List> items, int defaultIndex = 1) + public static int SelectMenuCompact(List items, int defaultIndex = 1) { DisplayHeader(); ClearTopic(); return SelectMenu(items, defaultIndex, 1); } - protected static int SelectMenu(List> items, int defaultIndex = 1, int style = 0) + protected static int SelectMenu(List items, int defaultIndex = 1, int style = 0) { int top = Console.CursorTop; int left = Console.CursorLeft; diff --git a/Data/CompromiseGraphData.cs b/Data/CompromiseGraphData.cs index ec35993..6cef5bb 100644 --- a/Data/CompromiseGraphData.cs +++ b/Data/CompromiseGraphData.cs @@ -59,7 +59,7 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(DomainFQDN, DomainSid, DomainNetBIOS); + _domain = DomainKey.Create(DomainFQDN, DomainSid, DomainNetBIOS); } return _domain; } @@ -241,7 +241,7 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(FQDN, Sid, Netbios); + _domain = DomainKey.Create(FQDN, Sid, Netbios); } return _domain; } diff --git a/Data/DomainKey.cs b/Data/DomainKey.cs index f5cb904..209445d 100644 --- a/Data/DomainKey.cs +++ b/Data/DomainKey.cs @@ -13,7 +13,7 @@ namespace PingCastle.Data { - [DebuggerDisplay("{DomainName} {DomainSID}")] + [DebuggerDisplay("FQDN: {DomainName} SID: {DomainSID} NetBIOS: {DomainNetBIOS}")] public class DomainKey : IComparable, IEquatable { public string DomainName { get; set; } @@ -27,7 +27,17 @@ private DomainKey() static Regex sidRegex = new Regex(@"(^$|^S-\d-(\d+-){1,14}\d+$)"); - public DomainKey(string DnsName, string domainSid, string domainNetbios) + public static DomainKey Create(string DnsName, string domainSid, string domainNetbios) + { + var key = new DomainKey(DnsName, domainSid, domainNetbios); + if (key.DomainSID == null && key.DomainNetBIOS == key.DomainSID && key.DomainName == key.DomainNetBIOS) + { + return null; + } + return key; + } + + protected DomainKey(string DnsName, string domainSid, string domainNetbios) { if (!string.IsNullOrEmpty(DnsName)) diff --git a/Data/HealthcheckData.cs b/Data/HealthcheckData.cs index ad32af7..91af68b 100644 --- a/Data/HealthcheckData.cs +++ b/Data/HealthcheckData.cs @@ -89,6 +89,7 @@ public HealthCheckGroupData() } + [DebuggerDisplay("FQDN: {DnsName} SiD: {Sid} NetBIOS: {NetbiosName} Forest: FQDN: {ForestName} SID: {ForestSid} NetBIOS {ForestNetbios}")] public class HealthCheckTrustDomainInfoData { public string DnsName { get; set; } @@ -108,12 +109,17 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(DnsName, Sid, NetbiosName); + _domain = DomainKey.Create(DnsName, Sid, NetbiosName); } return _domain; } + set + { + _domain = value; + } } + private bool _forestSet = false; private DomainKey _forest; [IgnoreDataMember] [XmlIgnore] @@ -121,15 +127,22 @@ public DomainKey Forest { get { - if (_forest == null) + if (!_forestSet) { - if (String.Equals(DnsName, ForestName, StringComparison.InvariantCultureIgnoreCase)) - _forest = Domain; - else - _forest = new DomainKey(ForestName, ForestSid, ForestNetbios); + _forestSet = true; + if (String.Equals(DnsName, ForestName, StringComparison.InvariantCultureIgnoreCase)) + _forest = Domain; + else + { + _forest = DomainKey.Create(ForestName, ForestSid, ForestNetbios); + } } return _forest; } + set + { + _forest = value; + } } } @@ -167,7 +180,7 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(TrustPartner, SID, NetBiosName); + _domain = DomainKey.Create(TrustPartner, SID, NetBiosName); } return _domain; } @@ -189,6 +202,15 @@ public class GPPPassword public string GPOName { get; set; } } + [DebuggerDisplay("{GPOName} {FileName}")] + public class GPPFileDeployed + { + public string FileName { get; set; } + public string Type { get; set; } + public string GPOName { get; set; } + public List Delegation { get; set; } + } + [DebuggerDisplay("{Property} {Value}")] public class GPPSecurityPolicyProperty { @@ -541,7 +563,7 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(FriendlyName, DomainSid, NetBIOSName); + _domain = DomainKey.Create(FriendlyName, DomainSid, NetBIOSName); } return _domain; } @@ -589,6 +611,8 @@ public class HealthcheckDomainController public bool RemoteSpoolerDetected { get; set; } public List IP { get; set; } + + public List FSMO { get; set; } } [DebuggerDisplay("{SiteName}")] @@ -669,6 +693,8 @@ public PingCastleReportDataExportLevel Level public int ForestFunctionalLevel { get; set; } public int SchemaVersion { get; set; } public int SchemaInternalVersion { get; set; } + public bool IsRecycleBinEnabled { get; set; } + public DateTime SchemaLastChanged { get; set; } public int NumberOfDC { get; set; } public int GlobalScore { get; set; } @@ -694,6 +720,12 @@ public PingCastleReportDataExportLevel Level public bool ShouldSerializeDsHeuristicsAnonymousAccess() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public bool DsHeuristicsAnonymousAccess { get; set; } + public bool ShouldSerializeDsHeuristicsAdminSDExMaskModified() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } + public bool DsHeuristicsAdminSDExMaskModified { get; set; } + + public bool ShouldSerializeDsHeuristicsDoListObject() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } + public bool DsHeuristicsDoListObject { get; set; } + public bool ShouldSerializeRiskRules() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List RiskRules { get; set; } @@ -728,6 +760,9 @@ public PingCastleReportDataExportLevel Level public bool ShouldSerializeKrbtgtLastVersion() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public int KrbtgtLastVersion { get; set; } + public bool ShouldSerializeExchangePrivEscVulnerable() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } + public bool ExchangePrivEscVulnerable { get; set; } + public bool ShouldSerializeAdminLastLoginDate() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public DateTime AdminLastLoginDate { get; set; } @@ -737,6 +772,9 @@ public PingCastleReportDataExportLevel Level public bool ShouldSerializeGPPPassword() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPPPassword { get; set; } + public bool ShouldSerializeGPPFileDeployed() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } + public List GPPFileDeployed { get; set; } + public bool ShouldSerializeGPPRightAssignment() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List GPPRightAssignment { get; set; } @@ -806,7 +844,7 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(DomainFQDN, DomainSid, NetBIOSName); + _domain = DomainKey.Create(DomainFQDN, DomainSid, NetBIOSName); } return _domain; } @@ -844,7 +882,7 @@ public DomainKey Forest } } } - _forest = new DomainKey(ForestFQDN, sid, netbiosname); + _forest = DomainKey.Create(ForestFQDN, sid, netbiosname); } } return _forest; @@ -871,7 +909,8 @@ public IList DomainKnown foreach (var d in t.KnownDomains) { output.Add(d.Domain); - output.Add(d.Forest); + if (d.Forest != null) + output.Add(d.Forest); } } } @@ -881,7 +920,8 @@ public IList DomainKnown foreach (var d in ReachableDomains) { output.Add(d.Domain); - output.Add(d.Forest); + if (d.Forest != null) + output.Add(d.Forest); } } return output; diff --git a/Graph/Database/IDataStorage.cs b/Graph/Database/IDataStorage.cs index 76eed19..59cdc6a 100644 --- a/Graph/Database/IDataStorage.cs +++ b/Graph/Database/IDataStorage.cs @@ -28,18 +28,29 @@ public struct DataStorageDomainTrusts public interface IDataStorage { + // used to store various information about the domain in general (its FQDN, its SID, ...) + Dictionary GetDatabaseInformation(); + + // used to locate an item based on its name int SearchItem(string name); - Node RetrieveNode(int id); + // once, the ID located by the previous function, return the node + Node RetrieveNode(int id); + // generated lookup function Dictionary RetrieveNodes(List nodes); - Dictionary GetDatabaseInformation(); - List SearchRelations(List SourceIds, List knownIds); + // based on a node list, return all path to other nodes (at the exclusion of the knownId ones) + List SearchRelations(List SourceIds, List knownIds); + // create a node int InsertNode(string shortname, string objectclass, string name, string sid, ADItem adItem); - + // create a link between 2 nodes void InsertRelation(string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType); List GetKnownDomains(); - bool IsSIDAlreadyInserted(string sid); + // used to retrieve objects in queue and not examined + List GetCNToInvestigate(); + List GetSIDToInvestigate(); + List GetPrimaryGroupIDToInvestigate(); + List GetFilesToInvestigate(); } } diff --git a/Graph/Database/LiveDataStorage.cs b/Graph/Database/LiveDataStorage.cs index c3ce07e..d7a9366 100644 --- a/Graph/Database/LiveDataStorage.cs +++ b/Graph/Database/LiveDataStorage.cs @@ -24,10 +24,12 @@ public class LiveDataStorage: IDataStorage public List KnownCN = new List(); public List KnownSID = new List(); + public List KnownFiles = new List(); public List KnownPGId = new List() { 513, 515 }; public List CNToInvestigate { get; private set; } public List SIDToInvestigate { get; private set; } public List PGIdToInvestigate { get; private set; } + public List FilesToInvestigate { get; private set; } public List KnownDomains { get; private set; } private string serverForSIDResolution; @@ -51,6 +53,7 @@ public LiveDataStorage() SIDToInvestigate = new List(); CNToInvestigate = new List(); PGIdToInvestigate = new List(); + FilesToInvestigate = new List(); KnownDomains = new List(); } @@ -90,6 +93,14 @@ public List GetPrimaryGroupIDToInvestigate() return output; } + public List GetFilesToInvestigate() + { + List output = new List(); + output.AddRange(FilesToInvestigate); + FilesToInvestigate.Clear(); + return output; + } + public int InsertNode(string shortname, string objectclass, string name, string sid, ADItem adItem) { if (String.Equals(objectclass, "unknown", StringComparison.OrdinalIgnoreCase)) @@ -122,53 +133,60 @@ public int InsertNode(string shortname, string objectclass, string name, string adItem = null; } Node node = new Node(); - node.Id = index; node.Shortname = shortname; node.Type = objectclass; node.Dn = name; + node.Sid = sid; node.ADItem = adItem; - if (!String.IsNullOrEmpty(name)) - { - KnownCN.Add(name); - if (CNToInvestigate.Contains(name)) - CNToInvestigate.Remove(name); - } - node.Sid = sid; - nodes.Add(index, node); - if (!String.IsNullOrEmpty(sid)) - { - KnownSID.Add(sid); - if (SIDToInvestigate.Contains(sid)) - SIDToInvestigate.Remove(sid); - // handle primary group id - if (objectclass == "group") + + //12345 + lock (nodes) + { + Trace.WriteLine("Inserting node " + index + " name=" + node.Name + " sid=" + node.Sid + " shortname=" + node.Shortname); + node.Id = index; + nodes.Add(index, node); + if (!string.IsNullOrEmpty(name)) + { + if (name.StartsWith("\\\\")) + { + KnownFiles.Add(name); + if (FilesToInvestigate.Contains(name)) + FilesToInvestigate.Remove(name); + } + else + { + KnownCN.Add(name); + if (CNToInvestigate.Contains(name)) + CNToInvestigate.Remove(name); + } + } + if (!String.IsNullOrEmpty(sid)) { - if (sid.StartsWith("S-1-5-21-")) + KnownSID.Add(sid); + if (SIDToInvestigate.Contains(sid)) + SIDToInvestigate.Remove(sid); + // handle primary group id + if (objectclass == "group") { - var part = sid.Split('-'); - int PGId = int.Parse(part[part.Length - 1]); - if (!KnownPGId.Contains(PGId) && !PGIdToInvestigate.Contains(PGId)) + if (sid.StartsWith("S-1-5-21-")) { - PGIdToInvestigate.Add(PGId); + var part = sid.Split('-'); + int PGId = int.Parse(part[part.Length - 1]); + if (!KnownPGId.Contains(PGId) && !PGIdToInvestigate.Contains(PGId)) + { + PGIdToInvestigate.Add(PGId); + } } } } - } - return index++; - } - - public bool IsSIDAlreadyInserted(string sid) - { - if (KnownSID.Contains(sid)) - { - return true; + return index++; } - return false; - } + } public void InsertRelation(string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType) { - RelationOnHold relation = new RelationOnHold(); + Trace.WriteLine("Stack:" + mappingMaster + "," + typeMaster.ToString() + "," + mappingSlave + "," + typeSlave + "," + relationType.ToString()); + RelationOnHold relation = new RelationOnHold(); relation.mappingMaster = mappingMaster; relation.typeMaster = typeMaster; relation.mappingSlave = mappingSlave; @@ -182,24 +200,33 @@ public void InsertRelation(string mappingMaster, MappingType typeMaster, string void AddDataToInvestigate(string mapping, MappingType type) { // avoid dealing with files - if (String.IsNullOrEmpty(mapping) || mapping.StartsWith("\\\\")) + if (String.IsNullOrEmpty(mapping)) { Trace.WriteLine("Ignoring addition of mapping " + mapping + "type = " + type); return; } - switch (type) + else if (mapping.StartsWith("\\\\")) { - case MappingType.Name: - if (!KnownCN.Contains(mapping)) - if (!CNToInvestigate.Contains(mapping)) - CNToInvestigate.Add(mapping); - break; - case MappingType.Sid: - if (mapping.StartsWith("S-1-5-32-") || mapping.StartsWith(databaseInformation["DomainSid"])) - if (!KnownSID.Contains(mapping)) - if (!SIDToInvestigate.Contains(mapping)) - SIDToInvestigate.Add(mapping); - break; + if (!KnownFiles.Contains(mapping)) + if (!FilesToInvestigate.Contains(mapping)) + FilesToInvestigate.Add(mapping); + } + else + { + switch (type) + { + case MappingType.Name: + if (!KnownCN.Contains(mapping)) + if (!CNToInvestigate.Contains(mapping)) + CNToInvestigate.Add(mapping); + break; + case MappingType.Sid: + if (mapping.StartsWith("S-1-5-32-") || mapping.StartsWith(databaseInformation["DomainSid"])) + if (!KnownSID.Contains(mapping)) + if (!SIDToInvestigate.Contains(mapping)) + SIDToInvestigate.Add(mapping); + break; + } } } @@ -320,7 +347,15 @@ private int GetIdx(string name, MappingType mappingType) public Node RetrieveNode(int id) { - return nodes[id]; + try + { + return nodes[id]; + } + catch (KeyNotFoundException) + { + Trace.WriteLine("Unable to get node #" + id); + throw; + } } public Dictionary RetrieveNodes(List nodesQueried) diff --git a/Graph/Database/RelationType.cs b/Graph/Database/RelationType.cs index afd2dde..f023818 100644 --- a/Graph/Database/RelationType.cs +++ b/Graph/Database/RelationType.cs @@ -12,45 +12,47 @@ namespace PingCastle.Graph.Database { // use [Description("")] attribute to change the record name in the database - public enum RelationType - { - EXT_RIGHT_FORCE_CHANGE_PWD, - EXT_RIGHT_REPLICATION_GET_CHANGES_ALL, - WRITE_PROPSET_MEMBERSHIP, - WRITE_PROP_MEMBER, - WRITE_PROP_GPLINK, - WRITE_PROP_GPC_FILE_SYS_PATH, - VAL_WRITE_SELF_MEMBERSHIP, - gPCFileSysPath, - container_hierarchy, - group_member, - primary_group_member, - scriptPath, - AD_OWNER, - GEN_RIGHT_ALL, - GEN_RIGHT_WRITE, - ADS_RIGHT_WRITE_DAC, - ADS_RIGHT_WRITE_OWNER, - EXT_RIGHT_ALL, - VAL_WRITE_ALL, - WRITE_PROP_ALL, - GPLINK, - file_hierarchy, - FILE_OWNER, - STAND_RIGHT_WRITE_DAC, - STAND_RIGHT_WRITE_OWNER, - FS_RIGHT_WRITEDATA_ADDFILE, - FS_RIGHT_APPENDDATA_ADDSUBDIR, - SIDHistory, - SeBackupPrivilege, - SeCreateTokenPrivilege, - SeDebugPrivilege, - SeEnableDelegationPrivilege, - SeSyncAgentPrivilege, - SeTakeOwnershipPrivilege, - SeTcbPrivilege, - SeTrustedCredManAccessPrivilege, - LogonScript, - LogoffScript, - } + public enum RelationType + { + EXT_RIGHT_FORCE_CHANGE_PWD, + EXT_RIGHT_REPLICATION_GET_CHANGES_ALL, + WRITE_PROPSET_MEMBERSHIP, + WRITE_PROP_MEMBER, + WRITE_PROP_GPLINK, + WRITE_PROP_GPC_FILE_SYS_PATH, + VAL_WRITE_SELF_MEMBERSHIP, + gPCFileSysPath, + container_hierarchy, + group_member, + primary_group_member, + scriptPath, + AD_OWNER, + GEN_RIGHT_ALL, + GEN_RIGHT_WRITE, + ADS_RIGHT_WRITE_DAC, + ADS_RIGHT_WRITE_OWNER, + EXT_RIGHT_ALL, + VAL_WRITE_ALL, + WRITE_PROP_ALL, + GPLINK, + file_hierarchy, + FILE_OWNER, + STAND_RIGHT_WRITE_DAC, + STAND_RIGHT_WRITE_OWNER, + FS_RIGHT_WRITEDATA_ADDFILE, + FS_RIGHT_APPENDDATA_ADDSUBDIR, + SIDHistory, + SeBackupPrivilege, + SeCreateTokenPrivilege, + SeDebugPrivilege, + SeEnableDelegationPrivilege, + SeSyncAgentPrivilege, + SeTakeOwnershipPrivilege, + SeTcbPrivilege, + SeTrustedCredManAccessPrivilege, + LogonScript, + LogoffScript, + msDSAllowedToActOnBehalfOfOtherIdentity, + READ_PROP_MS_MCS_ADMPWD, + } } diff --git a/Graph/Export/ExportDataFromActiveDirectoryLive.cs b/Graph/Export/ExportDataFromActiveDirectoryLive.cs index da7bf47..4cb6cf2 100644 --- a/Graph/Export/ExportDataFromActiveDirectoryLive.cs +++ b/Graph/Export/ExportDataFromActiveDirectoryLive.cs @@ -6,7 +6,9 @@ // using PingCastle.ADWS; using PingCastle.Graph.Database; +using PingCastle.Graph.Export; using PingCastle.Graph.Reporting; +using PingCastle.misc; using PingCastle.RPC; using System; using System.Collections.Generic; @@ -15,6 +17,7 @@ using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; +using System.Threading; namespace PingCastle.Export { @@ -23,7 +26,8 @@ public class ExportDataFromActiveDirectoryLive string Server; int Port; NetworkCredential Credential; - string[] properties = new string[] { + + List properties = new List { "distinguishedName", "displayName", "name", @@ -63,7 +67,7 @@ private void DisplayAdvancement(string data) public GraphObjectReference ExportData(List UsersToInvestigate) { ADDomainInfo domainInfo = null; - RelationFactory relationFactory = null; + IRelationFactory relationFactory = null; GraphObjectReference objectReference = null; DisplayAdvancement("Getting domain information (" + Server + ")"); using (ADWebService adws = new ADWebService(Server, Port, Credential)) @@ -71,7 +75,8 @@ public GraphObjectReference ExportData(List UsersToInvestigate) domainInfo = GetDomainInformation(adws); Storage.Initialize(domainInfo); Trace.WriteLine("Creating new relation factory"); - relationFactory = new RelationFactory(Storage, domainInfo, Credential); + relationFactory = new RelationFactory(Storage, domainInfo); + relationFactory.Initialize(adws); DisplayAdvancement("Exporting objects from Active Directory"); objectReference = new GraphObjectReference(domainInfo); ExportReportData(adws, domainInfo, relationFactory, Storage, objectReference, UsersToInvestigate); @@ -100,7 +105,7 @@ private ADDomainInfo GetDomainInformation(ADWebService adws) Console.ResetColor(); } // adding the domain sid - string[] properties = new string[] {"objectSid", + string[] propertiesdomain = new string[] {"objectSid", }; WorkOnReturnedObjectByADWS callback = (ADItem aditem) => @@ -110,7 +115,7 @@ private ADDomainInfo GetDomainInformation(ADWebService adws) adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=domain)(distinguishedName=" + domainInfo.DefaultNamingContext + "))", - properties, callback); + propertiesdomain, callback); // adding the domain Netbios name string[] propertiesNetbios = new string[] { "nETBIOSName" }; adws.Enumerate("CN=Partitions," + domainInfo.ConfigurationNamingContext, @@ -121,10 +126,16 @@ private ADDomainInfo GetDomainInformation(ADWebService adws) domainInfo.NetBIOSName = aditem.NetBIOSName; } , "OneLevel"); + + if (domainInfo.ForestFunctionality >= 5) + { + properties.Add("msDS-AllowedToActOnBehalfOfOtherIdentity"); + } + return domainInfo; } - private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, LiveDataStorage storage, GraphObjectReference objectReference, List UsersToInvestigate) + private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, IDataStorage storage, GraphObjectReference objectReference, List UsersToInvestigate) { ADItem aditem = null; foreach (var typology in objectReference.Objects.Keys) @@ -153,7 +164,12 @@ private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, Relati aditem = Search(adws, domainInfo, user); if (aditem != null) { - objectReference.Objects[Data.CompromiseGraphDataTypology.UserDefined].Add(new GraphSingleObject(user, user)); + string userKey = user; + if (aditem.ObjectSid != null) + { + userKey = aditem.ObjectSid.Value; + } + objectReference.Objects[Data.CompromiseGraphDataTypology.UserDefined].Add(new GraphSingleObject(userKey, user)); relationFactory.AnalyzeADObject(aditem); } else @@ -161,12 +177,9 @@ private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, Relati } AnalyzeMissingObjets(adws, domainInfo, relationFactory, storage); - relationFactory.InsertFiles(); - AnalyzeMissingObjets(adws, domainInfo, relationFactory, storage); - } - int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, LiveDataStorage Storage) + int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, IDataStorage Storage) { int num = 0; while (true) @@ -189,14 +202,20 @@ int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, RelationFac num += primaryGroupId.Count; ExportPrimaryGroupData(adws, domainInfo, relationFactory, primaryGroupId); } - if (cns.Count == 0 && sids.Count == 0 && primaryGroupId.Count == 0) + List files = Storage.GetFilesToInvestigate(); + if (files.Count > 0) + { + num += files.Count; + ExportFilesData(adws, domainInfo, relationFactory, files); + } + if (cns.Count == 0 && sids.Count == 0 && primaryGroupId.Count == 0 && files.Count == 0) { return num; } } } - private void ExportCNData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, List cns) + private void ExportCNData(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, List cns) { WorkOnReturnedObjectByADWS callback = (ADItem aditem) => @@ -208,11 +227,11 @@ private void ExportCNData(ADWebService adws, ADDomainInfo domainInfo, RelationFa { adws.Enumerate(domainInfo.DefaultNamingContext, "(distinguishedName=" + ADConnection.EscapeLDAP(cn) + ")", - properties, callback); + properties.ToArray(), callback); } } - private void ExportSIDData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, List sids) + private void ExportSIDData(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, List sids) { WorkOnReturnedObjectByADWS callback = (ADItem aditem) => @@ -224,11 +243,11 @@ private void ExportSIDData(ADWebService adws, ADDomainInfo domainInfo, RelationF { adws.Enumerate(domainInfo.DefaultNamingContext, "(objectSid=" + ADConnection.EncodeSidToString(sid) + ")", - properties, callback); + properties.ToArray(), callback); } } - private void ExportPrimaryGroupData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, List primaryGroupIDs) + private void ExportPrimaryGroupData(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, List primaryGroupIDs) { WorkOnReturnedObjectByADWS callback = (ADItem aditem) => @@ -240,7 +259,81 @@ private void ExportPrimaryGroupData(ADWebService adws, ADDomainInfo domainInfo, { adws.Enumerate(domainInfo.DefaultNamingContext, "(primaryGroupID=" + id + ")", - properties, callback); + properties.ToArray(), callback); + } + } + + + private void ExportFilesData(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, List files) + { + if (Credential != null) + { + using (WindowsIdentity identity = NativeMethods.GetWindowsIdentityForUser(Credential, domainInfo.DnsHostName)) + using (var context = identity.Impersonate()) + { + ExportFilesDataWithImpersonation(adws, domainInfo, relationFactory, files); + context.Undo(); + } + } + else + { + ExportFilesDataWithImpersonation(adws, domainInfo, relationFactory, files); + } + } + + private void ExportFilesDataWithImpersonation(ADWebService adws, ADDomainInfo domainInfo, IRelationFactory relationFactory, List files) + { + // insert relation related to the files already seen. + // add subdirectory / sub file is the permission is not inherited + BlockingQueue queue = new BlockingQueue(200); + int numberOfThread = 20; + Thread[] threads = new Thread[numberOfThread]; + try + { + ThreadStart threadFunction = () => + { + for (; ; ) + { + string fileName = null; + if (!queue.Dequeue(out fileName)) break; + + // function is safe and will never trigger an exception + relationFactory.AnalyzeFile(fileName); + + } + Trace.WriteLine("Consumer quitting"); + }; + + // Consumers + for (int i = 0; i < numberOfThread; i++) + { + threads[i] = new Thread(threadFunction); + threads[i].Start(); + } + + // do it in parallele (else time *6 !) + foreach (string file in files) + { + queue.Enqueue(file); + } + queue.Quit(); + Trace.WriteLine("insert file completed. Waiting for worker thread to complete"); + for (int i = 0; i < numberOfThread; i++) + { + threads[i].Join(); + } + Trace.WriteLine("Done insert file"); + } + finally + { + + queue.Quit(); + for (int i = 0; i < numberOfThread; i++) + { + if (threads[i] != null) + if (threads[i].ThreadState == System.Threading.ThreadState.Running) + threads[i].Abort(); + } } } @@ -257,7 +350,7 @@ private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userNam { adws.Enumerate(domainInfo.DefaultNamingContext, "(objectSid=" + ADConnection.EncodeSidToString(userName) + ")", - properties, callback); + properties.ToArray(), callback); if (output != null) return output; } @@ -265,7 +358,7 @@ private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userNam { adws.Enumerate(domainInfo.DefaultNamingContext, "(distinguishedName=" + ADConnection.EscapeLDAP(userName) + ")", - properties, callback); + properties.ToArray(), callback); if (output != null) return output; } @@ -273,18 +366,18 @@ private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userNam { adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectCategory=person)(objectClass=user)(sAMAccountName=" + ADConnection.EscapeLDAP(userName) + "))", - properties, callback); + properties.ToArray(), callback); if (output != null) return output; } adws.Enumerate(domainInfo.DefaultNamingContext, "(cn=" + ADConnection.EscapeLDAP(userName) + ")", - properties, callback); + properties.ToArray(), callback); if (output != null) return output; adws.Enumerate(domainInfo.DefaultNamingContext, "(displayName=" + ADConnection.EscapeLDAP(userName) + ")", - properties, callback); + properties.ToArray(), callback); if (output != null) return output; return output; diff --git a/Graph/Export/IRelationFactory.cs b/Graph/Export/IRelationFactory.cs new file mode 100644 index 0000000..41030a8 --- /dev/null +++ b/Graph/Export/IRelationFactory.cs @@ -0,0 +1,15 @@ +using PingCastle.ADWS; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Graph.Export +{ + public interface IRelationFactory + { + void AnalyzeADObject(ADItem aditem); + void AnalyzeFile(string fileName); + + void Initialize(IADConnection adws); + } +} diff --git a/Graph/Export/RelationFactory.cs b/Graph/Export/RelationFactory.cs index c16711c..3b1cf2c 100644 --- a/Graph/Export/RelationFactory.cs +++ b/Graph/Export/RelationFactory.cs @@ -6,6 +6,7 @@ // using PingCastle.ADWS; using PingCastle.Graph.Database; +using PingCastle.Graph.Export; using PingCastle.misc; using System; using System.Collections.Generic; @@ -20,24 +21,22 @@ using System.Text.RegularExpressions; using System.Threading; -namespace PingCastle.Export +namespace PingCastle.Graph.Export { - - public class RelationFactory + + public class RelationFactory : IRelationFactory { public IDataStorage Storage { get; set; } public ADDomainInfo DomainInfo { get; set; } - public NetworkCredential Credential { get; set; } private List Files = new List(); private List GPO = new List(); - public RelationFactory(IDataStorage storage, ADDomainInfo domainInfo, NetworkCredential credential) + public RelationFactory(IDataStorage storage, ADDomainInfo domainInfo) { Storage = storage; DomainInfo = domainInfo; - Credential = credential; } public static KeyValuePair[] GuidsControlExtendedRights = new KeyValuePair[] { @@ -58,12 +57,24 @@ public RelationFactory(IDataStorage storage, ADDomainInfo domainInfo, NetworkCre new KeyValuePair(new Guid("bf9679c0-0de6-11d0-a285-00aa003049e2"),RelationType.VAL_WRITE_SELF_MEMBERSHIP), }; + public List> GuidsReadProperties = new List>(); + + public void Initialize(IADConnection adws) + { + string[] propertiesLaps = new string[] { "schemaIDGUID" }; + // note: the LDAP request does not contain ms-MCS-AdmPwd because in the old time, MS consultant was installing customized version of the attriute, * being replaced by the company name + // check the oid instead ? (which was the same even if the attribute name was not) + adws.Enumerate(DomainInfo.SchemaNamingContext, "(name=ms-*-AdmPwd)", propertiesLaps, (ADItem aditem) => { + GuidsReadProperties.Add(new KeyValuePair(aditem.SchemaIDGUID, RelationType.READ_PROP_MS_MCS_ADMPWD)); + }, "OneLevel"); + } + public void AnalyzeADObject(ADItem aditem) { // avoid reentry which can be caused by primary group id checks if (aditem.ObjectSid != null) { - if (Storage.IsSIDAlreadyInserted(aditem.ObjectSid.Value)) + if (Storage.SearchItem(aditem.ObjectSid.Value) != -1) { Trace.WriteLine("Item " + aditem.DistinguishedName + " has already been analyzed"); return; @@ -102,21 +113,34 @@ private void InsertNode(ADItem aditem) Storage.InsertNode(shortname, aditem.Class.ToLowerInvariant(), aditem.DistinguishedName, (aditem.ObjectSid != null ? aditem.ObjectSid.Value : null), aditem); } - public void InsertFileNode(string file) + private void InsertFileNode(string fileName) { - Storage.InsertNode(file, "file", file, null, null); + Storage.InsertNode(fileName, "file", fileName, null, null); + } + + private string SanitizeFileName(string filename, string domainSysVolLocation) + { + if (filename.StartsWith("\\\\")) + { + return filename; + } + else + { + return ("\\\\" + DomainInfo.DomainName + "\\sysvol\\" + DomainInfo.DomainName + "\\" + domainSysVolLocation + "\\" + filename).ToLowerInvariant(); + } } private void AddFileRelation(ADItem aditem) { + if (!String.IsNullOrEmpty(aditem.ScriptPath)) + { + string file = SanitizeFileName(aditem.ScriptPath, "scripts"); + Storage.InsertRelation(file, MappingType.Name, aditem.DistinguishedName, MappingType.Name, RelationType.scriptPath); + } if (!String.IsNullOrEmpty(aditem.GPCFileSysPath)) { - string path = aditem.GPCFileSysPath.ToLowerInvariant(); - if (!GPO.Contains(path)) - { - GPO.Add(path); - } - Storage.InsertRelation(aditem.GPCFileSysPath, MappingType.Name, aditem.DistinguishedName, MappingType.Name, RelationType.gPCFileSysPath); + string file = SanitizeFileName(aditem.GPCFileSysPath, "Policies"); + Storage.InsertRelation(file, MappingType.Name, aditem.DistinguishedName, MappingType.Name, RelationType.gPCFileSysPath); } } @@ -149,19 +173,14 @@ private void AddADRelation(ADItem aditem) { InsertSecurityDescriptorRelation(aditem); } + if (aditem.msDSAllowedToActOnBehalfOfOtherIdentity != null) + { + InsertDelegationRelation(aditem); + } if (!String.IsNullOrEmpty(aditem.GPLink)) { InsertGPORelation(aditem); } - if (!String.IsNullOrEmpty(aditem.ScriptPath)) - { - string file = ("\\\\" + DomainInfo.DomainName + "\\sysvol\\" + DomainInfo.DomainName + "\\scripts\\" + aditem.ScriptPath).ToLowerInvariant(); - if (!Files.Contains(file)) - { - Files.Add(file); - } - Storage.InsertRelation(file, MappingType.Name, aditem.DistinguishedName, MappingType.Name, RelationType.scriptPath); - } if (aditem.SIDHistory != null) { foreach (SecurityIdentifier sidHistory in aditem.SIDHistory) @@ -171,6 +190,15 @@ private void AddADRelation(ADItem aditem) } } + private void InsertDelegationRelation(ADItem aditem) + { + ActiveDirectorySecurity sd = aditem.msDSAllowedToActOnBehalfOfOtherIdentity; + foreach (ActiveDirectoryAccessRule rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier))) + { + Storage.InsertRelation(((SecurityIdentifier)rule.IdentityReference).Value, MappingType.Sid, aditem.DistinguishedName, MappingType.Name, RelationType.msDSAllowedToActOnBehalfOfOtherIdentity); + } + } + // utility fonction to avoid inserting duplicate relations private static void IncludeRelationInDictionary(Dictionary> relationToAdd, string targetsid, RelationType relationType) @@ -200,87 +228,97 @@ private void InsertSecurityDescriptorRelation(ADItem aditem) continue; // ADS_RIGHT_GENERIC_ALL - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.GenericAll)) { IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_ALL); } - // ADS_RIGHT_GENERIC_WRITE - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.GenericWrite) == ActiveDirectoryRights.GenericWrite) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_WRITE); - } - // ADS_RIGHT_WRITE_DAC - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.ADS_RIGHT_WRITE_DAC); - } - // ADS_RIGHT_WRITE_OWNER - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.ADS_RIGHT_WRITE_OWNER); - } - if (accessrule.ObjectFlags == ObjectAceFlags.None) + else { - // ADS_RIGHT_DS_CONTROL_ACCESS - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.EXT_RIGHT_ALL); - } - // ADS_RIGHT_DS_SELF - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.Self) == ActiveDirectoryRights.Self) + // ADS_RIGHT_GENERIC_WRITE + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.GenericWrite)) { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.VAL_WRITE_ALL); + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_WRITE); } - // ADS_RIGHT_DS_WRITE_PROP - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty) + // ADS_RIGHT_WRITE_DAC + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.WriteDacl)) { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.WRITE_PROP_ALL); + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.ADS_RIGHT_WRITE_DAC); } - } - else if ((accessrule.ObjectFlags & ObjectAceFlags.ObjectAceTypePresent) == ObjectAceFlags.ObjectAceTypePresent) - { - if (new Guid("00299570-246d-11d0-a768-00aa006e0529") == accessrule.ObjectType) + // ADS_RIGHT_WRITE_OWNER + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.WriteOwner)) { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.ADS_RIGHT_WRITE_OWNER); } - // ADS_RIGHT_DS_CONTROL_ACCESS - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + if (accessrule.ObjectFlags == ObjectAceFlags.None) { - foreach (KeyValuePair extendedright in GuidsControlExtendedRights) + // ADS_RIGHT_DS_CONTROL_ACCESS + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.ExtendedRight)) { - if (extendedright.Key == accessrule.ObjectType) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, extendedright.Value); - } + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.EXT_RIGHT_ALL); + } + // ADS_RIGHT_DS_SELF + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.Self)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.VAL_WRITE_ALL); + } + // ADS_RIGHT_DS_WRITE_PROP + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.WriteProperty)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.WRITE_PROP_ALL); } } - // ADS_RIGHT_DS_SELF - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.Self) == ActiveDirectoryRights.Self) + else if ((accessrule.ObjectFlags & ObjectAceFlags.ObjectAceTypePresent) == ObjectAceFlags.ObjectAceTypePresent) { - foreach (KeyValuePair validatewrite in GuidsControlValidatedWrites) + // ADS_RIGHT_DS_CONTROL_ACCESS + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.ExtendedRight)) { - if (validatewrite.Key == accessrule.ObjectType) + foreach (KeyValuePair extendedright in GuidsControlExtendedRights) { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, validatewrite.Value); + if (extendedright.Key == accessrule.ObjectType) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, extendedright.Value); + } } } - } - // ADS_RIGHT_DS_WRITE_PROP - if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty) - { - foreach (KeyValuePair controlproperty in GuidsControlProperties) + // ADS_RIGHT_DS_SELF + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.Self)) { - if (controlproperty.Key == accessrule.ObjectType) + foreach (KeyValuePair validatewrite in GuidsControlValidatedWrites) { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, controlproperty.Value); + if (validatewrite.Key == accessrule.ObjectType) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, validatewrite.Value); + } } } - foreach (KeyValuePair controlpropertyset in GuidsControlPropertiesSets) + // ADS_RIGHT_DS_WRITE_PROP + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.WriteProperty)) { - if (controlpropertyset.Key == accessrule.ObjectType) + foreach (KeyValuePair controlproperty in GuidsControlProperties) + { + if (controlproperty.Key == accessrule.ObjectType) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, controlproperty.Value); + } + } + foreach (KeyValuePair controlpropertyset in GuidsControlPropertiesSets) { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, controlpropertyset.Value); + if (controlpropertyset.Key == accessrule.ObjectType) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, controlpropertyset.Value); + } } } + if (IsRightSetinAccessRule(accessrule, ActiveDirectoryRights.ReadProperty)) + { + foreach (KeyValuePair controlproperty in GuidsReadProperties) + { + if (controlproperty.Key == accessrule.ObjectType) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, controlproperty.Value); + } + } + } } } } @@ -335,139 +373,64 @@ private string GetContainerDN(string dn) return m.Groups[2].Value; } - public void InsertFiles() + public void AnalyzeFile(string fileName) { - // insert relation related to the files already seen. - // add subdirectory / sub file is the permission is not inherited - WindowsIdentity identity = null; - WindowsImpersonationContext context = null; - BlockingQueue queue = new BlockingQueue(200); - int numberOfThread = 20; - Thread[] threads = new Thread[numberOfThread]; + Trace.WriteLine("working on filenode=" + fileName); try { - if (Credential != null) - { - identity = NativeMethods.GetWindowsIdentityForUser(Credential, DomainInfo.DnsHostName); - context = identity.Impersonate(); - } + InsertFileNode(fileName); - ThreadStart threadFunction = () => + // when connecting a login password, unc path starting the with domain name (<> domain controller) fails + // rebuild it by replacing the domain name by the domain controller name + Uri uri; + if (!Uri.TryCreate(fileName, UriKind.RelativeOrAbsolute, out uri)) { - for (; ; ) - { - string filenode = null; - if (!queue.Dequeue(out filenode)) break; - Uri uri; - if (!Uri.TryCreate(filenode, UriKind.RelativeOrAbsolute, out uri)) - { - Trace.WriteLine("Unable to parse the url: " + filenode); - return; - } - if (!uri.IsUnc) - { - Trace.WriteLine("File " + filenode + " is not a unc path"); - InsertFileNode(filenode); - return; - } - // when connecting a login password, unc path starting the with domain name (<> domain controller) fails - // rebuild it by replacing the domain name by the domain controller name - if (Credential != null) - { - if (uri.Host.Equals(DomainInfo.DomainName, StringComparison.InvariantCultureIgnoreCase)) - { - UriBuilder builder = new UriBuilder(uri); - builder.Host = DomainInfo.DnsHostName; - uri = builder.Uri; - } - } - string filepath = uri.LocalPath; - - // function is safe and will never trigger an exception - InsertFile(filenode, filepath); - - } - Trace.WriteLine("Consumer quitting"); - }; - - // Consumers - for (int i = 0; i < numberOfThread; i++) - { - threads[i] = new Thread(threadFunction); - threads[i].Start(); - } - - // do it in parallele (else time *6 !) - foreach (string filenode in Files) - { - queue.Enqueue(filenode); + Trace.WriteLine("Unable to parse the url: " + fileName); + return; } - foreach (string filenode in GPO) + if (!uri.IsUnc) { - queue.Enqueue(filenode); + Trace.WriteLine("File " + fileName + " is not a unc path"); + return; } - queue.Quit(); - Trace.WriteLine("insert file completed. Waiting for worker thread to complete"); - for (int i = 0; i < numberOfThread; i++) - { - threads[i].Join(); - } - Trace.WriteLine("Done insert file"); - } - finally - { - queue.Quit(); - for (int i = 0; i < numberOfThread; i++) + // SYSVOL volume cannot be accessed with login / password login + // in this case, the server (aka the domain) needs to be replaced with the FQDN of the server + if (uri.Host.Equals(DomainInfo.DomainName, StringComparison.InvariantCultureIgnoreCase)) { - if (threads[i] != null) - if (threads[i].ThreadState == System.Threading.ThreadState.Running) - threads[i].Abort(); + UriBuilder builder = new UriBuilder(uri); + builder.Host = DomainInfo.DnsHostName; + uri = builder.Uri; } - if (context != null) - context.Undo(); - if (identity != null) - identity.Dispose(); - } - } + string alternativeFilepath = uri.LocalPath; - // filePath is different from filenode - // SYSVOL volume cannot be accessed with login / password login - // in this case, the server (aka the domain) needs to be replaced with the FQDN of the server - private void InsertFile(string filenode, string filepath) - { - Trace.WriteLine("working on filenode=" + filenode); - try - { - InsertFileNode(filenode); - FileSystemInfo info = null; - FileAttributes attr = File.GetAttributes(filepath); + FileAttributes attr = File.GetAttributes(alternativeFilepath); FileSystemSecurity fss = null; // insert relation related to security descriptor if ((attr & FileAttributes.Directory) == FileAttributes.Directory) { - info = new DirectoryInfo(filepath); + info = new DirectoryInfo(alternativeFilepath); fss = ((DirectoryInfo)info).GetAccessControl(); } else { - info = new FileInfo(filepath); + info = new FileInfo(alternativeFilepath); fss = ((FileInfo)info).GetAccessControl(); } - InsertFileDescriptorRelation(filenode, fss, false, null); + InsertFileDescriptorRelation(fileName, fss, false, null); // try to find illegitimate soons if (info as DirectoryInfo != null) { // analyse SD of files in directory - AnalyzeFile(filenode, (DirectoryInfo)info, fss.GetOwner(typeof(SecurityIdentifier)).Value); + AnalyzeFile(fileName, (DirectoryInfo)info, fss.GetOwner(typeof(SecurityIdentifier)).Value); // find hidden relations - AnalyzeGPODirectory(filenode, (DirectoryInfo)info); + AnalyzeGPODirectory(fileName, (DirectoryInfo)info); } } catch (Exception ex) { - Trace.WriteLine("An exception occured while working on the file '" + filenode + "':" + ex.Message); + Trace.WriteLine("An exception occured while working on the file '" + fileName + "':" + ex.Message); } } @@ -677,34 +640,37 @@ private bool InsertFileDescriptorRelation(string filenode, FileSystemSecurity sd continue; // GEN_RIGHT_ALL - if ((accessrule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) + if (IsRightSetinAccessRule(accessrule, FileSystemRights.FullControl)) { IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_ALL); } - // GEN_RIGHT_WRITE - if ((accessrule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_WRITE); - } - // STAND_RIGHT_WRITE_DAC - if ((accessrule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.STAND_RIGHT_WRITE_DAC); - } - // STAND_RIGHT_WRITE_OWNER - if ((accessrule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.STAND_RIGHT_WRITE_OWNER); - } - // FILE_WRITEDATA_ADDFILE - if ((accessrule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) - { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.FS_RIGHT_WRITEDATA_ADDFILE); - } - // FILE_APPENDDATA_ADDSUBDIR - if ((accessrule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) + else { - IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.FS_RIGHT_APPENDDATA_ADDSUBDIR); + // GEN_RIGHT_WRITE + if (IsRightSetinAccessRule(accessrule, FileSystemRights.Write)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.GEN_RIGHT_WRITE); + } + // STAND_RIGHT_WRITE_DAC + if (IsRightSetinAccessRule(accessrule, FileSystemRights.ChangePermissions)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.STAND_RIGHT_WRITE_DAC); + } + // STAND_RIGHT_WRITE_OWNER + if (IsRightSetinAccessRule(accessrule, FileSystemRights.TakeOwnership)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.STAND_RIGHT_WRITE_OWNER); + } + // FILE_WRITEDATA_ADDFILE + if (IsRightSetinAccessRule(accessrule, FileSystemRights.WriteData)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.FS_RIGHT_WRITEDATA_ADDFILE); + } + // FILE_APPENDDATA_ADDSUBDIR + if (IsRightSetinAccessRule(accessrule, FileSystemRights.AppendData)) + { + IncludeRelationInDictionary(relationToAdd, accessrule.IdentityReference.Value, RelationType.FS_RIGHT_APPENDDATA_ADDSUBDIR); + } } } foreach (string target in relationToAdd.Keys) @@ -718,7 +684,15 @@ private bool InsertFileDescriptorRelation(string filenode, FileSystemSecurity sd return newRelation; } + private static bool IsRightSetinAccessRule(ActiveDirectoryAccessRule accessrule, ActiveDirectoryRights right) + { + return (accessrule.ActiveDirectoryRights & right) == right; + } + private static bool IsRightSetinAccessRule(FileSystemAccessRule accessrule, FileSystemRights right) + { + return (accessrule.FileSystemRights & right) == right; + } } } diff --git a/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs b/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs index a3665f2..adf49d1 100644 --- a/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs +++ b/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs @@ -15,6 +15,7 @@ namespace PingCastle.Graph.Rules [RuleObjectiveAttribute("P-OperatorsEmpty", RiskRuleCategory.PrivilegedAccounts, RiskModelObjective.PrivilegedBestPractices)] [RuleComputation(RuleComputationType.Objective, 25)] [RuleANSSI("R27", "subsection.3.5")] + [RuleIntroducedIn(2, 6)] public class CompromiseGraphPrivilegedOperatorsEmpty : CompromiseGraphRule { protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) diff --git a/Healthcheck/ADModel.cs b/Healthcheck/ADModel.cs index 16e8389..07d9c39 100644 --- a/Healthcheck/ADModel.cs +++ b/Healthcheck/ADModel.cs @@ -24,12 +24,12 @@ private GraphNodeCollection() // SID is suppose to identify uniquely a domain // but we cannot guarantee that it is here (for example domain removed or firewalled) - public GraphNode CreateNodeIfNeeded(ref int number, DomainKey Domain, string NetBiosName, DateTime ReferenceDate) + public GraphNode CreateNodeIfNeeded(ref int number, DomainKey Domain, DateTime ReferenceDate) { GraphNode output = null; if (!data.ContainsKey(Domain)) { - output = new GraphNode(number++, Domain, NetBiosName, ReferenceDate); + output = new GraphNode(number++, Domain, ReferenceDate); data[Domain] = output; } else @@ -103,7 +103,7 @@ public GraphNode Locate(DomainKey domain) public GraphNode GetDomain(string center) { - DomainKey key = new DomainKey(center, null, null); + DomainKey key = DomainKey.Create(center, null, null); return Locate(key); } @@ -118,6 +118,8 @@ private static void EnrichDomainInfo(PingCastleReportCollection { di.DnsName = data.DomainFQDN; di.ForestName = data.ForestFQDN; + di.Forest = data.Forest; + di.Domain = data.Domain; break; } foreach (var trust in data.Trusts) @@ -140,6 +142,8 @@ private static void EnrichDomainInfo(PingCastleReportCollection { di.DnsName = forestinfo.DnsName; di.ForestName = trust.TrustPartner; + di.Forest = trust.Domain; + di.Domain = forestinfo.Domain; enriched = true; break; } @@ -162,7 +166,7 @@ static public GraphNodeCollection BuildModel(PingCastleReportCollection Trusts { get; private set; } - public GraphNode(int Id, DomainKey Domain, string NetBiosName, DateTime ReferenceDate) + public GraphNode(int Id, DomainKey Domain, DateTime ReferenceDate) { Trace.WriteLine("Creating " + Domain); this.Id = Id; this.Domain = Domain; - this.NetBiosName = NetBiosName; this.ReferenceDate = ReferenceDate; Trusts = new Dictionary(); } @@ -439,7 +447,7 @@ public void LinkTwoForests(GraphNode destination) public static GraphNode CloneWithoutTrusts(GraphNode inputNode) { - GraphNode output = new GraphNode(inputNode.Id, inputNode.Domain, inputNode.NetBiosName, inputNode.ReferenceDate); + GraphNode output = new GraphNode(inputNode.Id, inputNode.Domain, inputNode.ReferenceDate); output.Forest = inputNode.Forest; output.HealthCheckData = inputNode.HealthCheckData; output.Entity = inputNode.Entity; diff --git a/Healthcheck/HealthcheckAnalyzer.cs b/Healthcheck/HealthcheckAnalyzer.cs index fe8b7a9..f71168e 100644 --- a/Healthcheck/HealthcheckAnalyzer.cs +++ b/Healthcheck/HealthcheckAnalyzer.cs @@ -118,6 +118,7 @@ public HealthcheckData PerformAnalyze(PingCastleAnalyzerParameters parameters) GenerateAnomalies(domainInfo, adws); DisplayAdvancement("Gathering domain controller data" + (SkipNullSession?null:" (including null session)")); GenerateDomainControllerData(domainInfo); + GenerateFSMOData(domainInfo, adws); DisplayAdvancement("Gathering network data"); GenerateNetworkData(domainInfo, adws); } @@ -288,7 +289,25 @@ private void GenerateGeneralData(ADDomainInfo domainInfo, ADWebService adws) healthcheckData.SchemaInternalVersion = domainInfo.SchemaInternalVersion; healthcheckData.SchemaLastChanged = domainInfo.SchemaLastChanged; healthcheckData.GenerationDate = DateTime.Now; - Version version = Assembly.GetExecutingAssembly().GetName().Version; + + string[] propertiesEnabledFeature = new string[] { "msDS-EnabledFeature" }; + adws.Enumerate("CN=Partitions," + domainInfo.ConfigurationNamingContext, + "(objectClass=*)", + propertiesEnabledFeature, (ADItem aditem) => + { + if (aditem.msDSEnabledFeature != null) + { + foreach (string feature in aditem.msDSEnabledFeature) + { + if (feature.StartsWith("CN=Recycle Bin Feature,", StringComparison.InvariantCultureIgnoreCase)) + { + healthcheckData.IsRecycleBinEnabled = true; + } + } + } + }, "Base"); + + Version version = Assembly.GetExecutingAssembly().GetName().Version; healthcheckData.EngineVersion = version.ToString(4); #if DEBUG healthcheckData.EngineVersion += " Beta"; @@ -322,7 +341,7 @@ private void GenerateUserData(ADDomainInfo domainInfo, ADWebService adws) // krbtgt if (x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountKrbtgtSid)) { - healthcheckData.KrbtgtLastChangeDate = x.PwdLastSet; + // krbtgt will be processed after - this avoid applying a filter on the object class return; } // admin account @@ -409,8 +428,9 @@ List CheckScriptPermission(ADDomainInfo domainI output.Add(new HealthcheckScriptDelegationData() { Account = account.Value.Value, Right = rule.FileSystemRights.ToString() }); } } - catch(Exception) + catch(Exception ex) { + Trace.WriteLine("Exception CheckScriptPermission " + ex.Message); } return output; } @@ -1113,7 +1133,6 @@ private void GeneratePrivilegedGroupData(ADDomainInfo domainInfo, ADWebService a new KeyValuePair("S-1-5-32-550","Print Operators"), new KeyValuePair("S-1-5-32-551","Backup Operators"), new KeyValuePair("S-1-5-32-569","Crypto Operators"), - new KeyValuePair("S-1-5-32-557","Incoming Forest Trust Builders"), new KeyValuePair("S-1-5-32-556","Network Operators"), new KeyValuePair(domainInfo.DomainSid + "-512","Domain Admins"), new KeyValuePair(domainInfo.DomainSid + "-519","Enterprise Admins"), @@ -1272,9 +1291,11 @@ private bool GetGroupMembers(ADDomainInfo domainInfo, ADWebService adws, string "sAMAccountName", "objectClass", "servicePrincipalName", + "primaryGroupID", }; bool IsObjectFound = false; List FutureDataToSearch = new List(); + List PrimaryGroupIDToSearch = new List(); WorkOnReturnedObjectByADWS callback = (ADItem x) => { @@ -1292,10 +1313,27 @@ private bool GetGroupMembers(ADDomainInfo domainInfo, ADWebService adws, string FutureDataToSearch.Add(member); } } - if (x.Class != "group") - { - output.Add(x.DistinguishedName, x); - } + if (x.Class != "group") + { + output.Add(x.DistinguishedName, x); + } + else + { + if (!x.ObjectSid.IsWellKnown(WellKnownSidType.AccountDomainUsersSid) && !x.ObjectSid.IsWellKnown(WellKnownSidType.AccountComputersSid)) + { + string sid = x.ObjectSid.Value; + if (sid.StartsWith(domainInfo.DomainSid.Value, StringComparison.OrdinalIgnoreCase)) + { + int rid = int.Parse(sid.Substring(sid.LastIndexOf('-') + 1)); + switch (rid) + { + default: + PrimaryGroupIDToSearch.Add(rid); + break; + } + } + } + } } ; string ldapSearch; @@ -1304,9 +1342,13 @@ private bool GetGroupMembers(ADDomainInfo domainInfo, ADWebService adws, string case 0: ldapSearch = "(objectSid=" + ADConnection.EncodeSidToString(dataToSearch) + ")"; break; + case 1: default: ldapSearch = "(distinguishedName=" + ADConnection.EscapeLDAP(dataToSearch) + ")"; break; + case 2: + ldapSearch = "(primaryGroupId=" + dataToSearch + ")"; + break; } adws.Enumerate(domainInfo.DefaultNamingContext, ldapSearch, properties, callback); // work on queued items @@ -1328,6 +1370,10 @@ private bool GetGroupMembers(ADDomainInfo domainInfo, ADWebService adws, string } } } + foreach (var rid in PrimaryGroupIDToSearch) + { + GetGroupMembers(domainInfo, adws, rid.ToString(), output, knownItems, 2); + } return IsObjectFound; } @@ -1476,6 +1522,10 @@ private void InspectDelegation(ADDomainInfo domainInfo, ADWebService adws) { delegation.Account = "Everyone"; } + else if (sid == "S-1-5-7") + { + delegation.Account = "Anonymous"; + } else if (sid == "S-1-5-11") { delegation.Account = "Authenticated Users"; @@ -1488,6 +1538,10 @@ private void InspectDelegation(ADDomainInfo domainInfo, ADWebService adws) { delegation.Account = "Domain Computers"; } + else if (sid == "S-1-5-32-545") + { + delegation.Account = "Users"; + } else { if (!sidCache.ContainsKey(sid)) @@ -1716,6 +1770,7 @@ private void GenerateGPOData(ADDomainInfo domainInfo, ADWebService adws, Network healthcheckData.GPOLocalMembership = new List(); healthcheckData.GPOEventForwarding = new List(); healthcheckData.GPODelegation = new List(); + healthcheckData.GPPFileDeployed = new List(); // subitility: GPOList = all active and not active GPO (but not the deleted ones) Dictionary GPOList = new Dictionary(); @@ -1723,6 +1778,14 @@ private void GenerateGPOData(ADDomainInfo domainInfo, ADWebService adws, Network // GPOList contains GPOListDisabled List GPOListDisabled = new List(); GetGPOList(domainInfo, adws, GPOList, GPOListDisabled); + + ParseGPOFiles(domainInfo, credential, GPOList, GPOListDisabled); + GenerateNTLMStoreData(domainInfo, adws); + GenerateMsiData(domainInfo, adws, GPOList, GPOListDisabled); + } + + private void ParseGPOFiles(ADDomainInfo domainInfo, NetworkCredential credential, Dictionary GPOList, List GPOListDisabled) + { WindowsIdentity identity = null; WindowsImpersonationContext context = null; BlockingQueue queue = new BlockingQueue(200); @@ -1767,8 +1830,8 @@ private void GenerateGPOData(ADDomainInfo domainInfo, ADWebService adws, Network { queue.Enqueue(directoryInfo); } - GenerateNTLMStoreData(domainInfo, adws); - queue.Quit(); + + queue.Quit(); Trace.WriteLine("examining file completed. Waiting for worker thread to complete"); for (int i = 0; i < numberOfThread; i++) { @@ -1826,8 +1889,6 @@ private void GenerateGPOData(ADDomainInfo domainInfo, ADWebService adws, Network } } - - private void GenerateNTLMStoreData(ADDomainInfo domainInfo, ADWebService adws) { string[] properties = new string[] { @@ -1859,6 +1920,59 @@ private void GenerateNTLMStoreData(ADDomainInfo domainInfo, ADWebService adws) } + + private void GenerateMsiData(ADDomainInfo domainInfo, ADWebService adws, Dictionary GPOList, List GPOListDisabled) + { + string[] properties = new string[] { + "distinguishedName", + "msiFileList", + }; + WorkOnReturnedObjectByADWS callback = + (ADItem x) => + { + if (x.msiFileList == null) + return; + int pos1 = x.DistinguishedName.IndexOf('{'); + if (pos1 < 0) + return; + int pos2 = x.DistinguishedName.IndexOf('}', pos1); + string GPOGuid = x.DistinguishedName.Substring(pos1, pos2 - pos1 + 1).ToLowerInvariant(); + if (!GPOList.ContainsKey(GPOGuid)) + return; + string GPOName = GPOList[GPOGuid]; + if (GPOListDisabled.Contains(GPOName)) + return; + string section = (x.DistinguishedName.Contains("Machine") ? "Computer" : "User"); + foreach (var msiFileItem in x.msiFileList) + { + var msiFile = msiFileItem.Split(':'); + if (msiFile.Length < 2) + continue; + var file = new GPPFileDeployed(); + file.GPOName = GPOName; + file.Type = "Application (" + section + " section)"; + file.FileName = msiFile[1]; + file.Delegation = new List(); + healthcheckData.GPPFileDeployed.Add(file); + if (File.Exists(file.FileName)) + { + var ac = File.GetAccessControl(file.FileName); + foreach (var value in AnalyzeFileSystemSecurity(ac, true)) + { + file.Delegation.Add(new HealthcheckScriptDelegationData() + { + Account = value.Value, + Right = value.Key, + } + ); + } + } + } + + }; + adws.Enumerate(domainInfo.DefaultNamingContext, "(objectClass=packageRegistration)", properties, callback); + } + void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo domainInfo, bool IsGPOActive) { string step = "initial"; @@ -1892,10 +2006,22 @@ void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo } step = "extract GPO login script"; ExtractGPOLoginScript(domainInfo, directoryInfo, GPOName); + path = directoryInfo.FullName + @"\User\Preferences\Files\Files.xml"; + if (File.Exists(path)) + { + step = "extract Files info"; + ExtractGPPFile(path, GPOName, domainInfo, "User"); + } + path = directoryInfo.FullName + @"\Machine\Preferences\Files\Files.xml"; + if (File.Exists(path)) + { + step = "extract Files info"; + ExtractGPPFile(path, GPOName, domainInfo, "Computer"); + } try { step = "extract Registry Pol info"; - ExtractRegistryPolInfo(directoryInfo, GPOName); + ExtractRegistryPolInfo(domainInfo, directoryInfo, GPOName); } catch (Exception ex) { @@ -1931,7 +2057,7 @@ void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo } } - private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) + private void ExtractRegistryPolInfo(ADDomainInfo domainInfo, DirectoryInfo directoryInfo, string GPOName) { GPPSecurityPolicy PSO = null; foreach (string gpotarget in new string[] { "Machine", "User" }) @@ -1959,6 +2085,29 @@ private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) } PSO.Properties.Add(new GPPSecurityPolicyProperty("ScreenSaverGracePeriod", intvalue)); } + if (reader.IsValueSet(@"Software\Policies\Microsoft\Windows NT\DNSClient", "EnableMulticast", out intvalue)) + { + GPPSecurityPolicy SecurityPolicy = null; + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + if (policy.GPOName == GPOName) + { + SecurityPolicy = policy; + break; + } + } + if (SecurityPolicy == null) + { + SecurityPolicy = new GPPSecurityPolicy(); + SecurityPolicy.GPOName = GPOName; + lock (healthcheckData.GPOLsaPolicy) + { + healthcheckData.GPOLsaPolicy.Add(SecurityPolicy); + } + SecurityPolicy.Properties = new List(); + } + SecurityPolicy.Properties.Add(new GPPSecurityPolicyProperty("EnableMulticast", intvalue)); + } for (int i = 1; ; i++) { string server = null; @@ -2026,7 +2175,7 @@ private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) } PSO.Properties.Add(new GPPSecurityPolicyProperty("ScreenSaverIsSecure", intvalue)); } - } + } // search for certificates foreach (string storename in new string[] {"Root", "CA","Trust", "TrustedPeople", "TrustedPublisher", }) { @@ -2046,6 +2195,35 @@ private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) } } } + foreach (RegistryPolRecord record in reader.SearchRecord(@"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run")) + { + if (record.Value == "**delvals.") + continue; + string filename = Encoding.Unicode.GetString(record.ByteValue).Trim(); + if (string.IsNullOrEmpty(filename)) + continue; + filename = filename.Replace("\0", string.Empty); + HealthcheckGPOLoginScriptData loginscript = new HealthcheckGPOLoginScriptData(); + loginscript.GPOName = GPOName; + loginscript.Action = "Logon"; + // this is bad, I'm assuming that the file name doesn't contain any space which is wrong. + // but a real command line parsing will bring more anomalies. + var filePart = filename.Split(' '); + loginscript.Source = "Registry.pol (" + (gpotarget == "Machine" ? "Computer" : "User") + " section)"; + loginscript.CommandLine = filePart[0]; + if (loginscript.CommandLine.StartsWith("\\\\")) + { + loginscript.Delegation = CheckScriptPermission(domainInfo, loginscript.CommandLine); + } + if (filePart.Length > 1) + { + loginscript.Parameters = filePart[1]; + } + lock (healthcheckData.GPOLoginScript) + { + healthcheckData.GPOLoginScript.Add(loginscript); + } + } } } } @@ -2056,10 +2234,18 @@ private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) { return new KeyValuePair(sid, "Everyone"); } + else if (sid.Value == "S-1-5-7") + { + return new KeyValuePair(sid, "Anonymous"); + } else if (sid.Value == "S-1-5-11") { return new KeyValuePair(sid, "Authenticated Users"); } + else if (sid.Value == "S-1-5-32-545") + { + return new KeyValuePair(sid, "Users"); + } else if (sid.IsWellKnown(WellKnownSidType.AccountDomainGuestsSid) || sid.IsWellKnown(WellKnownSidType.AccountDomainUsersSid) || sid.IsWellKnown(WellKnownSidType.AuthenticatedUserSid)) { try @@ -2107,42 +2293,45 @@ private void ExtractGPODelegation(string path, string GPOName) void ExtractGPODelegationAnalyzeAccessControl(string GPOName, FileSystemSecurity security, string name, bool includeInherited) { - var Owner = (SecurityIdentifier)security.GetOwner(typeof(SecurityIdentifier)); - var matchOwner = MatchesBadUsersToCheck(Owner); - if (matchOwner.HasValue) + foreach (var value in AnalyzeFileSystemSecurity(security, includeInherited)) { healthcheckData.GPODelegation.Add(new GPODelegationData() { GPOName = GPOName, Item = name, - Right = "Owner", - Account = matchOwner.Value.Value, + Right = value.Key, + Account = value.Value, } ); } + } + + List> AnalyzeFileSystemSecurity(FileSystemSecurity security, bool includeInherited) + { + var output = new List>(); + var Owner = (SecurityIdentifier)security.GetOwner(typeof(SecurityIdentifier)); + var matchOwner = MatchesBadUsersToCheck(Owner); + if (matchOwner.HasValue) + { + output.Add(new KeyValuePair("Owner",matchOwner.Value.Value)); + } var accessRules = security.GetAccessRules(true, includeInherited, typeof(SecurityIdentifier)); if (accessRules == null) - return; + return output; foreach (FileSystemAccessRule accessrule in accessRules) { if (accessrule.AccessControlType == AccessControlType.Deny) continue; - if ((FileSystemRights.Write & accessrule.FileSystemRights) != FileSystemRights.Write) + if ((FileSystemRights.Write & accessrule.FileSystemRights) == 0) continue; - var match = MatchesBadUsersToCheck((SecurityIdentifier) accessrule.IdentityReference); + var match = MatchesBadUsersToCheck((SecurityIdentifier)accessrule.IdentityReference); if (!match.HasValue) continue; - healthcheckData.GPODelegation.Add(new GPODelegationData() - { - GPOName = GPOName, - Item = name, - Right = accessrule.FileSystemRights.ToString(), - Account = match.Value.Value, - } - ); + output.Add(new KeyValuePair(accessrule.FileSystemRights.ToString(), match.Value.Value)); } + return output; } @@ -2243,7 +2432,7 @@ private void ParseGPOLoginScript(ADDomainInfo domainInfo, string path, string GP HealthcheckGPOLoginScriptData loginscript = new HealthcheckGPOLoginScriptData(); loginscript.GPOName = GPOName; loginscript.Action = "Logon"; - loginscript.Source = filename; + loginscript.Source = filename + " (" + (gpoType == "Machine" ? "Computer" : "User") + " section)"; loginscript.CommandLine = logonscript[i + "cmdline"]; loginscript.Delegation = CheckScriptPermission(domainInfo, loginscript.CommandLine); if (logonscript.ContainsKey(i + "parameters")) @@ -2264,7 +2453,7 @@ private void ParseGPOLoginScript(ADDomainInfo domainInfo, string path, string GP HealthcheckGPOLoginScriptData loginscript = new HealthcheckGPOLoginScriptData(); loginscript.GPOName = GPOName; loginscript.Action = "Logoff"; - loginscript.Source = filename; + loginscript.Source = filename + " (" + (gpoType == "Machine" ? "Computer" : "User") + " section)"; loginscript.CommandLine = logoffscript[i + "cmdline"]; loginscript.Delegation = CheckScriptPermission(domainInfo, loginscript.CommandLine); if (logoffscript.ContainsKey(i + "parameters")) @@ -2278,34 +2467,72 @@ private void ParseGPOLoginScript(ADDomainInfo domainInfo, string path, string GP } } + private void ExtractGPPFile(string path, string GPOName, ADDomainInfo domainInfo, string UserOrComputer) + { + XmlDocument doc = new XmlDocument(); + doc.Load(path); + XmlNodeList nodeList = doc.SelectNodes("/Files/File"); + foreach (XmlNode node in nodeList) + { + XmlNode action = node.SelectSingleNode("Properties/@action"); + if (action == null) + continue; + if (action.Value == "D") + continue; + XmlNode fromPath = node.SelectSingleNode("Properties/@fromPath"); + if (fromPath == null) + continue; + if (!fromPath.Value.StartsWith("\\\\")) + continue; + var file = new GPPFileDeployed(); + file.GPOName = GPOName; + file.Type = "Files (" + UserOrComputer + " section)"; + file.FileName = fromPath.Value; + file.Delegation = new List(); + healthcheckData.GPPFileDeployed.Add(file); + if (File.Exists(file.FileName)) + { + var ac = File.GetAccessControl(file.FileName); + foreach (var value in AnalyzeFileSystemSecurity(ac, true)) + { + file.Delegation.Add(new HealthcheckScriptDelegationData() + { + Account = value.Value, + Right = value.Key, + } + ); + } + } + } + } private void ExtractGPPPassword(string shortname, string fullname, string GPOName) { - string xpath = null; + string[] xpaths = null; string xpathUser = "Properties/@userName"; string xpathNewName = null; switch (shortname) { case "groups.xml": - xpath = "/Groups/User"; + xpaths = new string[] {"/Groups/User"}; xpathNewName = "Properties/@newName"; break; case "services.xml": - xpath = "/NTServices/NTService"; + xpaths = new string[] {"/NTServices/NTService"}; xpathUser = "Properties/@accountName"; break; case "scheduledtasks.xml": - xpath = "/ScheduledTasks/Task"; + xpaths = new string[] { "/ScheduledTasks/Task", "/ScheduledTasks/ImmediateTask", "/ScheduledTasks/TaskV2", "/ScheduledTasks/ImmediateTaskV2" }; xpathUser = "Properties/@runAs"; break; case "datasources.xml": - xpath = "/DataSources/DataSource"; + xpaths = new string[] {"/DataSources/DataSource"}; break; case "printers.xml": - xpath = "/Printers/SharedPrinter"; + xpaths = new string[] {"/Printers/SharedPrinter"}; break; case "drives.xml": - xpath = "/Drives/Drive"; + xpaths = new string[] {"/Drives/Drive"}; break; default: return; @@ -2313,50 +2540,53 @@ private void ExtractGPPPassword(string shortname, string fullname, string GPONam XmlDocument doc = new XmlDocument(); doc.Load(fullname); - XmlNodeList nodeList = doc.SelectNodes(xpath); - foreach (XmlNode node in nodeList) + foreach (string xpath in xpaths) { - XmlNode password = node.SelectSingleNode("Properties/@cpassword"); - // no password - if (password == null) - continue; - // password has been manually changed - if (String.IsNullOrEmpty(password.Value)) - continue; - GPPPassword PasswordData = new GPPPassword(); - PasswordData.GPOName = GPOName; - PasswordData.Password = DecodeGPPPassword(password.Value); + XmlNodeList nodeList = doc.SelectNodes(xpath); + foreach (XmlNode node in nodeList) + { + XmlNode password = node.SelectSingleNode("Properties/@cpassword"); + // no password + if (password == null) + continue; + // password has been manually changed + if (String.IsNullOrEmpty(password.Value)) + continue; + GPPPassword PasswordData = new GPPPassword(); + PasswordData.GPOName = GPOName; + PasswordData.Password = DecodeGPPPassword(password.Value); - XmlNode userNameNode = node.SelectSingleNode(xpathUser); - PasswordData.UserName = (userNameNode != null ? userNameNode.Value : string.Empty); + XmlNode userNameNode = node.SelectSingleNode(xpathUser); + PasswordData.UserName = (userNameNode != null ? userNameNode.Value : string.Empty); - XmlNode changed = node.SelectSingleNode("@changed"); - if (changed != null) - { - PasswordData.Changed = DateTime.Parse(changed.Value); - } - else - { - FileInfo fi = new FileInfo(fullname); - PasswordData.Changed = fi.LastWriteTime; - } - if (xpathNewName != null) - { - XmlNode newNameNode = node.SelectSingleNode(xpathNewName); - if (newNameNode != null && !String.IsNullOrEmpty(newNameNode.Value)) + XmlNode changed = node.SelectSingleNode("@changed"); + if (changed != null) { - PasswordData.Other = "NewName:" + newNameNode.Value; + PasswordData.Changed = DateTime.Parse(changed.Value); + } + else + { + FileInfo fi = new FileInfo(fullname); + PasswordData.Changed = fi.LastWriteTime; + } + if (xpathNewName != null) + { + XmlNode newNameNode = node.SelectSingleNode(xpathNewName); + if (newNameNode != null && !String.IsNullOrEmpty(newNameNode.Value)) + { + PasswordData.Other = "NewName:" + newNameNode.Value; + } + } + XmlNode pathNode = node.SelectSingleNode("Properties/@path"); + if (pathNode != null && !String.IsNullOrEmpty(pathNode.Value)) + { + PasswordData.Other = "Path:" + pathNode.Value; + } + PasswordData.Type = shortname; + lock (healthcheckData.GPPPassword) + { + healthcheckData.GPPPassword.Add(PasswordData); } - } - XmlNode pathNode = node.SelectSingleNode("Properties/@path"); - if (pathNode != null && !String.IsNullOrEmpty(pathNode.Value)) - { - PasswordData.Other = "Path:" + pathNode.Value; - } - PasswordData.Type = shortname; - lock (healthcheckData.GPPPassword) - { - healthcheckData.GPPPassword.Add(PasswordData); } } } @@ -2409,12 +2639,15 @@ private string DecodeGPPPassword(string encryptedPassword) private void ExtractGPPPrivilegePasswordLsaSettingEtc(string filename, string GPOName, ADDomainInfo domainInfo) { - using (StreamReader file = new System.IO.StreamReader(filename)) + bool isDCGPO = filename.Contains("\\{6AC1786C-016F-11D2-945F-00C04fB984F9}\\"); + using (StreamReader file = new System.IO.StreamReader(filename)) { string line; while ((line = file.ReadLine()) != null) { SubExtractPrivilege(line, GPOName, domainInfo); + if (isDCGPO) + SubExtractDCGPOPrivilege(line, GPOName, domainInfo); SubExtractLsaSettings(line, GPOName); SubExtractLsaSettingsBis(line, GPOName); SubExtractPasswordSettings(line, GPOName); @@ -2462,10 +2695,18 @@ private void SubExtractGroupMembership(string line, string GPOName, ADDomainInfo { user1 = "Everyone"; } + else if (user1 == "*S-1-5-7") + { + user1 = "Anonymous"; + } else if (user1 == "*S-1-5-11") { user1 = "Authenticated Users"; } + else if (user1 == "*S-1-5-32-545") + { + user1 = "Users"; + } else if (user1.StartsWith("*S-1", StringComparison.InvariantCultureIgnoreCase)) { user1 = NativeMethods.ConvertSIDToName(user1.Substring(1), domainInfo.DnsHostName); @@ -2475,10 +2716,18 @@ private void SubExtractGroupMembership(string line, string GPOName, ADDomainInfo { user2 = "Everyone"; } + else if (user2 == "*S-1-5-7") + { + user2 = "Anonymous"; + } else if (user2 == "*S-1-5-11") { user2 = "Authenticated Users"; } + else if (user1 == "*S-1-5-32-545") + { + user1 = "Users"; + } else if (user2.StartsWith("*S-1", StringComparison.InvariantCultureIgnoreCase)) { user2 = NativeMethods.ConvertSIDToName(user2.Substring(1), domainInfo.DnsHostName); @@ -2594,33 +2843,54 @@ private void SubExtractLsaSettings(string line, string GPOName) continue; if (lsasetting == "RestrictAnonymousSAM" && value == 1) continue; - lock (healthcheckData.GPOLsaPolicy) - { - GPPSecurityPolicy PSO = null; - foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) - { - if (policy.GPOName == GPOName) - { - PSO = policy; - break; - } - } - if (PSO == null) - { - PSO = new GPPSecurityPolicy(); - PSO.GPOName = GPOName; - healthcheckData.GPOLsaPolicy.Add(PSO); - PSO.Properties = new List(); - } - PSO.Properties.Add(new GPPSecurityPolicyProperty(lsasetting, value)); - } + AddGPOLsaPolicy(GPOName, lsasetting, value); } } } } } + else if (line.StartsWith(@"MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Setup\RecoveryConsole\SecurityLevel=4,1", StringComparison.InvariantCultureIgnoreCase)) + { + AddGPOLsaPolicy(GPOName, "recoveryconsole_securitylevel", 1); + } + else if (line.StartsWith(@"MACHINE\System\CurrentControlSet\Services\LDAP\LDAPClientIntegrity=4,0", StringComparison.InvariantCultureIgnoreCase)) + { + AddGPOLsaPolicy(GPOName, "LDAPClientIntegrity", 0); + } + else if (line.StartsWith(@"MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange=4,1", StringComparison.InvariantCultureIgnoreCase)) + { + AddGPOLsaPolicy(GPOName, "RefusePasswordChange", 1); + } + else if (line.StartsWith(@"MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\EnableSecuritySignature=4,0", StringComparison.InvariantCultureIgnoreCase)) + { + AddGPOLsaPolicy(GPOName, "EnableSecuritySignature", 0); + } } + private void AddGPOLsaPolicy(string GPOName, string setting, int value) + { + lock (healthcheckData.GPOLsaPolicy) + { + GPPSecurityPolicy PSO = null; + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + if (policy.GPOName == GPOName) + { + PSO = policy; + break; + } + } + if (PSO == null) + { + PSO = new GPPSecurityPolicy(); + PSO.GPOName = GPOName; + healthcheckData.GPOLsaPolicy.Add(PSO); + PSO.Properties = new List(); + } + PSO.Properties.Add(new GPPSecurityPolicyProperty(setting, value)); + } + } + private void SubExtractLsaSettingsBis(string line, string GPOName) { @@ -2665,8 +2935,8 @@ private void SubExtractLsaSettingsBis(string line, string GPOName) } } - private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domainInfo) - { + private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domainInfo) + { string[] privileges = new string[] { "SeBackupPrivilege", "SeCreateTokenPrivilege", @@ -2681,87 +2951,140 @@ private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domai "SeRestorePrivilege", "SeImpersonatePrivilege", "SeAssignPrimaryTokenPrivilege", - }; - foreach (string privilege in privileges) - { - if (line.StartsWith(privilege, StringComparison.InvariantCultureIgnoreCase)) - { - int pos = line.IndexOf('=') + 1; - if (pos > 1) - { - string value = line.Substring(pos).Trim(); - string[] values = value.Split(','); - foreach (string user in values) - { - // ignore empty privilege assignment - if (String.IsNullOrEmpty(user)) - continue; - // ignore well known sid - // - if (user.StartsWith("*S-1-5-32-", StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - // Local system - if (user.StartsWith("*S-1-5-18", StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - // SERVICE - if (user.StartsWith("*S-1-5-6", StringComparison.InvariantCultureIgnoreCase)) - { + }; + foreach (string privilege in privileges) + { + if (line.StartsWith(privilege, StringComparison.InvariantCultureIgnoreCase)) + { + int pos = line.IndexOf('=') + 1; + if (pos > 1) + { + string value = line.Substring(pos).Trim(); + string[] values = value.Split(','); + foreach (string user in values) + { + var user2 = ConvertGPOUserToUserFriendlyUser(user, domainInfo); + // ignore empty privilege assignment + if (String.IsNullOrEmpty(user2)) continue; - } - // LOCAL_SERVICE - if (user.StartsWith("*S-1-5-19", StringComparison.InvariantCultureIgnoreCase)) + + GPPRightAssignment right = new GPPRightAssignment(); + lock (healthcheckData.GPPRightAssignment) { - continue; + healthcheckData.GPPRightAssignment.Add(right); } - // NETWORK_SERVICE - if (user.StartsWith("*S-1-5-20", StringComparison.InvariantCultureIgnoreCase)) - { + right.GPOName = GPOName; + right.Privilege = privilege; + right.User = user2; + } + + } + } + } + } + + private void SubExtractDCGPOPrivilege(string line, string GPOName, ADDomainInfo domainInfo) + { + string[] privileges = new string[] { + "SeInteractiveLogonRight", + "SeRemoteInteractiveLogonRight", + }; + foreach (string privilege in privileges) + { + if (line.StartsWith(privilege, StringComparison.InvariantCultureIgnoreCase)) + { + int pos = line.IndexOf('=') + 1; + if (pos > 1) + { + string value = line.Substring(pos).Trim(); + string[] values = value.Split(','); + foreach (string user in values) + { + var user2 = ConvertGPOUserToUserFriendlyUser(user, domainInfo); + // ignore empty privilege assignment + if (String.IsNullOrEmpty(user2)) continue; + + GPPRightAssignment right = new GPPRightAssignment(); + lock (healthcheckData.GPPRightAssignment) + { + healthcheckData.GPPRightAssignment.Add(right); } - GPPRightAssignment right = new GPPRightAssignment(); - lock (healthcheckData.GPPRightAssignment) - { - healthcheckData.GPPRightAssignment.Add(right); - } - right.GPOName = GPOName; - right.Privilege = privilege; - if (user == "*S-1-1-0") - { - right.User = "Everyone"; - } - else if (user == "*S-1-5-11") - { - right.User = "Authenticated Users"; - } - else if (user.StartsWith("*S-1", StringComparison.InvariantCultureIgnoreCase)) - { - if (user.EndsWith("-513")) - { - right.User = "Domain Users"; - } - else if (user.EndsWith("-515")) - { - right.User = "Domain Computers"; - } - else - { - right.User = NativeMethods.ConvertSIDToName(user.Substring(1), domainInfo.DnsHostName); - } - } - else - { - right.User = user; - } - } + right.GPOName = GPOName; + right.Privilege = privilege + " on DC"; + right.User = user2; + } - } - } - } - } + } + } + } + } + + private string ConvertGPOUserToUserFriendlyUser(string user, ADDomainInfo domainInfo) + { + // ignore well known sid + // + if (user.StartsWith("*S-1-5-32-", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + // Local system + if (user.StartsWith("*S-1-5-18", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + // SERVICE + if (user.StartsWith("*S-1-5-6", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + // LOCAL_SERVICE + if (user.StartsWith("*S-1-5-19", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + // NETWORK_SERVICE + if (user.StartsWith("*S-1-5-20", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + + if (user == "*S-1-1-0") + { + return "Everyone"; + } + else if (user == "*S-1-5-7") + { + return "Anonymous"; + } + else if (user == "*S-1-5-11") + { + return "Authenticated Users"; + } + else if (user == "*S-1-5-32-545") + { + return "Users"; + } + else if (user.StartsWith("*S-1", StringComparison.InvariantCultureIgnoreCase)) + { + if (user.EndsWith("-513")) + { + return "Domain Users"; + } + else if (user.EndsWith("-515")) + { + return "Domain Computers"; + } + else + { + return NativeMethods.ConvertSIDToName(user.Substring(1), domainInfo.DnsHostName); + } + } + else + { + return user; + } + } private void GeneratePSOData(ADDomainInfo domainInfo, ADWebService adws, NetworkCredential credential) { @@ -2801,8 +3124,8 @@ private void GeneratePSOData(ADDomainInfo domainInfo, ADWebService adws, Network PSO.Properties.Add(new GPPSecurityPolicyProperty("PasswordComplexity", 0)); PSO.Properties.Add(new GPPSecurityPolicyProperty("PasswordHistorySize", x.msDSPasswordHistoryLength)); PSO.Properties.Add(new GPPSecurityPolicyProperty("LockoutBadCount", x.msDSLockoutThreshold)); - PSO.Properties.Add(new GPPSecurityPolicyProperty("ResetLockoutCount", (int)(x.msDSLockoutObservationWindow / -3000000000))); - PSO.Properties.Add(new GPPSecurityPolicyProperty("LockoutDuration", (int)(x.msDSLockoutDuration / -3000000000))); + PSO.Properties.Add(new GPPSecurityPolicyProperty("ResetLockoutCount", (int)(x.msDSLockoutObservationWindow / -600000000))); + PSO.Properties.Add(new GPPSecurityPolicyProperty("LockoutDuration", (int)(x.msDSLockoutDuration / -600000000))); if (x.msDSPasswordReversibleEncryptionEnabled) PSO.Properties.Add(new GPPSecurityPolicyProperty("ClearTextPassword", 1)); else @@ -2829,11 +3152,16 @@ private void GenerateAnomalies(ADDomainInfo domainInfo, ADWebService adws) } , "Base"); - string[] propertieskrbtgt = new string[] { "distinguishedName", "replPropertyMetaData" }; + string[] propertieskrbtgt = new string[] { "distinguishedName", "replPropertyMetaData", "pwdLastSet" }; adws.Enumerate(domainInfo.DefaultNamingContext, "(objectSid=" + ADConnection.EncodeSidToString(domainInfo.DomainSid.Value + "-502") + ")", propertieskrbtgt, (ADItem aditem) => { + Trace.WriteLine("krbtgt found"); healthcheckData.KrbtgtLastVersion = aditem.ReplPropertyMetaData[0x9005A].Version; - + healthcheckData.KrbtgtLastChangeDate = aditem.PwdLastSet; + if (healthcheckData.KrbtgtLastChangeDate < aditem.ReplPropertyMetaData[0x9005A].LastOriginatingChange) + { + healthcheckData.KrbtgtLastChangeDate = aditem.ReplPropertyMetaData[0x9005A].LastOriginatingChange; + } } ); @@ -2925,12 +3253,20 @@ private void GenerateAnomalies(ADDomainInfo domainInfo, ADWebService adws) WorkOnReturnedObjectByADWS callbackdSHeuristics = (ADItem x) => { - if (!String.IsNullOrEmpty(x.DSHeuristics) && x.DSHeuristics.Length >= 7) + if (!String.IsNullOrEmpty(x.DSHeuristics)) { - if (x.DSHeuristics.Substring(6, 1) == "2") + if (x.DSHeuristics.Length >= 7 && x.DSHeuristics.Substring(6, 1) == "2") { healthcheckData.DsHeuristicsAnonymousAccess = true; } + if (x.DSHeuristics.Length >= 16 && x.DSHeuristics.Substring(15, 1) != "0") + { + healthcheckData.DsHeuristicsAdminSDExMaskModified = true; + } + if (x.DSHeuristics.Length >= 3 && x.DSHeuristics.Substring(2, 1) != "0") + { + healthcheckData.DsHeuristicsDoListObject = true; + } } }; adws.Enumerate(domainInfo.ConfigurationNamingContext, "(distinguishedName=CN=Directory Service,CN=Windows NT,CN=Services," + domainInfo.ConfigurationNamingContext + ")", DsHeuristicsproperties, callbackdSHeuristics); @@ -2976,6 +3312,33 @@ private void GenerateAnomalies(ADDomainInfo domainInfo, ADWebService adws) { adws.Enumerate(domainInfo.DefaultNamingContext, "(distinguishedName=" + ADConnection.EscapeLDAP(DC.DistinguishedName) + ")", DCProperties, callbackDomainControllers); } + + string[] ExchangePrivEscProperties = new string[] { + "distinguishedName", + "nTSecurityDescriptor", + }; + WorkOnReturnedObjectByADWS callbackExchangePrivEscProperties = + (ADItem x) => + { + if (x.NTSecurityDescriptor != null) + { + foreach (ActiveDirectoryAccessRule rule in x.NTSecurityDescriptor.GetAccessRules(true, false, typeof(SecurityIdentifier))) + { + if (((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) != 0) + && (rule.ObjectType == new Guid("00000000-0000-0000-0000-000000000000")) + && rule.PropagationFlags == PropagationFlags.None) + { + string principal = NativeMethods.ConvertSIDToName(rule.IdentityReference.Value, domainInfo.DnsHostName); + if (principal.EndsWith("\\Exchange Windows Permissions")) + { + + healthcheckData.ExchangePrivEscVulnerable = true; + } + } + } + } + }; + adws.Enumerate(domainInfo.DefaultNamingContext, "(objectClass=*)", ExchangePrivEscProperties, callbackExchangePrivEscProperties, "Base"); } private void GenerateDomainControllerData(ADDomainInfo domainInfo) @@ -3042,7 +3405,10 @@ private void GenerateDomainControllerData(ADDomainInfo domainInfo) DC.SupportSMB2OrSMB3 = true; } DC.SMB2SecurityMode = securityMode; - DC.RemoteSpoolerDetected = SpoolerScanner.CheckIfTheSpoolerIsActive(dns); + if (!SkipNullSession) + { + DC.RemoteSpoolerDetected = SpoolerScanner.CheckIfTheSpoolerIsActive(dns); + } } }; @@ -3129,5 +3495,75 @@ private void GenerateNetworkData(ADDomainInfo domainInfo, ADWebService adws) adws.Enumerate(domainInfo.ConfigurationNamingContext, "(objectClass=site)", properties, callback); } + + // this function has been designed to avoid LDAP query reentrance (to avoid the 5 connection limit) + private void GenerateFSMOData(ADDomainInfo domainInfo, ADWebService adws) + { + //query the NTDS objects + string[] properties = new string[] { + "distinguishedName", + "fSMORoleOwner", + }; + + var computerToQuery = new Dictionary(); + string role = null; + WorkOnReturnedObjectByADWS callback = + (ADItem x) => + { + string DN = x.fSMORoleOwner; + if (DN.Contains("\0DEL:")) + { + Trace.WriteLine(DN + " FSMO Warning !"); + } + string parent = DN.Substring(DN.IndexOf(",") + 1); + computerToQuery.Add(role, parent); + }; + role = "PDC"; + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=domainDNS)(fSMORoleOwner=*))", properties, callback); + role = "RID pool manager"; + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=rIDManager)(fSMORoleOwner=*))", properties, callback); + role = "Infrastructure master"; + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=infrastructureUpdate)(fSMORoleOwner=*))", properties, callback); + role = "Schema master"; + adws.Enumerate(domainInfo.SchemaNamingContext, "(&(objectClass=dMD)(fSMORoleOwner=*))", properties, callback); + role = "Domain naming Master"; + adws.Enumerate(domainInfo.ConfigurationNamingContext, "(&(objectClass=crossRefContainer)(fSMORoleOwner=*))", properties, callback); + + + foreach (var computerRole in computerToQuery.Keys) + { + string dns = null; + WorkOnReturnedObjectByADWS computerCallback = + (ADItem x) => + { + dns = x.DNSHostName; + }; + adws.Enumerate(domainInfo.ConfigurationNamingContext, "(distinguishedName=" + ADConnection.EscapeLDAP(computerToQuery[computerRole]) + ")", new string[] { "dnsHostName" }, computerCallback); + + if (string.IsNullOrEmpty(dns)) + { + Trace.WriteLine("Unable to get DNSHostName for " + computerToQuery[computerRole]); + continue; + } + HealthcheckDomainController theDC = null; + foreach (var DC in healthcheckData.DomainControllers) + { + if (string.Equals(DC.DCName + "." + domainInfo.DomainName,dns, StringComparison.OrdinalIgnoreCase)) + { + theDC = DC; + break; + } + } + if (theDC == null) + { + Trace.WriteLine("Unable to get DC for " + dns); + continue; + } + if (theDC.FSMO == null) + theDC.FSMO = new List(); + theDC.FSMO.Add(computerRole); + } + } + } } diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs index bff7336..726475f 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs @@ -13,6 +13,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-AnonymousAuthorizedGPO", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-14798", "Directory data (outside the root DSE) of a non-public directory must be configured to prevent anonymous access.", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyAnonymousAuthorizedGPO : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs index 2c02cf0..9bbf992 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD2IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD2Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs index 11d1076..66cdb06 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD2RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD2Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs index 8e0e7e8..81d8d35 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD4IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD4Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs index 06b5c20..8e71efa 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD4RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD4Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs index 5efc3c1..0ec18e6 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD5IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD5Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs index 359cd56..474977d 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-MD5RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertMD5Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs index 7110943..acfe117 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-SHA0IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertSHA0Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs index 3e89402..9666e2c 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-SHA0RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertSHA0Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs index 619abf1..ac88c9b 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-SHA1IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 1)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertSHA1Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs index 00a326d..0cd9f68 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-SHA1RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertSHA1Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs index 73ccd3a..a6213d3 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs @@ -16,6 +16,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-WeakRSARootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-14820", "PKI certificates (server and clients) must be issued by the DoD PKI or an approved External Certificate Authority (ECA).", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyCertWeakRSA : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyDCRefuseComputerPwdChange.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCRefuseComputerPwdChange.cs new file mode 100644 index 0000000..4ebc030 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCRefuseComputerPwdChange.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("A-DCRefuseComputerPwdChange", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleIntroducedIn(2, 7)] + [RuleSTIG("V-4408", "The domain controller must be configured to allow reset of machine account passwords.", STIGFramework.ActiveDirectoryService2008)] + public class HeatlcheckRuleAnomalyDCRefuseComputerPwdChange : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + if (property.Property == "RefusePasswordChange") + { + if (property.Value == 1) + { + AddRawDetail(policy.GPOName); + break; + } + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs index 1302ea2..b6ae5eb 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs @@ -13,7 +13,8 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-DC-Spooler", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyDCSpooler : RuleBase + [RuleIntroducedIn(2, 6)] + public class HeatlcheckRuleAnomalyDCSpooler : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs index 0ff1bca..44d5509 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs @@ -13,7 +13,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-DsHeuristicsAnonymous", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] - [RuleSTIG("V-8555", "Anonymous Access to AD forest data above the rootDSE level must be disabled. ", true)] + [RuleSTIG("V-8555", "Anonymous Access to AD forest data above the rootDSE level must be disabled. ", STIGFramework.Forest)] public class HeatlcheckRuleAnomalyDsHeuristicsAnonymous : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyLDAPSigningDisabled.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyLDAPSigningDisabled.cs new file mode 100644 index 0000000..9eb8fd2 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyLDAPSigningDisabled.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("A-LDAPSigningDisabled", RiskRuleCategory.Anomalies, RiskModelCategory.NetworkSniffing)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleBSI("M 2.412")] + [RuleCERTFR("CERTFR-2015-ACT-021", "SECTION00010000000000000000")] + [RuleSTIG("V-3381", "The Recovery Console option is set to permit automatic logon to the system.", STIGFramework.Windows2008)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRuleAnomalyLDAPSigningDisabled : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + if (property.Property == "LDAPClientIntegrity") + { + if (property.Value == 0) + { + AddRawDetail(policy.GPOName); + break; + } + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs index 399070e..66fe318 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs @@ -15,11 +15,12 @@ namespace PingCastle.Healthcheck.Rules [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] [RuleANSSI("R37", "paragraph.3.6.2.1")] [RuleBSI("M 2.412")] + [RuleSTIG("V-3379", "The system is configured to store the LAN Manager hash of the password in the SAM.", STIGFramework.Windows2008)] public class HeatlcheckRuleAnomalyLMHash : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { - foreach (GPPSecurityPolicy policy in healthcheckData.GPPPasswordPolicy) + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) { foreach (GPPSecurityPolicyProperty property in policy.Properties) { @@ -27,7 +28,7 @@ public class HeatlcheckRuleAnomalyLMHash : RuleBase { if (property.Value == 0) { - AddRawDetail(policy.GPOName); + AddRawDetail(policy.GPOName, property.Property); break; } } diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs index 958ff68..5aaac6f 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs @@ -19,7 +19,7 @@ public class HeatlcheckRuleAnomalyMembershipEveryone : RuleBase { foreach (GPOMembership membership in healthcheckData.GPOLocalMembership) { - if (membership.User == "Authenticated Users" || membership.User == "Everyone") + if (membership.User == "Authenticated Users" || membership.User == "Everyone" || membership.User == "Users" || membership.User == "Anonymous") { AddRawDetail(membership.GPOName, membership.MemberOf, membership.User); } diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyNoGPOLLMNR.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyNoGPOLLMNR.cs new file mode 100644 index 0000000..eb2f10a --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyNoGPOLLMNR.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("A-NoGPOLLMNR", RiskRuleCategory.Anomalies, RiskModelCategory.NetworkSniffing)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRuleAnomalyNoGPOLLMNR : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + bool found = false; + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + if (property.Property == "EnableMulticast") + { + found = true; + if (property.Value == 1) + { + AddRawDetail(policy.GPOName); + } + } + } + } + if (!found) + return 1; + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs index 17f84e0..dda2aff 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs @@ -13,6 +13,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("A-NotEnoughDC", RiskRuleCategory.Anomalies, RiskModelCategory.Backup)] [RuleComputation(RuleComputationType.TriggerIfLessThan, 5, Threshold: 2)] + [RuleIntroducedIn(2, 6)] public class HeatlcheckRuleAnomalyNotEnoughDC : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs index 17653f3..55eaddc 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("A-NullSession", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] [RuleBSI("M 2.412")] + [RuleSTIG("V-14798", "Directory data (outside the root DSE) of a non-public directory must be configured to prevent anonymous access.", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRuleAnomalyNullSession : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs index d2ab0ea..43f3581 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs @@ -15,7 +15,8 @@ namespace PingCastle.Healthcheck.Rules [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] [RuleBSI("M 2.412")] [RuleCERTFR("CERTFR-2015-ACT-021", "SECTION00010000000000000000")] - public class HeatlcheckRuleAnomalySMB2SignatureNotEnabled : RuleBase + [RuleIntroducedIn(2, 5)] + public class HeatlcheckRuleAnomalySMB2SignatureNotEnabled : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs index 1812f22..cff6b23 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs @@ -15,7 +15,8 @@ namespace PingCastle.Healthcheck.Rules [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] [RuleBSI("M 2.412")] [RuleCERTFR("CERTFR-2015-ACT-021", "SECTION00010000000000000000")] - public class HeatlcheckRuleAnomalySMB2SignatureNotRequired : RuleBase + [RuleIntroducedIn(2, 5)] + public class HeatlcheckRuleAnomalySMB2SignatureNotRequired : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs index 03bb008..c22171e 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs @@ -11,7 +11,7 @@ namespace PingCastle.Healthcheck.Rules { - [RuleModel("P-Delegated", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleModel("P-Delegated", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AccountTakeOver)] [RuleComputation(RuleComputationType.TriggerOnPresence, 20)] [RuleSTIG("V-36435", "Delegation of privileged accounts must be prohibited.")] public class HeatlcheckRulePrivilegedDelegated : RuleBase diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs index f948719..4b7e68a 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs @@ -14,13 +14,16 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-DelegationEveryone", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.PerDiscover, 15)] [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleSTIG("V-2370", "The access control permissions for the directory service site group policy must be configured to use the required access permissions.", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRulePrivilegedDelegationEveryone : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { foreach (HealthcheckDelegationData delegation in healthcheckData.Delegations) { - if (delegation.Account == "Authenticated Users" || delegation.Account == "Everyone" || delegation.Account == "Domain Users" || delegation.Account == "Domain Computers") + if (delegation.Account == "Authenticated Users" || delegation.Account == "Everyone" || delegation.Account == "Domain Users" || delegation.Account == "Domain Computers" + || delegation.SecurityIdentifier == "S-1-5-32-545" || delegation.SecurityIdentifier.EndsWith("-513") || delegation.SecurityIdentifier.EndsWith("-515") + || delegation.Account == "Anonymous") { AddRawDetail(delegation.DistinguishedName, delegation.Account, delegation.Right); } diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationFileDeployed.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationFileDeployed.cs new file mode 100644 index 0000000..35658ff --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationFileDeployed.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-DelegationFileDeployed", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 5)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleSTIG("V-2370", "The access control permissions for the directory service site group policy must be configured to use the required access permissions.", STIGFramework.ActiveDirectoryService2003)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRulePrivilegedDelegationFileDeployed : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (var file in healthcheckData.GPPFileDeployed) + { + if (file.Delegation != null) + { + foreach (var delegation in file.Delegation) + { + AddRawDetail(file.GPOName, file.Type, file.FileName, delegation.Account, delegation.Right); + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs index 735eaa1..192172d 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs @@ -14,6 +14,8 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-DelegationGPOData", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.PerDiscover, 15)] [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleSTIG("V-2370", "The access control permissions for the directory service site group policy must be configured to use the required access permissions.", STIGFramework.ActiveDirectoryService2003)] + [RuleIntroducedIn(2, 6)] public class HeatlcheckRulePrivilegedDelegationGPOData : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs index 014d579..2bba022 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs @@ -14,7 +14,8 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-DelegationKeyAdmin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] [RuleANSSI("R18", "subsubsection.3.3.2")] - public class HeatlcheckRulePrivilegedDelegationKeyAdmin : RuleBase + [RuleIntroducedIn(2, 6)] + public class HeatlcheckRulePrivilegedDelegationKeyAdmin : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs index 47765f0..4f06b02 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs @@ -14,6 +14,8 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-DelegationLoginScript", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.PerDiscover, 15)] [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleSTIG("V-2370", "The access control permissions for the directory service site group policy must be configured to use the required access permissions.", STIGFramework.ActiveDirectoryService2003)] + [RuleIntroducedIn(2, 5)] public class HeatlcheckRulePrivilegedDelegationLoginScript : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsAdminSDExMaskModified.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsAdminSDExMaskModified.cs new file mode 100644 index 0000000..5f7ceb6 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsAdminSDExMaskModified.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-DsHeuristicsAdminSDExMask", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleIntroducedIn(2,7)] + public class HeatlcheckRulePrivilegedDsHeuristicsAdminSDExMaskModified : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + if (healthcheckData.DsHeuristicsAdminSDExMaskModified) + { + return 1; + } + return 0; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsDoListObject.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsDoListObject.cs new file mode 100644 index 0000000..16cc6bd --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDsHeuristicsDoListObject.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-DsHeuristicsDoListObject", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleIntroducedIn(2,7)] + public class HeatlcheckRulePrivilegedDsHeuristicsDoListObject : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + if (healthcheckData.DsHeuristicsDoListObject) + { + return 1; + } + return 0; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs index a7df5b4..4eeb4c6 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs @@ -14,7 +14,8 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-ExchangeAdminSDHolder", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] [RuleANSSI("R18", "subsubsection.3.3.2")] - public class HeatlcheckRulePrivilegedExchangeAdminSDHolder : RuleBase + [RuleIntroducedIn(2, 6)] + public class HeatlcheckRulePrivilegedExchangeAdminSDHolder : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangePrivEsc.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangePrivEsc.cs new file mode 100644 index 0000000..e6a74e3 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangePrivEsc.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-ExchangePrivEsc", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleIntroducedIn(2,7)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedExchangePrivEsc : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + if (healthcheckData.ExchangePrivEscVulnerable) + return 1; + return 0; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedKerberoasting.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedKerberoasting.cs new file mode 100644 index 0000000..c4bebed --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedKerberoasting.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-Kerberoasting", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AccountTakeOver)] + [RuleComputation(RuleComputationType.PerDiscover, 5)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRulePrivilegedKerberoasting : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + var dangerousGroups = new List() { + "Domain Admins", + "Enterprise Admins", + "Schema Admins", + "Administrators", + }; + foreach (var group in healthcheckData.PrivilegedGroups) + { + if (!dangerousGroups.Contains(group.GroupName)) + { + continue; + } + foreach (var user in group.Members) + { + if (user.IsService && user.PwdLastSet.AddDays(40) < DateTime.Now) + { + AddRawDetail(group.GroupName, user.Name); + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedLoginDCEveryone.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedLoginDCEveryone.cs new file mode 100644 index 0000000..d6bad11 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedLoginDCEveryone.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-LoginDCEveryone", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRulePrivilegedLoginDCEveryone : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + var dangerousPrivileges = new List() + { + "SeInteractiveLogonRight on DC", + "SeRemoteInteractiveLogonRight on DC", + }; + foreach (var privilege in healthcheckData.GPPRightAssignment) + { + if (!dangerousPrivileges.Contains(privilege.Privilege)) + continue; + if (privilege.User == "Authenticated Users" || privilege.User == "Everyone" || privilege.User == "Domain Users" + || privilege.User == "Domain Computers" || privilege.User == "Users" + || privilege.User == "Anonymous") + { + AddRawDetail(privilege.GPOName, privilege.User, privilege.Privilege); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs index 0677543..a4816bb 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs @@ -14,6 +14,7 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-PrivilegeEveryone", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.PerDiscover, 15)] [RuleANSSI("R18", "subsubsection.3.3.2")] + [RuleIntroducedIn(2, 6)] public class HeatlcheckRulePrivilegedPrivilegeEveryone : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) @@ -34,7 +35,9 @@ public class HeatlcheckRulePrivilegedPrivilegeEveryone : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (GPPSecurityPolicy policy in healthcheckData.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + if (property.Property == "recoveryconsole_securitylevel") + { + if (property.Value > 0) + { + AddRawDetail(policy.GPOName); + break; + } + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedRecycleBin.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedRecycleBin.cs new file mode 100644 index 0000000..1113f3f --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedRecycleBin.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-RecycleBin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.IrreversibleChange)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRulePrivilegedRecycleBin : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + if (healthcheckData.IsRecycleBinEnabled) + { + return 0; + } + return 1; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs index 7e2d241..5058436 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs @@ -13,7 +13,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("P-SchemaAdmin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.IrreversibleChange)] [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] - [RuleSTIG("V-72835", "Membership to the Schema Admins group must be limited", true)] + [RuleSTIG("V-72835", "Membership to the Schema Admins group must be limited", STIGFramework.Forest)] [RuleANSSI("R13", "subsection.3.2")] public class HeatlcheckRulePrivilegedSchemaAdmins : RuleBase { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs index e4e22a6..5184355 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs @@ -14,7 +14,8 @@ namespace PingCastle.Healthcheck.Rules [RuleModel("P-UnconstrainedDelegation", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.PerDiscover, 5)] [RuleANSSI("R18", "subsubsection.3.3.2")] - public class HeatlcheckRulePrivilegedUnconstrainedDelegation : RuleBase + [RuleIntroducedIn(2, 6)] + public class HeatlcheckRulePrivilegedUnconstrainedDelegation : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs index 1fe4bb5..86af331 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs @@ -13,6 +13,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("P-UnkownDelegation", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + [RuleSTIG("V-2370", "The access control permissions for the directory service site group policy must be configured to use the required access permissions.", STIGFramework.ActiveDirectoryService2003)] public class HeatlcheckRulePrivilegedUnknownDelegation : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) diff --git a/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs b/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs index 826617e..b89f760 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs @@ -25,7 +25,10 @@ public class HeatlcheckRuleStaleADRegistrationEnabled : RuleBase + [RuleIntroducedIn(2, 5)] + public class HeatlcheckRuleStaledDCSubnetMissing : RuleBase { - private class Subnet - { - private int _mask; - private byte[] _startAddress; - - public Subnet(IPAddress startAddress, int mask) - { - _mask = mask; - _startAddress = startAddress.GetAddressBytes(); - ApplyBitMask(_startAddress); - } - - private void ApplyBitMask(byte[] address) - { - int remainingMask = _mask; - for (int i = 0; i < address.Length; i++) - { - if (remainingMask >= 8) - { - remainingMask -= 8; - continue; - } - if (remainingMask == 0) - { - address[i] = 0; - continue; - } - address[i] = (byte) (address[i] & (0xFF00 >> remainingMask)); - remainingMask = 0; - } - } - - public bool MatchIp(IPAddress ipaddress) - { - byte[] ipAddressBytes = ipaddress.GetAddressBytes(); - if (ipAddressBytes.Length != _startAddress.Length) - return false; - ApplyBitMask(ipAddressBytes); - for (int i = 0; i < _startAddress.Length; i++) - { - if (ipAddressBytes[i] != _startAddress[i]) return false; - } - return true; - } - } - protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { var subnets = new List(); diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs b/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs index 5fac709..f100421 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs @@ -12,7 +12,7 @@ namespace PingCastle.Healthcheck.Rules { [RuleModel("S-SMB-v1", RiskRuleCategory.StaleObjects, RiskModelCategory.OldAuthenticationProtocols)] - [RuleComputation(RuleComputationType.TriggerOnPresence, 1)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] [RuleBSI("M 2.412")] [RuleCERTFR("CERTFR-2017-ACT-019", "SECTION00010000000000000000")] [RuleCERTFR("CERTFR-2016-ACT-039", "SECTION00010000000000000000")] diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustFileDeployedOutOfDomain.cs b/Healthcheck/Rules/HeatlcheckRuleTrustFileDeployedOutOfDomain.cs new file mode 100644 index 0000000..1cd4bc4 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleTrustFileDeployedOutOfDomain.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("T-FileDeployedOutOfDomain", RiskRuleCategory.Trusts, RiskModelCategory.TrustImpermeability)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleIntroducedIn(2, 7)] + public class HeatlcheckRuleTrustFileDeployedOutOfDomain : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + var data = new Dictionary>>(); + foreach (var file in healthcheckData.GPPFileDeployed) + { + string domain = HeatlcheckRuleTrustLoginScriptOutOfDomain.IsForeignScript(file.FileName, healthcheckData); + if (domain != null) + { + Trace.WriteLine("File:" + file.FileName); + if (!data.ContainsKey(domain)) + data[domain] = new List>(); + data[domain].Add(new KeyValuePair(file.GPOName, file.FileName)); + } + } + foreach(var domain in data.Keys) + { + AddRawDetail(domain, string.Join(",", data[domain].ConvertAll(k => k.Key).ToArray()), string.Join(",", data[domain].ConvertAll(k => k.Value).ToArray())); + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs b/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs index 8ddbb91..2b00ce1 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs @@ -20,7 +20,7 @@ public class HeatlcheckRuleTrustLoginScriptOutOfDomain : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData, ICollection AllowedMigrationDomains) + { + foreach (HealthCheckTrustData trust in healthcheckData.Trusts) + { + bool skip = false; + if (AllowedMigrationDomains != null) + { + foreach (DomainKey allowedDomain in AllowedMigrationDomains) + { + if (allowedDomain == trust.Domain) + { + skip = true; + break; + } + if (trust.KnownDomains != null) + { + foreach (HealthCheckTrustDomainInfoData kd in trust.KnownDomains) + { + if (kd.Domain == allowedDomain) + { + skip = true; + break; + } + } + } + if (skip) + break; + } + } + if (!skip && TrustAnalyzer.GetTGTDelegation(trust) == "Yes") + { + AddRawDetail(trust.TrustPartner); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/RuleDescription.resx b/Healthcheck/Rules/RuleDescription.resx index 0f9bcb8..9109857 100644 --- a/Healthcheck/Rules/RuleDescription.resx +++ b/Healthcheck/Rules/RuleDescription.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The purpose is to ensure that the SMB version 2 protocol has the signature enabled when dialoging with domain controllers + The purpose is to ensure that the SMB version 2 protocol has the signature enabled when communicating with domain controllers Enable the group policy "Digitally sign communications (if client agrees)" or check for any policy which may alter the server settings @@ -130,7 +130,7 @@ Domain controller: {0} - The purpose is to ensure that the SMB version 2 protocol has the signature enforced when dialoging with domain controllers + The purpose is to ensure that the SMB version 2 protocol has the signature enforced when communicating with domain controllers Enable the group policy "Digitally sign communications (always)" or check for any policy which may alter the server settings @@ -142,22 +142,22 @@ Domain controller: {0} - The purpose is to ensure that the minimum set of subnet as been configured in the domain + The purpose is to ensure that the minimum set of subnet(s) has been configured in the domain - Locate the IP address which was found as not being part of declared subnet then add this subnet to the "Active Directory Sites" tool. If you found IPv6 addresses and it was not expected, you should disable in addition the protocol IPv6 on the network card. + Locate the IP address which was found as not being part of declared subnet then add this subnet to the "Active Directory Sites" tool. If you have found IPv6 addresses and it was not expected, you should disable the IPv6 protocol on the network card. - The subnet declaration is incomplete [{count} ip of DC not found in declared subnets] + The subnet declaration is incomplete [{count} IP of DC not found in declared subnets] Domain Controller {0} ip address {1} - The purpose is check if the backups are actually up to date in case they are needed. The alert can be triggered when a domain is back up using non recommended methods + The purpose is check if the backups are actually up to date in case they are needed. The alert can be triggered when a domain is backed up using non-recommended methods - Planify backups based on Microsoft standard. These standards depends on the Operating System. Example with the wbadmin utility: <i>wbadmin start systemstatebackup -backuptarget:d:</i> + Plan AD backups based on Microsoft standards. These standards depend on the Operating System. For example with the wbadmin utility: <i>wbadmin start systemstatebackup -backuptarget:d:</i> Last AD backup has been performed {count} day(s) ago @@ -166,7 +166,7 @@ The purpose is to ensure that the schema has been updated for the creation of Protected Users group. - The Protected Users group is automatically created when a Windows 2012 R2 domain controller is installed and upgraded it as PDC (primary DC). The group is then be automatically created and replicated. + The Protected Users group is automatically created when a Windows 2012 R2 domain controller is installed and upgraded to a PDC (primary DC). The group is then be automatically created and replicated. <b> Warning: Do not add service account into this group as this will result in "authentication failure" messages. Use "protected accounts" instead</b> @@ -176,28 +176,28 @@ The purpose is to make sure that there is a proper password policy in place for the native local administrator account. - If you don't have any provisionning process or password solution to manage local administrators, you should install the LAPS solution. If you mitigate the risk differently, you should add this rule as an exception, as the risk is covered. + If you don't have any provisioning process or password solution to manage local administrators, you should install the LAPS solution. If you mitigate the risk differently, you should add this rule as an exception, as the risk is covered. LAPS doesn't seem to be installed - The purpose is to verify if Domain Controller are vulnerable to the MS17-010 vulnerability + The purpose is to verify if Domain Controller(s) are vulnerable to the MS17-010 vulnerability - To fix the security breach, you should path the DC as soon as it has been established it was vulnerable. Another good remediation is to disable SMB v1 (see "DC Vulnerability (SMB v1)). You can verify that using the github program in the links: this program will check remotely the last startup time of the DC and evaluate the risk + To fix the security breach, you should pacth the DC as soon as it has been established it was vulnerable. Another good remediation is to disable SMB v1 (see "DC Vulnerability (SMB v1)). You can verify that using the github program in the links: this program will check remotely the last startup time of the DC and evaluate the risk - Number of DC vulnerable to MS17-010 = {count} (>0) + Number of DC(s) vulnerable to MS17-010 = {count} (>0) Domain controller {0} based on {1} - The purpose is to verify if Domain Controller are vulnerable to the SMB v1 vulnerability + The purpose is to verify if Domain Controller(s) are vulnerable to the SMB v1 vulnerability - It is highly recommended by Microsoft to disable SMB v1 whenever it is possible on both client and server side. <b>Do note that if you are still not following best practices regarding the usage of deprecated OS (Windows 2000, 2003, XP, CE), regarding Network printer using SMBv1 scan2shares functionnalities, or regarding software accessing Windows share with a custom implementation relying on SMB v1, you should consider fixing this issues before disabling SMB v1, as it will generates additionnal errors</b>. + It is highly recommended by Microsoft to disable SMB v1 whenever it is possible on both client and server side. <b>Do note that if you are still not following best practices regarding the usage of deprecated OS (Windows 2000, 2003, XP, CE), regarding Network printer using SMBv1 scan2shares functionalities, or regarding software accessing Windows share with a custom implementation relying on SMB v1, you should consider fixing this issues before disabling SMB v1, as it will generates additional errors</b>. SMB v1 activated on {count} DC @@ -206,19 +206,19 @@ Domain controller: {0} - The purpose is to verify if Domain Controller are vulnerable to the MS14-068 vulnerability + The purpose is to verify if Domain Controller(s) are vulnerable to the MS14-068 vulnerability - To fix the security breach, you should path the DC as soon as it has been established it was vulnerable. You can verify that using a program in the links: this program will check remotely the last startup time of the DC and evaluate the risk + To fix the security breach, you should pacth the DC as soon as it has been established it was vulnerable. You can verify that using a program in the links: this program will check remotely the last startup time of the DC and evaluate the risk - Number of DC vulnerable to MS14-068 = {count} (>0) + Number of DC(s) vulnerable to MS14-068 = {count} (>0) Domain controller {0} based on {1} - The purpose is to identify if there are restricted group such as local administrators, terminal server access, … where Authenticated Users or Everyone is being ganted access by a GPO + The purpose is to identify if there are restricted group such as local administrators, terminal server access, … where Authenticated Users or Everyone is being granted access by a GPO In order to correct the issue, you should edit the GPO and remove the "Members" security access rule. Another solution is to change the group by a more targeted one containing a limited set of users. @@ -230,16 +230,16 @@ Found in GPO {0} member of {1} user {2} - The purpose is to perform a review of which accounts have ownership rights on domain controller and can then modify their permissions + The purpose is to perform a review of which accounts have ownership rights on a domain controller and can then modify their permissions To solve this security issue, you should change the ownership of the domain controller to match the "Domain Administrators" group. -To control the ownership of domain controller objects, you can use the following powershell command: +To control the ownership of domain controller objects, you can use the following PowerShell command: <i>Get-ADComputer -server my.domain.to.check -LDAPFilter "(&(objectCategory=computer)(|(primarygroupid=521)(primarygroupid=516)))" -properties name, ntsecuritydescriptor | select name,{$_.ntsecuritydescriptor.Owner}</i>. -To change it, you can edit the owner of an object using <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/adexplorer">adexplorer.exe</a>. First, locate the dc object then right click to select properties. Open the security tab and press the advanced button. You have then a new dialog with an owner tab. Select the owner and change it for the domain administrators group. You’re done (no reboot needed) +To change it you can edit the owner of an object using <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/adexplorer">adexplorer.exe</a>. First, locate the DC object then right click to select properties. Open the security tab and press the advanced button. You then have a new dialog with an owner tab. Select the owner and change it for the domain administrators group. You’re done (no reboot needed) - {count} domain controller(s) have been found where the owner is not the Domain admins group or the Enterprise admins group + {count} domain controller(s) have been found where the owner is not the Domain Admins group or the Enterprise Admins group Domain controller: {0} Owner: {1} @@ -260,7 +260,7 @@ To change it, you can edit the owner of an object using <a href="https://docs The purpose is to ensure that accounts are not linked for more privileged accounts in the same domain - It is not possible to have this occurrence except if a user from domain A has been migrated to domain B and then migrated again to domain A. This should be strongly investigated as it may be linked to a compromission of the domain. + It is not possible to have this occurrence except if a user from domain A has been migrated to domain B and then migrated again to domain A. This should be strongly investigated as it may be linked to a compromise of the domain. Account(s) with SID History matching the domain = {count} @@ -269,10 +269,10 @@ To change it, you can edit the owner of an object using <a href="https://docs The purpose is to ensure that basic users cannot register extra computers in the domain - To solve the issue, the limit number of extra computers that can be registered by a basic user should be reduced by modifying the value of <i>ms-DS-MachineAccountQuota</i> to zero (0). Another solution can be to remove altogether the authenticated users group in the domain controllers policy. Do note that if you need to set delegation to an account so it can add computers to the domain, it can be done through 2 methods: Delegation in the OU or by assigning the <i>SeMachineAccountPrivilege</i> to a special group + To solve the issue limit the number of extra computers that can be registered by a basic user. It can be reduced by modifying the value of <i>ms-DS-MachineAccountQuota</i> to zero (0). Another solution can be to remove altogether the authenticated users group in the domain controllers policy. Do note that if you need to set delegation to an account so it can add computers to the domain, it can be done through 2 methods: Delegation in the OU or by assigning the <i>SeMachineAccountPrivilege</i> to a special group - Non admin users can add up to {count} computer(s) to a domain + Non-admin users can add up to {count} computer(s) to a domain Domain controller: {0} Owner: {1} @@ -395,7 +395,7 @@ To change it, you can edit the owner of an object using <a href="https://docs The SIDHistory auditing group is present: SID History creation is enabled - The purpose is to ensure that a compromise domain cannot use scripts located in it to compromise other domains + The purpose is to ensure that a compromised domain cannot use scripts located in it to compromise other domains Copy the login script to a share located inside the domain and not in trusted domains. @@ -407,25 +407,25 @@ To change it, you can edit the owner of an object using <a href="https://docs {0} - The purpose is to access without any account, aka NULL Sessions, within the Active Directory. A NULL Session is a session opened anonymously to access the AD, often used by attackers to perform a reckon operation on the AD, to identify weaknesses + The purpose is to access without any account, aka NULL Sessions, within the Active Directory. A NULL Session is a session opened anonymously to access the AD, often used by attackers to perform a recon operation on the AD, to identify weaknesses - Locate other PingCastle rules such as A-PreWin2000Anonymous or A-DsHeuristicsAnonymous which triggered and apply the solutions. You can use the PingCastle scanner mode to do a manual check and proove the extraction of the data. + Locate other PingCastle rules such as A-PreWin2000Anonymous or A-DsHeuristicsAnonymous which triggered and apply the solutions. You can use the PingCastle scanner mode to do a manual check and prove the extraction of the data. - Number of DC with NULL SESSION enabled: {count} + Number of DC(s) with NULL SESSION enabled: {count} DC involved: {0} - The purpose is to identify domains that which allows access without any account because of a pre-Windows 2000 compatibility. + The purpose is to identify domains which allow access without any account because of a Pre-Windows 2000 compatibility. Remove the "EveryOne" and "Anonymous" from the PreWin2000 group while making sure that the group "Authenticated Users" is present. Then reboot each DC - The group Everyone and/or Anonymous is present in the Pre Windows 2000 group. + The group Everyone and/or Anonymous is present in the Pre-Windows 2000 group. The purpose is to ensure that there is no use of the SHA1 hashing algorithm in Intermediate Certificate @@ -455,7 +455,7 @@ To change it, you can edit the owner of an object using <a href="https://docs The purpose is to give information regarding a best practice for the Service Account password policy. Indeed, having a 20+ characters password for this account greatly helps reducing the risk behind Kerberoast attack (offline crack of the TGS tickets) - The recommended way to handle service account is to use "Managed service accounts" introduced since Windows 2008 R2 (search for "msDS-ManagedServiceAccount"). + The recommended way to handle service accounts is to use "Managed service accounts" introduced since Windows 2008 R2 (search for "msDS-ManagedServiceAccount"). To solve the anomaly, you should implement a PSO or GPO password guarantying a 20+ length password. @@ -465,13 +465,13 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 Found in GPO {0} - The purpose is to identify if accounts without password are allowed to be accessed from the network. This represents a high risk, as an account without a password is essentialy an account that cannot be assign to anyone. + The purpose is to identify if accounts without password are allowed to be accessed from the network. This represents a high risk, as an account without a password is essentially an account that cannot be assigned to anyone. Locate the policy having the setting "Limit local account use of blank passwords to console logon only" disabled and enabled the setting. - One policy has been found where the account having an empty password can be accessed from the network + At least one policy has been found where the account having an empty password can be accessed from the network [{count}] GPO: {0} @@ -492,13 +492,16 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 The authentication protocol NTLM v1 can use the LM password hash algorithm which is weak if enabled by a GPO. - a GPO explicitely disable the default security policy LmCompatibilityLevel or NoLMHash. Using the information provided, identify the setting modified in the GPO and fix it. + A GPO explicitly disabled the default security policy LmCompatibilityLevel or NoLMHash. Using the information provided, identify the setting modified in the GPO and fix it. +All security settings should be modified in the Domain GPO Editor and are located in Computer Configuration / Policies / Windows Settings / Security Settings / Local Policies / Security Options +For NoLMHash the setting is located in: Network security: Do not store LAN Manager hash value on next password change +For LmCompatibilityLevel the setting is located in: Network security: LAN Manager authentication level - One policy has been found where the LM hash can be used [{count}] + At least one policy has been found where the LM hash can be used [{count}] - Found in GPO {0} + Found in GPO {0} with setting {1} The purpose is to verify if the password policy of the domain enforces users to have at least 8 characters in their password @@ -516,10 +519,10 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 The purpose is to verify if a GPO alters the password policy of the domain to enable reversible passwords - In order to remove the anonymous access, we advise to identify the GPO indicated by the program and change the setting "Store passwords using reversible encryption" + In order to remove the anonymous access, we advise to identify the GPO indicated by the program and change the setting "Store passwords using reversible encryption" - One policy has been found where the reversible encryption has been enabled + At least one policy has been found where the reversible encryption has been enabled [{count}] GPO: {0} @@ -531,7 +534,7 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 There are 3 solutions to fix this issue, the most obvious being to change the user password on a regular basis. The fastest way is to check if the domain has the attribute <i>msDS-ExpirePasswordsOnSmartCardOnlyAccounts</i>, which is available for Windows 2016 and later versions and handle periodically hash change. Another possibility instead of changing the password is to disable the flag "this account requires a smart card" then re-enable it which will trigger internally a password hash change. - Number of account using a smart card whose password is not changed: {count} + Number of account(s) using a smart card whose password is not changed: {count} The purpose is to alert when a clear text password has been identified in the GPO. Regardless of whether the password is present or not, both the account and password should be considered compromised @@ -540,7 +543,7 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 In order to solve this issue, you should manually change the password to a new one. If this password is shared on many systems, each system should have a different password. If the GPO was used to define the native local administrator account, it is recommended to install a password solution manager such as the LAPS solution. - Number of passwords found in GPO: {count} + Number of password(s) found in GPO: {count} GPO: {0} login: {1} password: {2} @@ -549,19 +552,23 @@ To solve the anomaly, you should implement a PSO or GPO password guarantying a 2 The purpose is to ensure that there is no rogue admin accounts in the Active Directory - These accounts should be reviewed, especially in regards with their past activities and have the admincount attribute removed. In order to identify which accounts are detected by this rule, we advise to run a Powershell command that will show you all users having this flag set: <i>get-adobject -ldapfilter "(admincount=1)"</i> + These accounts should be reviewed, especially in regards with their past activities and have the admincount attribute removed. In order to identify which accounts are detected by this rule, we advise to run a PowerShell command that will show you all users having this flag set: <i>get-adobject -ldapfilter "(admincount=1)"</i> Do not forget to look at the section AdminSDHolder below. Suspicious admin activities detected on {count} user(s) - The purpose is to alert when the password for the krbtgt account can be used to compromise the whole domain. This password can be used to sign every kerberos ticket, and monitoring it closely often mitigates the risk of golden ticket attacks greatly. + The purpose is to alert when the password for the krbtgt account can be used to compromise the whole domain. This password can be used to sign every kerberos ticket. Monitoring it closely often mitigates the risk of golden ticket attacks greatly. The password of the krbtgt account should be changed twice to invalidate the golden ticket attack. -<b>Beware: two changes of the krbtgt password not replicated to domain controllers can break these domain controllers</b> -There are several possibilities to change the krbtgt password. First, a Microsoft script can be run in order to guarantee the correct replication of these secrets. Unfortunately this script supports only English operating systems. Second, a more manual way is to essentialy reset the password manually once, then to wait 3 days, then to reset it again. This is the safest way as it ensures the password is no longer usable by the Golden ticket attack. +<b>Beware: two changes of the krbtgt password not replicated to domain controllers can break these domain controllers</b> You should wait at least 8 hours between each krbtgt password change. + +There are several possibilities to change the krbtgt password. +First, a Microsoft script can be run in order to guarantee the correct replication of these secrets. Unfortunately this script supports only English operating systems. +Second, a more manual way is to essentially reset the password manually once, then to wait 3 days, then to reset it again. This is the safest way as it ensures the password is no longer usable by the Golden ticket attack. + Last change of the Kerberos password: {count} day(s) ago @@ -570,7 +577,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to verify the presence of dangerous rights when a part of the domain is delegated to a third party - Unless there is a strong justification of their presence, these delegations should be removed. In addition, if the origin of this delegation cannot be found, their creation should be investigated as it can be related to a compromission of the domain + Unless there is a strong justification of their presence, these delegations should be removed. In addition, if the origin of this delegation cannot be found, their creation should be investigated as it can be related to a compromise of the domain Presence of dangerous extended right in delegation: {count} @@ -594,7 +601,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to verify that each delegation are linked to an account which exists - To reduce the risk, the easiest way is essentialy to remove the delegation + To reduce the risk, the easiest way is essentially to remove the delegation Presence of unknown account in delegation: {count} @@ -615,7 +622,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to check for "Service Accounts" in the "Domain Administrator" group - To mitigate the security risk, it is strongly advised to lower the privileges of the "Service Accounts", meaning that they should be removed of the "Domain Administrator" group, while ensuring that the password of each and every "Service Account" is higher than 20 characters + To mitigate the security risk, it is strongly advised to lower the privileges of the "Service Accounts", meaning that they should be removed from the "Domain Administrator" group, while ensuring that the password of each and every "Service Account" is higher than 20 characters Presence of service accounts in the domain admin group (at least {threshold} accounts have a password which never expire): {count} @@ -624,7 +631,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that all Administrator Accounts have the configuration flag "this account is sensitive and cannot be delegated" - To correct the situation, you should make sure that all your Administrator Accounts has the checkbox "This account is sensitive and cannot be delegated" active. Please not that there is a section bellow in this report named "Admin Groups" which give more information. + To correct the situation, you should make sure that all your Administrator Accounts has the check-box "This account is sensitive and cannot be delegated" active. Please not that there is a section bellow in this report named "Admin Groups" which give more information. Presence of Admin accounts which have not the flag "this account is sensitive and cannot be delegated": {count} @@ -636,7 +643,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that the Administrator Accounts in the AD are all necessary and used - To correct the situation, you should make sure that all your Administrator Account are "Active", meaning that you should remove Administator rights if an account is set as not "Active" + To correct the situation, you should make sure that all your Administrator Account(s) are "Active", meaning that you should remove Administrator rights if an account is set as not "Active" More than {threshold}% of admins are inactive: {count}% @@ -705,7 +712,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows 2003 as Domain Controller within the domain - To resolve this security risk, the only way is to decomission DC running Windows 2003 OS, in order to use new versions that are more secured and that are still being patched regarding new security threats + To resolve this security risk, the only way is to decommission DC running Windows 2003 OS, in order to use new versions that are more secured and that are still being patched regarding new security threats Presence of Windows 2003 as DC = {count} @@ -714,7 +721,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows 2000 as Domain Controller within the domain - To resolve this security risk, the only way is to decomission DC running Windows 2000 OS, in order to use new versions that are more secured and that are still being patched regarding new security threats + To resolve this security risk, the only way is to decommission DC running Windows 2000 OS, in order to use new versions that are more secured and that are still being patched regarding new security threats Presence of Windows 2000 as DC = {count} @@ -723,7 +730,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows 2003 for the workstations within the domain - In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following powershell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> + In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following PowerShell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> Presence of Windows 2003 = {count} @@ -732,7 +739,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows 2000 for the workstations within the domain - In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following powershell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> + In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following PowerShell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> Presence of Windows 2000 = {count} @@ -741,7 +748,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows XP for the workstations within the domain - In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following powershell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> + In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following PowerShell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> Presence of Windows XP = {count} @@ -750,7 +757,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that there is no use of the obsolete and vulnerable OS Windows NT for the workstations within the domain - In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following powershell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> + In order to solve this security issue, you should upgrade all the workstations to a more recent version of Windows, starting from Windows 7. Do note that you can get the full details regarding the OS used with the following PowerShell command: <i>Get-ADComputer -Filter * -Property * | Format-Table Name,OperatingSystem,OperatingSystemServicePack,OperatingSystemVersion -Wrap –Auto You can replace [-Filter *] by [-Filter {OperatingSystem -Like "Windows Server*"}</i> Presence of Windows NT = {count} @@ -759,7 +766,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that all the Domain Controllers are updated regularly. This is done by checking if a DC has been rebooted in the past 6 months. If not, it means it has not be patched as well in these 6 monthes - Frequently updating the DC should be part of the AD policies, as there should be a dedicated timeslot for the servers to reboot and apply security patches + Frequently updating the DC should be part of the AD policies, as there should be a dedicated time-slot for the servers to reboot and apply security patches Number of DC not updated = {count} @@ -785,10 +792,10 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t trust without SID Filtering: {0} - The purpose is to very if they currently are duplicate accounts within the domain. A duplicate account is essentially a duplicate of two objets having the same attributes. + The purpose is to very if there currently are duplicate accounts within the domain. A duplicate account is essentially a duplicate of two objects having the same attributes. - Duplicate accounts often means there are weaknesses in term of processes, that is why they should be monitored and removed. To identify all duplicate accounts, you should use the following powershell commands: <i>get-adobject -ldapfilter "(cn=*cnf:*)"</i> ; <i>get-adobject -ldapfilter "(sAMAccountName=$duplicate)"</i> + Duplicate accounts often means there are weaknesses in term of processes, that is why they should be monitored and removed. To identify all duplicate accounts, you should use the following PowerShell commands: <i>get-adobject -ldapfilter "(cn=*cnf:*)"</i> ; <i>get-adobject -ldapfilter "(sAMAccountName=$duplicate)"</i> Presence of duplicate accounts = {count} @@ -797,7 +804,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to verify if there are accounts currently running with a reversible password - To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> + To remove this risk, there should be no account(s) with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following PowerShell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> Number of computers which have a reversible password: {count} @@ -806,19 +813,19 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to verify if there are user accounts currently running with a reversible password - To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> + To remove this risk, there should be no account(s) with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following PowerShell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> - Number of accounts which have a reversible password: {count} + Number of account(s) which have a reversible password: {count} - The purpose is to ensure that there is every account requires a password + The purpose is to ensure that every account requires a password - The best solution to solve the problem is to change the "useraccountcontrol" attribue of all the accounts that have it and that are not used in trusts. If the flag is removed while there is no password set, you will have an error. You can use this to detect accounts without any passwords. Do note that you can manually check all the accounts that need to be worked on using the following powershell command: <i>get-adobject -ldapfilter "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))" -properties useraccountcontrol</i> + The best solution to solve the problem is to change the "useraccountcontrol" attribute of all the accounts that have it and that are not used in trusts. If the flag is removed while there is no password set, you will have an error. You can use this to detect accounts without any passwords. Do note that you can manually check all the accounts that need to be worked on using the following PowerShell command: <i>get-adobject -ldapfilter "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))" -properties useraccountcontrol</i> - Number of accounts which can have an empty password (can be overriden by GPO): {count} + Number of accounts which can have an empty password (can be overridden by GPO): {count} The purpose is to verify that no weak hashing algorithm such as DES is used to hash the password of the account. @@ -830,19 +837,19 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t Presence of Des Enabled account = {count} - The purpose is to check for unsual value in the primarygroupid attribute used to store group membership + The purpose is to check for unusual value in the primarygroupid attribute used to store group membership - Unless stronly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". + Unless strongly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". Presence of wrong primary group for computers: {count} - The purpose is to check for unsual value in the primarygroupid attribute used to store group membership + The purpose is to check for unusual value in the primarygroupid attribute used to store group membership - Unless stronly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". + Unless strongly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". Presence of wrong primary group for users: {count} @@ -860,7 +867,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to ensure that there are as few inactive accounts as possible within the domain - To mitigate the risk, you should monitore the number of inactive accounts and reduce it as much as possible. A list of all inactive accounts is obtainable through the command: <i>Search-ADaccount -UsersOnly -AccountInactive -Timespan 180</i>. + To mitigate the risk, you should monitor the number of inactive accounts and reduce it as much as possible. A list of all inactive accounts is obtainable through the command: <i>Search-ADaccount -UsersOnly -AccountInactive -Timespan 180</i>. Relatively high number of inactive user accounts: {count}% (more than {threshold}% of all users) @@ -870,7 +877,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t To solve the security issue, you should remove all the SIDHistory attributes. To do so, you can list the objects having an SIDHistory attribute using the command: <i>get-ADObject -ldapfilter "(sidhistory=*)" -properties sidhistory</i>. -Each security descriptor of the domain (including file shares for example) should be reviewed to be rewritten with the new SID of the account. Then, the attribute can be removed of these accounts using the migration tool or a powershell snippet <i>Remove-SIDHistory</i> once the migration is completed. Please note that once the SID History has been removed, it cannot be added back again without doing a real migration. Hopefully hacking tools such as mimikatz can be used to undo a deletion with for example the lsadump::dcshadow attack. +Each security descriptor of the domain (including file shares for example) should be reviewed to be rewritten with the new SID of the account. Then, the attribute can be removed of these accounts using the migration tool or a PowerShell snippet <i>Remove-SIDHistory</i> once the migration is completed. Please note that once the SID History has been removed, it cannot be added back again without doing a real migration. Hopefully hacking tools such as mimikatz can be used to undo a deletion with for example the lsadump::dcshadow attack. {count} domain(s) used in SIDHistory @@ -915,7 +922,7 @@ Each security descriptor of the domain (including file shares for example) shoul Retrieve data from the domain without any account - Check the processus of registration of computers to the domain + Check the process of registration of computers to the domain Check for presence of the Protected users group @@ -987,7 +994,7 @@ Each security descriptor of the domain (including file shares for example) shoul Domain Controller Update - Check for weak algorithm in password hashing (DES algorythm) + Check for weak algorithm in password hashing (DES algorithm) DC Vulnerability (SMB v1) @@ -1044,24 +1051,24 @@ Each security descriptor of the domain (including file shares for example) shoul A Delegation is granted to Everyone - A check is performed non-admin accounts in order to identify if they have an attribute <i>admincount</i> set. If they have this attribute, it means that this account, which is not supposed to be admin, has been granted administrator rights in the past. This tipically happens when an administrator gives temporary rights to a normal account, off process. + A check is performed on non-admin accounts in order to identify if they have an attribute <i>admincount</i> set. If they have this attribute, it means that this account, which is not supposed to be admin, has been granted administrator rights in the past. This typically happens when an administrator gives temporary rights to a normal account, off process. It is possible that domains are set to authorize connection without any account, which represents a security breach. It allows potential attackers to enumerate all the users and computers belonging to a domain, in order to identify very efficiently future weak targets. It is possible to verify the results provided by the PingCastle solution by using a Kali distribution. You should run [rpcclient -U " target_ip_address] and press enter at the password prompt to finally type [enumdomusers]. - A verification is done on the backups, ensuring that the backup is performed according to Microsoft standard. Indeed at each backup the DIT Database Partition Backup Signature is updated.  if for any reasons, backups are needed to perform a rollback (rebuild a domain) or to track past changes, the backups will actually be up to date. This check is equivalent to a <i>REPADMIN /showbackup *</i>. + A verification is done on the backups, ensuring that the backup is performed according to Microsoft standards. Indeed at each backup the DIT Database Partition Backup Signature is updated.  If for any reasons, backups are needed to perform a rollback (rebuild a domain) or to track past changes, the backups will actually be up to date. This check is equivalent to a <i>REPADMIN /showbackup *</i>. - The way an Active Directory behave can be controlled via the attribute <i>DsHeuristics</i> of <i>CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration</i>. A parameter stored in its attribute and whose value is <i>fLDAPBlockAnonOps</i> can be set to allow access without any account on the <b>whole forest level</b>. + The way an Active Directory behaves can be controlled via the attribute <i>DsHeuristics</i> of <i>CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration</i>. A parameter stored in its attribute and whose value is <i>fLDAPBlockAnonOps</i> can be set to allow access without any account on the <b>whole forest level</b>. It is possible to verify the results provided by the PingCastle solution by using a Kali distribution. You should run <i>rpcclient -U " target_ip_address</i> and press enter at the password prompt to finally type <i>enumdomusers</i>. Kerberos is an authentication protocol. It is using to sign its tickets a secret stored as the password of the krbtgt account. If the hash of the password of the krbtgt account is retrieved, it can be use to generate authentication tickets at will. -To mitigate this attack, it is recommanded to change the krbtgt password every 40 days. If it not the case, every backups done until the last password change of the krbtgt account can be used to emit Goldent tickets, compromising the entiere domain. +To mitigate this attack, it is recommended to change the krbtgt password every 40 days. If it not the case, every backup done until the last password change of the krbtgt account can be used to emit Golden tickets, compromising the entire domain. Retrieval of this secret is one of the highest priority in an attack, as this password is rarely changed and offer a long term backdoor. -Also this attack can also be performed using the former password of the krbtgt account +Also this attack can be performed using the former password of the krbtgt account. That's why the krbtgt password should be changed twice to invalidate its leak. LAPS (Local Administrator Password Solution) is the advised solution to handle passwords for the native local administrator account on all workstations, as it is a simple way to handle most of the subject. @@ -1070,7 +1077,7 @@ Also this attack can also be performed using the former password of the krbtgt a This rule verifies if there is a GPO with the setting "Limit local account use of blank passwords to console logon only" disabled. - LM hash, or LAN Manager hash is a hash algorithm developped by Microsot since Windows 3.1. Due to flaw design, hashes retrieved from the network can be reverted to the clear text password in a matter of seconds. + LM hash, or LAN Manager hash is a hash algorithm developed by Microsoft since Windows 3.1. Due to flaw design, hashes retrieved from the network can be reverted to the clear text password in a matter of seconds. The MD2 hashing algorithm is not considered as safe. There are design flaws inherent to the algorithm that allow an attacker to generate a hash collision in less than a brute-force time @@ -1094,14 +1101,14 @@ Also this attack can also be performed using the former password of the krbtgt a It is possible that a GPO add local membership of a restricted group. In this case the rule trigger if one is found with "Everyone" or "Authenticated Users" as members. It basically means that the Restricted Group has no restriction on belongs to it. This represents a security risk as Restricted Group are supposed to have more accesses or rights - A check is perfomed to identify if the GPO regarding password policy allows less than 8 characters password. Short passwords represents a high risk because they can fairly easily be brute-forced. Most CERT and agencies advises for at least 8 characters (and often this number goes up to 12) + A check is performed to identify if the GPO regarding password policy allows less than 8 characters password. Short passwords represents a high risk because they can fairly easily be brute-forced. Most CERT and agencies advises for at least 8 characters (and often this number goes up to 12) The rule is purely informative, as it gives insights regarding a best practice. It verifies if there is a GPO or PSO enforcing a 20+ characters password for the Service Account. - Unless other rules which check for known cause of anonymous access, this rule try to enumerate accounts from the domain without any account. The program use two methods: MS-SAMR with a NULL connection and MS-LSAT which forces SID resolution with well known SID. -NULL sessions are deactivated by default since Windows 2003 and Windows XP. But for compatibility reasons, a setting enabling them may be still active years after. + Unless other rules which check for known cause of anonymous access, this rule tries to enumerate accounts from the domain without any account. The program use two methods: MS-SAMR with a NULL connection and MS-LSAT which forces SID resolution with well known SID. +NULL sessions are deactivated by default since Windows 2003 and Windows XP. For compatibility reasons a setting enabling them may be still active years after. It is possible to verify the results provided by the PingCastle solution by using a Kali distribution. You should run [rpcclient -U " target_ip_address] and press enter at the password prompt to finally type [enumdomusers]. @@ -1113,10 +1120,10 @@ It is possible to verify the results provided by the PingCastle solution by usin A check is performed to identify passwords in the GPO. If a password is identified through the PingCastle solution, it means that it can be identified through many other means by attackers, and that the account should be considered compromised. -Do note that the AES key used to encrypt password in the GPO has been made public for interoperability reasons, which is why even an encrypted password is compromised. It has been revealed in <a href="https://msdn.microsoft.com/en-us/library/cc422924.aspx">this page</a> +Do note that the AES key used to encrypt passwords in GPOs has been made public for interoperability reasons, which is why even an encrypted password is compromised. It has been revealed in <a href="https://msdn.microsoft.com/en-us/library/cc422924.aspx">this page</a> - The policy "Store passwords using reversible encryption" is enabled. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrived using DCSync attack. + The policy "Store passwords using reversible encryption" is enabled. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrieved using DCSync attack. The SHA0 hashing algorithm is not considered as safe. There are design flaws inherent to the algorithm that allow an attacker to generate a hash collision in less than a brute-force time @@ -1146,19 +1153,19 @@ Do note that the AES key used to encrypt password in the GPO has been made publi The right "REANIMATE_TOMBSTONE" used to undelete objects, "UNEXPIRE_PASSWORD" used to undo the expiration of a password, or "SID_HISTORY" used to create an alternate identity is considered dangerous. Indeed this rights can be used to trigger a backdoor. - By default, the "Domain Administrators Group" or the "Enterprise Administrators Group" are set as owners for "Domain Controllers". Nonetheless, in some cases (for instance when the server has been promoted from an existing server), the owner can be a non admin person which joined the server to the domain. If this person has still rights over this account, it can be used to take ownership over the whole domain. A chain of compromission can be designed to take control of the domain by including this account. + By default, the "Domain Administrators" group or the "Enterprise Administrators" group are set as owners for "Domain Controllers". Nonetheless, in some cases (for instance when the server has been promoted from an existing server), the owner can be a non-admin person which joined the server to the domain. If this person has still rights over this account, it can be used to take ownership over the whole domain. A chain of compromising events can be designed to take control of the domain by including this account. - Whitout the flag "This account is sensitive and cannot be delegated" any account can be impersonated by some service account. It is a best practice to enforce this flag on administrators accounts. + Without the flag "This account is sensitive and cannot be delegated" any account can be impersonated by some service account. It is a best practice to enforce this flag on administrators accounts. To delegate control to a OU, access checks can be modified. In case of a misconfiguration, access can be granted to the group "Everyone" or "Authenticated Users". - Accounts wihtin the AD have attributes indicating the creation date of the account and the last login of this account. Accounts which doesn't have a login since 6 months or created more than 6 months ago without any login are considered inactive. If an Administrator Account is set as inactive, the reason for having Administrator rights should be strongly justified. + Accounts within the AD have attributes indicating the creation date of the account and the last login of this account. Accounts which haven't have a login since 6 months or created more than 6 months ago without any login are considered inactive. If an Administrator Account is set as inactive, the reason for having Administrator rights should be strongly justified. - The group "Schema Admins" is used to give permissions to alter the schema. Once a modification is performed on the schema such as new objects, it cannot be undone. This can result in a rebuild of the domain. The best pratice is to have this group empty and to add an administrator when a schema update is required then to remove this group membership. + The group "Schema Admins" is used to give permissions to alter the schema. Once a modification is performed on the schema such as new objects, it cannot be undone. This can result in a rebuild of the domain. The best practice is to have this group empty and to add an administrator when a schema update is required then to remove this group membership. "Service Accounts" can imply a high security risk as their password are stored in clear text in the LSA database, which can then be easily exploited using Mimikatz or Cain&Abel for instance. In addition, their passwords don't change and can be used in kerberoast attacks. @@ -1167,16 +1174,20 @@ Do note that the AES key used to encrypt password in the GPO has been made publi In the case where a delegation has been created where the account can't be translated to a NT account, it means that the delegation is actually from another domain or that the user has been deleted. - By default, a basic user can register up to 10 computers within the domain. This default set up represents a security issue as basic users shouldn't be able to create such accounts, this task being handled by administrators + By default, a basic user can register up to 10 computers within the domain. This default configuration represents a security issue as basic users shouldn't be able to create such accounts and this task should be handled by administrators. - Inactive computers often stay in the network because of weaknesses in the decomissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. + Inactive computers often stay in the network because of weaknesses in the decommissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. - In Active Directory, group membersip is stored on the "members" attribute and on the "primarygroupid" attribute. The default primary group value is "Domain Users" for the users, "Domain Computers" for the computers and "Domain Controllers" for the domain controllers. The primarygroupid contains the RID (last digits of a SID) of the group targeted. It can be used to store hidden membership as this attribute is not often analyzed. + In Active Directory, group membership is stored on the "members" attribute and on the "primarygroupid" attribute. + The default primary group value is "Domain Users" for the users, "Domain Computers" for the computers and "Domain Controllers" for the domain controllers. + The primarygroupid contains the RID (last digits of a SID) of the group targeted. It can be used to store hidden membership as this attribute is not often analyzed. + This rule can also be triggered if one domain controller is not in the default container (named "Domain Controllers" and located at the root) which is not a recommended practice. + - It is possible that domains have accounts with an encryption that can be reversed. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrived using DCSync attack + It is possible that domains have accounts with an encryption that can be reversed. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrieved using DCSync attack The OS Windows 2000 as a DC is vulnerable to many publicly known exploits such as MS17-010 or MS14-068 and it can no longer be patched. A domain running this OS version should be considered compromised @@ -1191,7 +1202,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi When multiple sites are created in a domain, networks should be declared in the domain in order to optimize processes such as DC attribution. In addition, PingCastle can collect the information to be able to build a network map. This rule has been triggered because at least one domain controller has an IP address which was not found in subnet declaration. These IP addresses have been collected by querying the DC FQDN IP address in both IPv6 and IPv4 format. - DES is very weak algorithm and once assigned to an account, it can be used to sign Kerberos ticket, even though it is easily breakable. It respresents a security risk for the kerberos ticket, therefore for the whole AD + DES is very weak algorithm and once assigned to an account, it can be used to sign Kerberos ticket, even though it is easily breakable. It represents a security risk for the kerberos ticket, therefore for the whole AD. To migrate accounts to another domain, the attribute SID History should be added to the new account. Despite the fact that numerous hacking tools such as mimikatz allows the creation of the SID History attribute, its official creation requires the presence of a special auditing group named DOMAIN-$$$ such as TEST-$$$ for the TEST domain. @@ -1200,7 +1211,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi In order to identify a duplicate account, a check is performed on the "DN" and the "sAMAccountName". Indeed, when a DC detects a conflict, there is a replacement performed on the second object - Inactive accounts often stay in the network because of weaknesses in the decomissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. + Inactive accounts often stay in the network because of weaknesses in the decommissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. The Windows 2000 OS is not supported any longer, as it is vulnerable to many publicly known exploits: Administrator's credentials can be captured, security protocols are weak, etc. @@ -1216,13 +1227,13 @@ Do note that the AES key used to encrypt password in the GPO has been made publi The Windows XP OS is not supported any longer, as it is vulnerable to many publicly known exploits: Administrator's credentials can be captured, security protocols are weak, etc. - In Active Directory, group membersip is stored on the "members" attribute and on the "primarygroupid" attribute. The default primary group value is "Domain Users" for the users, "Domain Computers" for the computers and "Domain Controllers" for the domain controllers. The primarygroupid contains the RID (last digits of a SID) of the group targeted. It can be used to store hidden membership as this attribute is not often analyzed. + In Active Directory, group membership is stored on the "members" attribute and on the "primarygroupid" attribute. The default primary group value is "Domain Users" for the users, "Domain Computers" for the computers and "Domain Controllers" for the domain controllers. The primarygroupid contains the RID (last digits of a SID) of the group targeted. It can be used to store hidden membership as this attribute is not often analyzed. An account can be set without a password if it has the flag "PASSWD_NOTREQD" set as "True" in the "useraccountcontrol" attribute. This represents a high security risk as the account is not protected at all without a password - It is possible that domains have accounts with an encryption that can be reversed. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrived using DCSync attack + It is possible that domains have accounts with an encryption that can be reversed. In this case, it means that the password is actually stored in clear text in the <i>supplementalCredential</i> attribute of the account and that it can be retrieved using DCSync attack The SIDHistory attribute is useful when doing a migration because it allows to keep the reference to the former account. On the other hand, once the migration is over, it is mandatory that this attribute is removed to evaluate the permissions in regards with the new account and not the former one. @@ -1231,10 +1242,10 @@ Do note that the AES key used to encrypt password in the GPO has been made publi The SMB downgrade attack is used to obtain credentials or executing commands on behalf of a user by using SMB v1 as protocol. Indeed, because SMB v1 supports old authentication protocol, the integrity can be bypassed - MS14-068 is a critical vulnerability that was published on 2014-11-18. It can be used to very quickly compromise an entiere domain, which is why having DC still vulnerable to this publicly known vulnerability represents a high security risk. + MS14-068 is a critical vulnerability that was published on November, 18th 2014. It can be used to very quickly compromise an entire domain, which is why having DC still vulnerable to this publicly known vulnerability represents a high security risk. - MS17-010 is a critical vulnerability that was published on 2014-11-18. It can be used to compromise an entiere domain via DC compromise. This exploit has been revealed by the Shadow browkers (EternalBlue, EternalRomance, EternalSinergy) and it uses the SMB v1 vulnerability + MS17-010 is a critical vulnerability that was published on March, 14th 2017. It can be used to compromise an entire domain via DC compromise. This exploit has been revealed by the Shadow brokers (EternalBlue, EternalRomance, EternalSinergy) and it uses the SMB v1 vulnerability A Downlevel trust is a special kind of trust compatible with NT4. The kind of trust can be displayed in the "Active Directory Domains and Trusts" tool. @@ -1246,7 +1257,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi Login script can be stored in any file share available in the network and that includes trusted domains shares. If a login script is located in a compromise domain, it can be used to compromise other domains. - SID Filtering is a mechanism used to block account presenting a SID History property. SID History is used to link to an existing account to another account and can be use to propagage a compromission through trusts. SID Filtering for domain to domain trust is called quarantine and is disabled by default. SID Filtering to a forest is enabled by default and disabling it is called "enabling SID History". + SID Filtering is a mechanism used to block account presenting a SID History property. SID History is used to link an existing account to another account and can be use to propagate a compromise through trusts. SID Filtering for domain to domain trust is called a quarantine and is disabled by default. SID Filtering to a forest is enabled by default and disabling it is called "enabling SID History". The algorithm to compute the SID Filtering is: get the attribute trustDirection and TrustAttributes of the trust object. @@ -1257,7 +1268,7 @@ If enabled: SID Filtering is deactivated. Else if not a forest trust (trustattributes & 8 == 0) then check for the quarantined attribute (trustattributes & 4 != 0). If the quarantine flag is set, SID Filtering is enabled. -You can use the powershell command to get its status: +You can use the PowerShell command to get its status: <i>[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().GetSidFilteringStatus('my.domain.to.test.local')</i> @@ -1517,10 +1528,10 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits Check if the account has been migrated from a domain which doesn't exist anymore - Python responsder is a tool used to compromise a domain by listening for SMB connections and injecting rogue data into the communications at the network level. SMB v1 does not provide a mechanism to enforce integrity and thus is compromised easily. SMB v2 (and subsequent version SMB v3) provides a way to guarantee the integrity of the network communication via a signature of each packet. By establishing a SMB v2 dialog with domain controllers, PingCastle checks the signature capability by looking at the SMB options provided by the server. + Python responder is a tool used to compromise a domain by listening for SMB connections and injecting rogue data into the communications at the network level. SMB v1 does not provide a mechanism to enforce integrity and thus is compromised easily. SMB v2 (and subsequent version SMB v3) provides a way to guarantee the integrity of the network communication via a signature of each packet. By establishing a SMB v2 dialog with domain controllers, PingCastle checks the signature capability by looking at the SMB options provided by the server. - Python responsder is a tool used to compromise a domain by listening for SMB connections and injecting rogue data into the communications at the network level. SMB v1 does not provide a mechanism to enforce integrity and thus is compromised easily. SMB v2 (and subsequent version SMB v3) provides a way to guarantee the integrity of the network communication via a signature of each packet. By establishing a SMB v2 dialog with domain controllers, PingCastle checks the signature capability by looking at the SMB options provided by the server. + Python responder is a tool used to compromise a domain by listening for SMB connections and injecting rogue data into the communications at the network level. SMB v1 does not provide a mechanism to enforce integrity and thus is compromised easily. SMB v2 (and subsequent version SMB v3) provides a way to guarantee the integrity of the network communication via a signature of each packet. By establishing a SMB v2 dialog with domain controllers, PingCastle checks the signature capability by looking at the SMB options provided by the server. https://msdn.microsoft.com/en-us/library/cc246675.aspx @@ -1529,22 +1540,22 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits https://msdn.microsoft.com/en-us/library/cc246675.aspx - The purpose is to ensure that all accounts do support kerberos pre authentication + The purpose is to ensure that all accounts do support kerberos pre-authentication http://www.harmj0y.net/blog/activedirectory/roasting-as-reps/ - Number of accounts which do not require kerberos pre authentication: {count} + Number of accounts which do not require kerberos pre-authentication: {count} - Edit the property of the involved accounts and select the Account tab. Uncheck "Do not require Kerberos preauthentication". For computers which doesn't have the Account tab, you have to manually edit the attribute useraccountcontrol. Substract from the attribute the value 4194304. + Edit the property of the involved accounts and select the Account tab. Uncheck "Do not require Kerberos preauthentication". For computers which doesn't have the Account tab, you have to manually edit the attribute useraccountcontrol. Subtract from the attribute the value 4194304. - Without kerberos preauthentication, an attacker can request kerberos data from the domain controller and use this data to bruteforce the account password. You can search accounts using the ldap query <i>(userAccountControl:1.2.840.113556.1.4.803:=4194304)</i> + Without kerberos pre-authentication, an attacker can request kerberos data from the domain controller and use this data to brute-force the account password. You can search accounts using the ldap query <i>(userAccountControl:1.2.840.113556.1.4.803:=4194304)</i> - Check if all accounts do support kerberos pre authentication + Check if all accounts do support kerberos pre-authentication The purpose is to ensure that standard users cannot modify login scripts @@ -1559,7 +1570,7 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits Edit the Access Control List (ACL) of the script object or the directory where the file is located. Then remove any write permission given to the group. - When the group Authenticated Users, Everyone or any similar groups have permission to modify a login script, it can be abused to take control of the accounts using this script. It can potentically lead to the compromise of the domain + When the group Authenticated Users, Everyone or any similar groups have permission to modify a login script, it can be abused to take control of the accounts using this script. It can potentially lead to the compromise of the domain Ensure that all login scripts cannot be modified by any user @@ -1574,10 +1585,10 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits The purpose is to ensure that credentials cannot be extracted from the DC via its printer spooler - The spooler service should be deactived on domain controllers. Please note as a consequence that the Printer Pruning functionality (rarely used) will be unavailable. + The spooler service should be deactivated on domain controllers. Please note as a consequence that the Printer Pruning functionality (rarely used) will be unavailable. - The spooler service is remotely acccessible from {count} DC + The spooler service is remotely accessible from {count} DC When there’s an account with unconstrained delegation configured (which is fairly common) and the Print Spooler service running on a computer, you can get that computers credentials sent to the system with unconstrained delegation as a user. With a domain controller, the TGT of the DC can be extracted allowing an attacker to reuse it with a DCSync attack and obtain all user hashes and impersonate them. @@ -1590,7 +1601,7 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits https://www.slideshare.net/harmj0y/derbycon-the-unintended-risks-of-trusting-active-directory - Ensure that there are enough DC to provide basic redundancy + Ensure that there are enough DCs to provide basic redundancy The purpose is to ensure the failure of one domain controller will not stop the domain. @@ -1599,7 +1610,7 @@ https://www.slideshare.net/harmj0y/derbycon-the-unintended-risks-of-trusting-act Increase the number of domain controllers by installing new ones. - The number of DC is too small to provide redundancy: {count} DC + The number of DCs is too small to provide redundancy: {count} DC A single domain controller failure can lead to a lack of availability of the domain if the number of servers is too low. To have a minimum redundancy, the number of DC should be at least 2. For Labs, this rule can be ignored and you can add this rule into the exception list. @@ -1614,13 +1625,13 @@ https://www.slideshare.net/harmj0y/derbycon-the-unintended-risks-of-trusting-act The purpose is to ensure no account can impersonate any account. - Replace unconstrained delegation by contrained delegation. In practice, on the account object, tab "delegation", replace "trust this computer for delegation to any service" by "trust this computer for delegation to specified services only". + Replace unconstrained delegation by constrained delegation. In practice, on the account object, tab "delegation", replace "trust this computer for delegation to any service" by "trust this computer for delegation to specified services only". Unconstrained delegations are configured on the domain: {count} account(s) - When an unconstrained delegation is configured, the kerberos ticket TGT can be captured. This TGT grant then access to any service the user has access. If the user is administor or a domain controller (a connection can be forced using the spooler service), the domain can be compromised. + When an unconstrained delegation is configured, the kerberos ticket TGT can be captured. This TGT grant then access to any service the user has access. If the user is an administrator or a domain controller (a connection can be forced using the spooler service), the domain can be compromised. https://blogs.technet.microsoft.com/389thoughts/2017/04/18/get-rid-of-accounts-that-use-kerberos-unconstrained-delegation/ @@ -1636,7 +1647,7 @@ https://adsecurity.org/?p=1667 The purpose is to ensure that no weakness has been introduced at Exchange installation. - After having carefuly studied the possible impact of the following change, alter the AdminSDHolder permissions to remove the Exchange objects. + After having carefully studied the possible impact of the following change, alter the AdminSDHolder permissions to remove the Exchange objects. Exchange did alter the AdminSDHolder object @@ -1644,23 +1655,23 @@ https://adsecurity.org/?p=1667 At install time, the Exchange Windows Permissions universal security group (USG) was granted the ability to modify the members attribute, the ability to change and reset passwords, and the ability to modify the permissions of any object protected by the AdminSDHolder role. This security group includes all the Exchange servers. - As a consequence, a malicious administrator could elevate their privileges on one of this server and thus gain control of the Active Directory forest. + As a consequence, a malicious administrator could elevate their privileges on one of the servers and thus gain control of the Active Directory forest. Newest versions of Exchange do not introduce this security vulnerability. https://blogs.technet.microsoft.com/exchange/2009/09/23/exchange-2010-and-resolution-of-the-adminsdholder-elevation-issue/ - Ensure that boggus Windows 2016 AD prep did not introduce vulnerabilities + Ensure that bogus Windows 2016 AD prep did not introduce vulnerabilities - The purpose is to ensure that no weakness has been introduced at Windows 2016 installation. + The purpose is to ensure that no weaknesses have been introduced following a Windows 2016 installation. - After having carefuly studied the possible impact of the following change, apply the script made by MSRC and referenced in the documentation below to alter the permission. + After having carefully studied the possible impact of the following change, apply the script made by MSRC and referenced in the documentation below to alter the permission. - A bogus Windows 2016 installation has granted to much right to the Enterprise Key Admins group + A bogus Windows 2016 installation has granted too many rights to the Enterprise Key Admins group After performing adprep /domainprep from Windows Server 2016 sources there may be an unwanted AccessControlEntry (ACE) in the DiscretionaryACL (DACL) of the targeted domain-naming-context's SecurityDescriptor (SD) that grants FullControl permission to the Enterprise Key Admins group ( SID = ending with -527 ). @@ -1676,7 +1687,7 @@ Note: The SID will only be resolvable after the PDC emulator role is transferred https://secureidentity.se/adprep-bug-in-windows-server-2016/ - The purpose is to ensure that the operator groups which can have indirect control to the domain are empty + The purpose is to ensure that the operator groups, which can have indirect control to the domain, are empty Group: {0} Member counts: {1} @@ -1688,10 +1699,10 @@ https://secureidentity.se/adprep-bug-in-windows-server-2016/ {count} operator group(s) are not empty - It is recommended to have these groups empty. Assign administrators into administrators group. Other accounts should have proper delegation rights in OU or in the scope they are managing. + It is recommended to have these groups empty. Assign administrators into administrators group. Other accounts should have proper delegation rights in an OU or in the scope they are managing. - Operator groups (account operators, server operators, ...) can take indirectly the control of the domain. Indeed these groups have write access to critical resources of the domain. + Operator groups (account operators, server operators, ...) can take indirect control of the domain. Indeed these groups have write access to critical resources of the domain. Check that operators group are empty @@ -1703,13 +1714,13 @@ https://secureidentity.se/adprep-bug-in-windows-server-2016/ - Number of gpo items that can be modified by any user: {count} + Number of GPO items that can be modified by any user: {count} Edit the Access Control List (ACL) of the GPO object or the directory where the items is located. Then remove any write permission given to the group. - When the group Authenticated Users, Everyone or any similar groups have permission to modify a GPO, it can be abused to take control of the accounts where this GPO applies. It can potentically lead to the compromise of the domain + When the group Authenticated Users, Everyone or any similar groups have permission to modify a GPO, it can be abused to take control of the accounts where this GPO applies. It can potentially lead to the compromise of the domain Ensure that GPO items cannot be modified by any user @@ -1739,10 +1750,10 @@ As an alternative, the file GptTmpl.inf can be manually edited. SeLoadDriverPrivilege can be used to take control of the system by loading a specifically designed driver. This procedure can be performed by low privileged users as the driver can be defined in HKCU. SeTcbPrivilege is the privilege used to "Act on behalf the operating system". This is the privilege reserved to the SYSTEM user. This procedure allow any users to act as SYSTEM. SeDebugPrivilege is the privilege used to debug program and to access any program's memory. It can be used to create a new process and set the parent process to a privileged one. -SeRestorePrivilege can be used to modifiy a service running as local system and startable by all users to a choosen one. +SeRestorePrivilege can be used to modify a service running as local system and startable by all users to a chosen one. SeBackupPrivilege can be used to backup Windows registry and use third party tools for extracting local NTLM hashes. -SeTakeOwnershipPrivilege can be used to take ownership of any securable object in the system including a service registry key. Then to change its ACL to define its own service running as LocalSystem. -SeCreateTokenPrivilege can be used to create a custom token with all privileges and thus be absued like SeTcbPrivilege +SeTakeOwnershipPrivilege can be used to take ownership of any secureable object in the system including a service registry key. Then to change its ACL to define its own service running as LocalSystem. +SeCreateTokenPrivilege can be used to create a custom token with all privileges and thus be abused like SeTcbPrivilege SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impersonate privileged tokens. These tokens can be retrieved by establishing security context such as Local DCOM DCE/RPC reflexion. @@ -1755,7 +1766,7 @@ SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impers The detail can be found in the <a href="#admincountequalsone">AdminSDHolder User List</a> - The detail can be found in <a href="#lsasettings">LSA settings</a> + The detail can be found in <a href="#lsasettings">Security settings</a> The detail can be found in <a href="#backup">Backup</a> @@ -1803,7 +1814,7 @@ SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impers The detail can be found in <a href="#laps">LAPS</a> - The detail can be found in <a href="#lsasettings">LSA settings</a> + The detail can be found in <a href="#lsasettings">Security settings</a> The detail can be found in <a href="#passwordpolicies">Password policies</a> @@ -1815,13 +1826,13 @@ SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impers The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> and <a href="#nullsession">Null Session</a> - The detail can be found in the <a href="#gpoobfuscatedpassword">Obfuscated Passwords</a> + The detail can be found in the <a href="#GPOobfuscatedpassword">Obfuscated Passwords</a> - The detail can be found in <a href="#lsasettings">LSA settings</a> + The detail can be found in <a href="#lsasettings">Security settings</a> - The detail can be found in <a href="#lsasettings">LSA settings</a> + The detail can be found in <a href="#lsasettings">Security settings</a> The schema version is indicated in <a href="#domaininformation">Domain Information</a> @@ -1961,4 +1972,459 @@ SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impers The SIDHistory detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> and a quick summary in <a href="#sidhistory">SID History</a> + + The purpose is to ensure that Exchange Installation did not introduce privilege escalation vulnerabilites by modifying domain permissions + + + https://github.com/gdedrouas/Exchange-AD-Privesc/blob/master/DomainObject/Fix-DomainObjectDACL.ps1 +https://blogs.technet.microsoft.com/exchange/2019/02/12/released-february-2019-quarterly-exchange-updates/ +https://support.microsoft.com/en-us/help/4490059/using-shared-permissions-model-to-run-exchange-server + + + The group Exchange Windows Permissions has the right to change the security descriptor of the domain root + + + Edit the root domain security descriptor. Identify the ACE giving the right ModifyDACL to the principal Exchange Windows Permissions. Go to the advanced settings and set the inheritance to Inherit Only. + +Or run the powershell script Fix-DomainObjectDACL.ps1 referenced below. + + + When Exchange is installed, a set of permissions is modified to allow a deep Windows integration. A dependancy analysis has shown that the permissions that Exchange set introduce privilege escalation. + The most basic exploitation is that a member of the group Exchange Windows Permissions can modified the security permission of the domain, granting itself the right Ds-Replication-Get-Changes-All. + This right allows the account to perform an attack named DCSync which retrieve the hash of the krbtgt account. With this hash, the attacker can then create a golden ticket and impersonate silently any user of the domain. + + + + Ensure that Exchange did not introduce security vulnerabilities + + + The purpose is to ensure that standard users cannot login to Domain Controllers + + + https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/allow-log-on-locally +https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/allow-log-on-through-remote-desktop-services +https://support.hpe.com/hpsc/doc/public/display?docId=emr_na-c04197764-1 + + + Anyone can interactively or remotely login to a DC + + + Locate the GPO specified in Details and remove the privilege "Allow log on locally" or "Allow log on through Remote Desktop Services" to "Everyone", "Authenticated Users", "Domain Users" or "Domain Computers". +The settings are located in : + Computer configuration -> Policies -> Windows Settings ->Security Settings -> Local Policies -> User Rights Assignment. +As an alternative, the file GptTmpl.inf can be manually edited. + + + Domain Controllers are critical components of the Active Directory. If an attacker is able to open a session, he will be able to discover unsecure backup media or perform a local privilege escalation to become the DC admin and thus the AD admin. + Local logon requires usually physical interaction, which explains why network seggregation is a best practice, but this can be bypassed. Indeed VNC or remote server management software is a way to perform local logon remotely. + In addition, remote server management software have been the subject of many vulnerabilites, some of them can be exploited even if this software is disabled. + + + Ensure that the privilege to log on Domain Controllers are not granted to everyone by GPO + + + GPO: {0} Account: {1} Privilege: {2} + + + The detail can be found in <a href="#gpoprivileges">Privileges</a> + + + The purpose is to ensure that the Recycle Bin feature is enabled + + + + + + The Recycle Bin is not enabled + + + First, be sure that the forest level is at least Windows 2008 R2. + You can check it with Get-ADForest or in the <a href="#domaininformation">Domain Information</a> section. + Then you can enable it using the powershell command: +Enable-ADOptionalFeature -identity 'CN=Recycle Bin Feature,CN=Optional Features,CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=test,DC=mysmartlogon,DC=com' -Scope ForestOrConfigurationSet -Target 'test.mysmartlogon.com' + + + The Recycle Bin avoids immediate deletion of objects (which can still be partially recovered by its tombstone). This lowers the administration work needed to restore. It also extends the period where traces are available when an investigation is needed. + + + Ensure that the Recycle Bin feature is enabled + + + The detail can be found in <a href="#domaininformation">Domain Information</a> + + + The purpose is to ensure that the AdminSDHolder mechanism has not been altered + + + https://www.petri.com/active-directory-security-understanding-adminsdholder-object +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e5899be4-862e-496f-9a38-33950617d2c5 + + + The AdminSDHolder safety mechanism has been modified for some privilege groups + + + Find the dsHeuristics configuration which is located in CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=ad,DC=contoso,DC=com. + Then edit the 16th character and set it to zero. + + + The AdminSDHolder service is a protection which prohibits an admin to loose control of the domain after a permission change or to introduce a weakness in the permissions. +It proceed by rewriting every 60 minutes the security descriptor of critical objects. + +By modifying the dsHeuristics attribute, this protection can be disabled for one or more critical group. +Each critical group is associated with a value: +Account Operators: 1, +Server Operators: 2, +Print Operators:4, +Backup Operators: 8. +The 16th character of dsHeuristics represents the sum of the values associated to the groups where the AdminSDHolder has been disabled. +To disable it for the 'Backup Operators' and the 'Server Operators', the value is 8 + 2 = 0x0A = 'a'. + + + + Ensure that the AdminSDHolder protection has not been disabled for some critical groups + + + The purpose is to check if the DoListObject feature has been enabled + + + https://dirteam.com/sander/2008/12/09/active-directory-visibility-modes/ +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e5899be4-862e-496f-9a38-33950617d2c5 +https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/990fb975-ab31-4bc1-8b75-5da132cd4584 + + + The DoListObject has been enabled + + + This is an informative rule. +If you want to reverse this behavior to its default value, find the dsHeuristics configuration which is located in CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=ad,DC=contoso,DC=com. +Then edit the 3rd character and set it to zero. + + + The DoListObject is a feature to probihit account located in an OU to look at another OU. It proceed by checking an special ACL named RIGHT_DS_LIST_OBJECT. + + + + Check if the behavior DoListObject has been enabled + + + The purpose is to check that it is not possible to go into recovery mode without the administrator password + + + https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/recovery-console-allow-automatic-administrative-logon + + + At least one GPO grant the right to get in the recovery mode without being admin + + + Locate the GPO specified in Details and turn off the setting "Recovery console: Allow automatic administrative logon" +The setting is located in : + Computer configuration -> Policies -> Windows Settings ->Security Settings -> Local Policies -> Security Options. +As an alternative, the file GptTmpl.inf can be manually edited. + + + The recovery mode is a special mode allowing an admin to fix an issue preventing the computer to boot. By pressing F8 in the short time span allowed, the computer boots with just a simple command line. + Usually, the administrator password is requested to avoid that people having physical access get control of it. It can typically be done by creating a new user account and add this account as member of the administrators group. This rule checks if there are any GPO which disable this password prompt. + + + Ensure the "automatic administrative logon" feature of the recovery mode is not enabled + + + GPO: {0} + + + The detail can be found in <a href="#lsasettings">Security settings</a> + + + The purpose is to check that the integrity of the network protocol LDAP as not been explicitly disabled. + + + https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/network-security-ldap-client-signing-requirements + + + At least one GPO disables explicitly LDAP client signature + + + Locate the GPO specified in Details and change the setting in "Network security: LDAP client signing requirements". + Disable this setting, or set it to "Negotiate signing" or "Require Signature". +The setting is located in : + Computer configuration -> Policies -> Windows Settings ->Security Settings -> Local Policies -> Security Options. +As an alternative, the file GptTmpl.inf can be manually edited. + + + The LDAP signature feature enables the integrity of the network communication between the computer and the domain controller. +Hackers aim at intercepting the communication at the network layer and modify the network dialog to grant themselves admin privileges. +The goal of this feature is to defeat these attacks. +Unfortunately, not all devices support LDAP signature. That's why the best practice is to Require Signature if possible or to, at least, try to negotiate it. +In this case, the LDAP signature feature is configured to None (no negotiation), which can enable hackers to perform their attacks. + + + Ensure LDAP signing requirements is not set to None + + + GPO: {0} + + + The detail can be found in <a href="#lsasettings">Security settings</a> + + + The purpose is to check that the computer account password can be changed as usual. + + + https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/domain-controller-refuse-machine-account-password-changes + + + At least one GPO disables explicitly the change of the computer account password + + + Locate the GPO specified in Details and change the setting in "Domain controller: Refuse machine account password changes". + Disable this setting, or set it to "Disabled". +The setting is located in : + Computer configuration -> Policies -> Windows Settings ->Security Settings -> Local Policies -> Security Options. +As an alternative, the file GptTmpl.inf can be manually edited. + + + For each computer, there is a hidden user account. This account is used to maintain the computer inside the Active Directory domain. + The password of this account is changed every 30 days automatically except if the Domain Controller prohibits this. + This is the case when the GPO is enabled. + + + Ensure that Domain Controllers don't deny the change of computers account password. + + + GPO: {0} + + + The detail can be found in <a href="#lsasettings">Security settings</a> + + + The purpose is to check that files deployed to computers cannot be changed by everyone. + + + + + + At least one GPO is deploying a file which can be modified by everyone + + + Locate the file mentionned by the GPO specified in Details and change its permissions. + + + Application provided in a msi form or general files can be deployed by a GPO. If an attacker can modify one of this file, it can take control of the user account. + + + Ensure that file deployed by a GPO cannot be modified by everyone. + + + GPO: {0} Type: {1} FileName: {2} Account: {3} Right: {4} + + + The detail can be found in <a href="#gpodeployedfiles">GPO Deployed Files</a> + + + The purpose is to ensure that a compromised domain cannot use file deployed by GPO to compromise other domains + + + Copy the file to a share located inside the domain and not in trusted domains. + + + Number of files deployed hosted in another domain: {count} + + + Server: {0} found in GPO: {1} with File: {2} + + + Files deployed (Application as msi, file copied by GPO, ...) can be stored in any file share available in the network and that includes trusted domains shares. If such file is located in a compromise domain, it can be used to compromise other domains. + + + + + + Check if files deployed may be located in a trusted domain + + + The detail can be found in <a href="#gpodeployedfiles">GPO Deployed Files</a> + + + The purpose is to ensure that local name resolution protocol (LLMNR) cannot be used to collect credentials by performing a network attack + + + Enable the GPO <a href="https://getadmx.com/?Category=Windows_10_2016&Policy=Microsoft.Policies.DNSClient::Turn_Off_Multicast">Turn off multicast name resolution</a> and check that no GPO override this setting. + (if it is the case, the policy involved will be displayed below) + + + No GPO has been found which disables LLMNR or at least one GPO does enable it explicitly + + + GPO overriding the setting: {0} + + + LLMNR is a protocol which translates names such as foo.bar.com into an ip address. LLMNR has been designed to translate name locally in case the default protocol DNS is not available. + Regarding Active Directory, DNS is mandatory which makes LLMNR useless. + LLMNR exploits typo mistakes or faster response time to redirect users to a specially designed share, server or website. + Being trusted, this service will trigger the single sign on procedure which can be abused to retrieve the user credentials. + + LLMNR is enabled by default on all OS except starting from Windows 10 v1903 and Windows Server v1903 where it is disabled. + + + + https://youtu.be/Fg2gvk0qgjM + + + Check if LLMNR can be used to steal credentials + + + The detail can be found in <a href="#lsasettings">Security settings</a> + + + By reusing existing objects, whose credentials may be the same among all objects or stored on configuration files or in memory, a third party can take them over. + + + Patching computers is part of the security process. Unpatched vulnerability is a way to gain control of a computer. + + + Active Directory uses a distributed architecture to have a high level of availability. This architecture replicates each change at a regular interval. Collision of changes can create unexpected objects which can be used later. + + + It is important to control who can create new objects in the Active Directory. Indeed, its owner may introduced an object in which it has a strong control. + + + Cryptography and computer power have evolved during the time and the oldest protocols do not provide the same level of security anymore. They can be broken and used to gain control of the domain. + + + Operating systems have a lifecycle where its manufacturer provides patches. If the operating system is not supported anymore, vulnerabilities are not fixed anymore. + + + By abusing a misconfiguration, an attacker can gain the control of the domain. + + + It is important to have a database of all the assets and control the physical security of the server. If one server is compromised physically, all the secrets of the domain can be exposed. + + + It is important to know how much administrators are in place and to track the use of emergency accounts + + + Privileges are granted to special groups to perform their duty. Sometimes, these privileges can be used to take control of the domain. + + + Delegation is used to perform day to day activities. It is important to control it. + + + Most of the changes can be reversed. Some not, and it can broke the domain. + + + Isolation of domain is critical to avoid a global compromission. + + + Any trust introduce a risk. The secret used for the trust can be exposed to take control of the domain. + + + A trust is a technical boundary which should not be altered. + + + When doing migrations, a double identity may be attributed. It can have side effects up to the compromission of the domain. + + + NT4 like trusts do not provide an accurate level of security and by the use of its old protocols, put the domain at risk. + + + At the begining of an attack, a hacker try to collect as much data as possible. Leaking information just reduce the time an attacker needs to gain control of the domain. + + + the GPO deploy settings which are applied to computers locally and it can be abuse to take control of individual computers. + + + Passwords stored in clear text or obfuscated can be retrieved. By reusing the user's identity, an attacker does not need to perform attack and it is difficult to detect it. + + + Misprotected credentials can be abused to be retrieved in plain text and then, impersonate the user. + + + Administrators grant sometimes privileged rights to colleagues without any approval from a security officer. + + + Network attacks such as interception or modification can be used to run commands on behalf an administrator. + + + Certificates are an alternative to passwords. Their protection is crucial to avoid any backdoor. + + + If the password is a secret which protects, its derivatives, such as the fingerprint named hash, can be used as if it was the password itself. + + + There are key secrets in Active Directory which provides seed to the cryptographic process. A leak can lead to a total compromise of a domain. + + + Althought Active Directory has been designed for redundancy, a backup process is key for a recovery plan. + + + The purpose is to ensure that a forest cannot be used to compromise another forest using kerberos delegation + + + TGT Delegation on forest trusts should be disabled, except for migrations. +You can use netdom to turn the TGT delegation on forest trust OFF. +Example: netdom.exe trust fabrikam.com /domain:contoso.com /EnableTGTDelegation:No +As an alternative, you can locate the forest trust and change its LDAP trustattribute from the value 8 to the value 520. + +The impact is to have non working services which relies on unconstrained delegation. Resource based delegation is not impacted. + +See the official Microsoft recommandations and a script to find potentially impacted services in the links below. + + + At least one forest trust has been found where TGT delegation over forest trust is allowed + + + Forest trust misconfigured: {0} + + + A Forest trust is a link between two forests. By default, this trust is secure and prohibits SID History attacks. +However, it allows kerberos delegation by default. +By configuring an uncontrainst delegation on forest A, an attacker located in forest A can collect admin or domain controller credentials, the TGT of the session, of the forest B. +This collection can be forced by using services such as the printer spooler, enabled by default on all domain controllers. +Having collected this TGT, the attacker can then request access to other systems in forest B, by asking for a TGS given the TGT, and then gain control of the whole forest. + + + http://www.harmj0y.net/blog/redteaming/not-a-security-boundary-breaking-forest-trusts/ +https://techcommunity.microsoft.com/t5/Premier-Field-Engineering/Changes-to-Ticket-Granting-Ticket-TGT-Delegation-Across-Trusts/ba-p/440283/tab/rich +https://support.microsoft.com/en-us/help/4490425/updates-to-tgt-delegation-across-incoming-trusts-in-windows-server + + + Check if kerberos delegation can be used to take control of the forest from a trusted forest + + + The detail can be found in <a href="#discovereddomains">Trusts section</a> + + + Members of administrators' groups are a priority target. By misconfiguring their protection, the password of the account can be retrieved by an attacker or it can leverage internal mechanisms of the AD such authentication to act on its behalf. + + + The purpose is to ensure that the password of admin accounts cannot be retrieved using the kerberoast attack. + + + If the account is a service account, the service should be removed from the privileged group or have a process to change it at a regular basis. +If the user is a person, the SPN attribute of the account should be removed. + + + At least one member of an admin group is vulnerable to the kerberoast attack. + + + Group: {0} User: {1} + + + To access a service using kerberos, a user does request a ticket (named TGS) to the DC specific to the service. +However this ticket is encrypted using a derivative of the service password. This ticket can then be brute-forced to retrieve the original password. +Any account having the attribute SPN populated is considered as a service account. +Given the fact that any user can request a ticket for service account, these accounts can have their password retrieved. +In addition, services are known to have their password not changed at a regular basis and to use well-known words. + +Please note that this program skips service accounts having their password changed for less than 40 days ago to allow a mitigation using a password change process. + + + https://adsecurity.org/?p=3466 + + + Check if admin accounts are vulnerable to the kerberoast attack. + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + diff --git a/Healthcheck/TrustAnalyzer.cs b/Healthcheck/TrustAnalyzer.cs index d7c8048..aca41f3 100644 --- a/Healthcheck/TrustAnalyzer.cs +++ b/Healthcheck/TrustAnalyzer.cs @@ -41,7 +41,12 @@ static public string GetSIDFiltering(int TrustDirection, int TrustAttributes) return "No"; return "Yes"; } - else + // tree root which is obsolete + else if (IsFlagSet(TrustAttributes, 0x00800000)) + { + return "Not applicable"; + } + else { // quarantined ? if (IsFlagSet(TrustAttributes, 4)) @@ -50,6 +55,27 @@ static public string GetSIDFiltering(int TrustDirection, int TrustAttributes) } } + static public string GetTGTDelegation(HealthCheckTrustData trust) + { + return GetTGTDelegation(trust.TrustDirection, trust.TrustAttributes); + } + + static public string GetTGTDelegation(int TrustDirection, int TrustAttributes) + { + if (TrustDirection == 0 || TrustDirection == 2) + { + return "Not applicable"; + } + if (IsFlagSet(TrustAttributes, 8)) + { + // quarantined ? + if (!IsFlagSet(TrustAttributes, 0x200)) + return "Yes"; + return "No"; + } + return "Not applicable"; + } + static public string GetTrustAttribute(int trustAttributes) { List attributes = new List(); diff --git a/NativeMethods.cs b/NativeMethods.cs index cc35f2b..0f3414f 100644 --- a/NativeMethods.cs +++ b/NativeMethods.cs @@ -62,8 +62,19 @@ public static WindowsIdentity GetWindowsIdentityForUser(NetworkCredential creden ref uint cchReferencedDomainName, out SID_NAME_USE peUse); + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool LookupAccountName( + string lpSystemName, + string lpAccountName, + [MarshalAs(UnmanagedType.LPArray)] byte[] Sid, + ref uint cbSid, + StringBuilder ReferencedDomainName, + ref uint cchReferencedDomainName, + out SID_NAME_USE peUse); + const int NO_ERROR = 0; const int ERROR_INSUFFICIENT_BUFFER = 122; + const int ERROR_INVALID_FLAGS = 1004; public enum SID_NAME_USE { @@ -84,6 +95,36 @@ public static string ConvertSIDToName(string sidstring, string server) return ConvertSIDToName(sidstring, server, out referencedDomain); } + public static SecurityIdentifier ConvertNameToSID(string accountName, string server) + { + byte [] Sid = null; + uint cbSid = 0; + StringBuilder referencedDomainName = new StringBuilder(); + uint cchReferencedDomainName = (uint)referencedDomainName.Capacity; + SID_NAME_USE sidUse; + + int err = NO_ERROR; + if (LookupAccountName(server, accountName, Sid, ref cbSid, referencedDomainName, ref cchReferencedDomainName, out sidUse)) + { + return new SecurityIdentifier(Sid, 0); + } + else + { + err = Marshal.GetLastWin32Error(); + if (err == ERROR_INSUFFICIENT_BUFFER || err == ERROR_INVALID_FLAGS) + { + Sid = new byte[cbSid]; + referencedDomainName.EnsureCapacity((int)cchReferencedDomainName); + err = NO_ERROR; + if (LookupAccountName(null, accountName, Sid, ref cbSid, referencedDomainName, ref cchReferencedDomainName, out sidUse)) + { + return new SecurityIdentifier(Sid, 0); + } + } + } + return null; + } + [EnvironmentPermissionAttribute(SecurityAction.Demand, Unrestricted = true)] public static string ConvertSIDToName(string sidstring, string server, out string referencedDomain) { @@ -260,7 +301,7 @@ internal struct LSA_OBJECT_ATTRIBUTES } [DllImport("advapi32.dll")] - internal static extern int LsaOpenPolicy( + internal static extern uint LsaOpenPolicy( ref UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, @@ -268,7 +309,7 @@ internal struct LSA_OBJECT_ATTRIBUTES ); [DllImport("advapi32.dll")] - internal static extern int LsaClose(IntPtr ObjectHandle); + internal static extern uint LsaClose(IntPtr ObjectHandle); [StructLayout(LayoutKind.Sequential)] internal struct LSA_TRUST_INFORMATION @@ -278,7 +319,7 @@ internal struct LSA_TRUST_INFORMATION } [DllImport("advapi32.dll")] - internal static extern int LsaEnumerateTrustedDomains( + internal static extern uint LsaEnumerateTrustedDomains( IntPtr PolicyHandle, ref IntPtr EnumerationContext, out IntPtr Buffer, @@ -335,7 +376,7 @@ internal struct LSA_FOREST_TRUST_RECORD { } [DllImport("advapi32.dll", SetLastError = true)] - internal static extern int LsaLookupSids( + internal static extern uint LsaLookupSids( IntPtr PolicyHandle, int Count, IntPtr ptrEnumBuf, @@ -344,9 +385,9 @@ internal struct LSA_FOREST_TRUST_RECORD { ); [DllImport("advapi32")] - internal static extern int LsaLookupNames( + internal static extern uint LsaLookupNames( IntPtr PolicyHandle, - uint Count, + int Count, UNICODE_STRING[] Names, out IntPtr ReferencedDomains, out IntPtr Sids @@ -367,6 +408,14 @@ public struct LSA_TRANSLATED_NAME public int DomainIndex; } + [StructLayout(LayoutKind.Sequential)] + public struct LSA_TRANSLATED_SID + { + public SID_NAME_USE Use; + public uint RelativeId; + public int DomainIndex; + } + [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] public static SecurityIdentifier GetSidFromDomainName(string server, string domainToResolve) { @@ -374,7 +423,7 @@ public static SecurityIdentifier GetSidFromDomainName(string server, string doma NativeMethods.LSA_OBJECT_ATTRIBUTES loa = new NativeMethods.LSA_OBJECT_ATTRIBUTES(); us.Initialize(server); IntPtr PolicyHandle = IntPtr.Zero; - int ret = NativeMethods.LsaOpenPolicy(ref us, ref loa, 0x00000800, out PolicyHandle); + uint ret = NativeMethods.LsaOpenPolicy(ref us, ref loa, 0x00000800, out PolicyHandle); if (ret != 0) { Trace.WriteLine("LsaOpenPolicy 0x" + ret.ToString("x")); diff --git a/PingCastle.csproj b/PingCastle.csproj index 3272485..86cd1fe 100644 --- a/PingCastle.csproj +++ b/PingCastle.csproj @@ -37,6 +37,7 @@ DEBUG;TRACE prompt 4 + 0436 AnyCPU @@ -80,6 +81,7 @@ + @@ -101,6 +103,7 @@ + @@ -131,6 +134,19 @@ + + + + + + + + + + + + + @@ -139,6 +155,7 @@ + @@ -146,11 +163,14 @@ + + + @@ -231,6 +251,8 @@ + + @@ -352,8 +374,18 @@ - - + if not exist $(SolutionDir)\CodeSigning\certificate.pfx goto :exit + +@echo ================ +@echo sign binaries +@echo ================ + +IF "$(ConfigurationName)" == "Release" "C:\Program Files (x86)\Windows Kits\8.0\bin\x86\signtool.exe" sign /d PingCastle /ac $(SolutionDir)\CodeSigning\addtrustexternalcaroot_kmod.crt /f $(SolutionDir)\CodeSigning\certificate.pfx /p vletoux /t http://timestamp.comodoca.com "$(TargetPath)" +IF "$(ConfigurationName)" == "Release" "C:\Program Files (x86)\Windows Kits\8.0\bin\x86\signtool.exe" sign /d PingCastle /ac $(SolutionDir)\CodeSigning\addtrustexternalcaroot_kmod.crt /f $(SolutionDir)\CodeSigning\certificate.pfx /p vletoux /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 /as "$(TargetPath)" + +@echo Done signing +:exit + + \ No newline at end of file diff --git a/PingCastleAutoUpdater/Program.cs b/PingCastleAutoUpdater/Program.cs new file mode 100644 index 0000000..1284e57 --- /dev/null +++ b/PingCastleAutoUpdater/Program.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.UI; +using System.Web.Script.Serialization; +using System.Net; +using System.IO; +using System.Reflection; +using System.IO.Compression; +using System.Diagnostics; + +namespace PingCastleAutoUpdater +{ + class Program + { + class Release + { + public string name { get; set; } + public bool prerelease { get; set; } + public DateTime published_at { get; set; } + public List assets { get; set; } + } + + class Asset + { + public string name { get; set; } + public int size { get; set; } + public string browser_download_url { get; set; } + } + + const string fileNameLastDownload = "LastDownloadedRelease.txt"; + + static void Main(string[] args) + { + Program program = new Program(); + program.Run(args); + } + + bool forceDownload = false; + bool preview = false; + int numberOfDaysToWay = 0; + string releaseInfoUrl = "https://api.github.com/repos/vletoux/pingcastle/releases"; + + void Run(string[] args) + { + Trace.WriteLine("Before parsing arguments"); + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--api-url": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --api-url is mandatory"); + return; + } + releaseInfoUrl = args[++i]; + break; + case "--force-download": + forceDownload = true; + break; + case "--help": + DisplayHelp(); + return; + case "--use-preview": + preview = true; + break; + case "--wait-for-days": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --wait-for-days is mandatory"); + return; + } + { + if (!int.TryParse(args[++i], out numberOfDaysToWay)) + { + WriteInRed("argument for --wait-for-days is not a valid value (typically: 30)"); + return; + } + } + break; + default: + WriteInRed("unknow argument: " + args[i]); + DisplayHelp(); + return; + } + } + Console.WriteLine("Do not forget that there are other command line switches like --help that you can use"); + Console.WriteLine("Running on " + Environment.Version); + Console.WriteLine(); + Console.WriteLine("Getting the list of releases"); + string releaseInfo = GET(releaseInfoUrl); + Console.WriteLine("Done"); + string lastRelease = null; + if (File.Exists(fileNameLastDownload) && !forceDownload) + { + if (forceDownload) + { + Console.WriteLine("Download is forced"); + } + lastRelease = File.ReadAllText(fileNameLastDownload); + Console.WriteLine("Current release is: " + lastRelease); + } + else + { + Console.WriteLine("No previous download"); + } + + JavaScriptSerializer jsonSerializer = new JavaScriptSerializer(); + IEnumerable releases = jsonSerializer.Deserialize>(releaseInfo); + if (numberOfDaysToWay > 0) + { + Console.WriteLine("Only releases older than " + numberOfDaysToWay + " day(s) are selected"); + releases = releases.Where(r => r.published_at.AddDays(numberOfDaysToWay) < DateTime.Now); + } + if (!preview) + { + releases = releases.Where(r => r.prerelease == false); + } + else + { + Console.WriteLine("Prerelease are included"); + } + releases = releases.OrderByDescending(i => i.published_at); + if (releases.Count() == 0) + { + Console.WriteLine("There is no release matching the requirements"); + return; + } + Release release = releases.First(); + Console.WriteLine("Latest release is: " + release.name); + if (release.name == lastRelease) + { + Console.WriteLine("This is the latest one. Program is stopping."); + return; + } + string downloadUrl = release.assets.First().browser_download_url; + Console.WriteLine("Downloading " + downloadUrl); + + ProceedReleaseInstall(downloadUrl); + + // success ! + Console.WriteLine("Saving status"); + File.WriteAllText(fileNameLastDownload, release.name); + Console.WriteLine("Update with success!"); + } + + private static void WriteInRed(string data) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(data); + Trace.WriteLine("[Red]" + data); + Console.ResetColor(); + } + + // Returns JSON string + static string GET(string url) + { + // github forces TLS 1.2 which is not enabled by default in .net + System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + Version version = Assembly.GetExecutingAssembly().GetName().Version; + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.UserAgent = "PingCastleAutoUpdater " + version.ToString(); + try + { + WebResponse response = request.GetResponse(); + using (Stream responseStream = response.GetResponseStream()) + { + StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.UTF8); + return reader.ReadToEnd(); + } + } + catch (WebException ex) + { + WebResponse errorResponse = ex.Response; + if (errorResponse != null) + { + using (Stream responseStream = errorResponse.GetResponseStream()) + { + StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding("utf-8")); + String errorText = reader.ReadToEnd(); + Console.WriteLine(errorText); + // log errorText + } + } + throw; + } + } + + static void ProceedReleaseInstall(string url) + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.UserAgent = "PingCastleAutoUpdater " + version.ToString(); + try + { + WebResponse response = request.GetResponse(); + using (Stream responseStream = response.GetResponseStream()) + using (var archive = new ZipArchive(responseStream, ZipArchiveMode.Read)) + { + foreach (var entry in archive.Entries) + { + // do not save .config file except if it doesn't exists + // and do not overwrite the updater file because it's running ! + if (entry.FullName.EndsWith(".config", StringComparison.OrdinalIgnoreCase)) + { + if (!File.Exists(entry.FullName)) + { + performCopy(entry); + } + } + else + { + performCopy(entry); + } + } + } + } + catch (WebException ex) + { + WebResponse errorResponse = ex.Response; + using (Stream responseStream = errorResponse.GetResponseStream()) + { + StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding("utf-8")); + String errorText = reader.ReadToEnd(); + Console.WriteLine(errorText); + // log errorText + } + throw; + } + } + + static void performCopy(ZipArchiveEntry entry) + { + using (var e = entry.Open()) + { + Console.WriteLine("Saving " + entry.FullName); + if (File.Exists(entry.FullName)) + { + string exePath = new Uri(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).LocalPath; + // if we try to overwrite the current exe, it will fail + // the trick is to move the current assembly to a new file + if (string.Compare(new FileInfo(entry.FullName).FullName,exePath,StringComparison.OrdinalIgnoreCase) == 0) + { + string bakFileName = entry.FullName + ".bak"; + if (File.Exists(bakFileName)) + File.Delete(bakFileName); + File.Move(entry.FullName, bakFileName); + } + } + using (var fileStream = File.Create(entry.FullName)) + { + e.CopyTo(fileStream); + fileStream.Close(); + } + + } + } + + private static void DisplayHelp() + { + Console.WriteLine("switch:"); + Console.WriteLine(" --help : display this message"); + Console.WriteLine(""); + Console.WriteLine(" --api-url http://xx : use an alternative url for checking for updates"); + Console.WriteLine(" --force-download : download the latest release even if it is not the most recent. Useful for tests"); + Console.WriteLine(" --use-preview : download preview release if it is the most recent"); + Console.WriteLine(" --wait-for-days 30 : ensure the releases has been made public for at least X days"); + } + } +} diff --git a/PingCastleAutoUpdater/Properties/AssemblyInfo.cs b/PingCastleAutoUpdater/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7334f53 --- /dev/null +++ b/PingCastleAutoUpdater/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Les informations générales relatives à un assembly dépendent de +// l'ensemble d'attributs suivant. Changez les valeurs de ces attributs pour modifier les informations +// associées à un assembly. +[assembly: AssemblyTitle("PingCastleAutoUpdater")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PingCastleAutoUpdater")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// L'affectation de la valeur false à ComVisible rend les types invisibles dans cet assembly +// aux composants COM. Si vous devez accéder à un type dans cet assembly à partir de +// COM, affectez la valeur true à l'attribut ComVisible sur ce type. +[assembly: ComVisible(false)] + +// Le GUID suivant est pour l'ID de la typelib si ce projet est exposé à COM +[assembly: Guid("74ba30cc-dda4-4174-9ae4-04262720d773")] + +// Les informations de version pour un assembly se composent des quatre valeurs suivantes : +// +// Version principale +// Version secondaire +// Numéro de build +// Révision +// +// Vous pouvez spécifier toutes les valeurs ou indiquer les numéros de build et de révision par défaut +// en utilisant '*', comme indiqué ci-dessous : +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PingCastleAutoUpdater/pingcastle.ico b/PingCastleAutoUpdater/pingcastle.ico new file mode 100644 index 0000000000000000000000000000000000000000..afa4377fb3f0d6a96ff5c26555b0791670159aaf GIT binary patch literal 34063 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 Q-)`@^7mTz@H0;9u2V(Wp&j0`b literal 0 HcmV?d00001 diff --git a/Program.cs b/Program.cs index c1f82ba..c3a2ea1 100644 --- a/Program.cs +++ b/Program.cs @@ -39,6 +39,7 @@ public class Program : IPingCastleLicenseInfo bool PerformCarto = false; bool PerformAdvancedLive; bool PerformUploadAllReport; + bool PerformHCRules = false; private bool PerformRegenerateReport; private bool PerformHealthCheckReloadReport; bool PerformHealthCheckGenerateDemoReports; @@ -139,7 +140,7 @@ private void Run(string[] args) # @@ > " + (license.EndTime < DateTime.MaxValue ? "End of support: " + license.EndTime.ToShortDateString() : "") + @" | @@@: : .# Vincent LE TOUX (contact@pingcastle.com) -.: https://www.pingcastle.com"; + .: twitter: @mysmartlogon https://www.pingcastle.com"; if (!ParseCommandLine(args)) return; // Trace to file or console may be enabled here @@ -183,6 +184,10 @@ private void Run(string[] args) { if (!tasks.ConsolidationTask()) return; } + if (PerformHCRules) + { + if (!tasks.HealthCheckRulesTask()) return; + } if (PerformRegenerateReport) { if (!tasks.RegenerateHtmlTask()) return; @@ -202,11 +207,13 @@ private void Run(string[] args) tasks.CompleteTasks(); } + const string basicEditionLicense = "PC2H4sIAAAAAAAEAO29B2AcSZYlJi9tynt/SvVK1+B0oQiAYBMk2JBAEOzBiM3mkuwdaUcjKasqgcplVmVdZhZAzO2dvPfee++999577733ujudTif33/8/XGZkAWz2zkrayZ4hgKrIHz9+fB8/In7NX+PX+DV+A/r/r/EH/Un/wZPFv/lr/tr060v6//Nfo/g1pr9G/mssf42G/k1/jfNfo/o1avr5kj5f/hoXv8bJr5HRN+2vUfK3e7/G+Nd4QD+f8Kd4M/01Tn+NGf3W0v+rX2P5a6CLP+jX+DV+jd//J/+4//TP+Ymf+Qv+hH/+X9j69L/81/6F//L3+5d3zl79T/P/6z/4s/+v/+nX/L9/5rfb/Wv/4d9n8k++/qW/9D/49p/2b/zPP/kXfPUHz+/+Z3/tPzj+K/78F7/5H/pn/R//+7/13/zdO//R//yLv731+/3Uv/9X/rO/3V/V/pd/1Ce/+W91cf9X/rEPf6vzf3j5T3+x9zf9KQ8X//x/9Tpf/QX/6/2/+0/55dv/0PFP/XZ//eFf+ev+on/6Z/6W/+P/AY4sXy/wAAAA"; string _serialNumber; public string GetSerialNumber() { if (String.IsNullOrEmpty(_serialNumber)) { + // try to load it from the configuration file try { _serialNumber = ADHealthCheckingLicenseSettings.Settings.License; @@ -221,9 +228,40 @@ public string GetSerialNumber() Trace.WriteLine(ex.InnerException.Message); Trace.WriteLine(ex.InnerException.StackTrace); } - throw new PingCastleException("Unable to load the license from the .config file. Check that all files have been copied in the same directory"); + + } + if (!String.IsNullOrEmpty(_serialNumber)) + { + try + { + var license = new ADHealthCheckingLicense(_serialNumber); + return _serialNumber; + } + catch (Exception ex) + { + _serialNumber = null; + Trace.WriteLine("Exception when verifying the external license"); + Trace.WriteLine(ex.Message); + Trace.WriteLine(ex.StackTrace); + if (ex.InnerException != null) + { + Trace.WriteLine(ex.InnerException.Message); + Trace.WriteLine(ex.InnerException.StackTrace); + } + } + } } + // fault back to the default license: + _serialNumber = basicEditionLicense; + try + { + var license = new ADHealthCheckingLicense(_serialNumber); + } + catch (Exception) + { + throw new PingCastleException("Unable to load the license from the .config file and the license embedded in PingCastle is not valid. Check that all files have been copied in the same directory and that you have a valid license"); + } return _serialNumber; } @@ -502,6 +540,9 @@ private bool ParseCommandLine(string[] args) case "--reachable": tasks.AnalyzeReachableDomains = true; break; + case "--rules": + PerformHCRules = true; + break; case "--scanner": if (i + 1 >= args.Length) { @@ -529,7 +570,7 @@ private bool ParseCommandLine(string[] args) } break; case "--scmode-single": - ScannerBase.ScanningMode = 1; + ScannerBase.ScanningMode = 2; break; case "--sendxmlTo": case "--sendXmlTo": @@ -663,7 +704,8 @@ private bool ParseCommandLine(string[] args) && !PerformRegenerateReport && !PerformHealthCheckReloadReport && !delayedInteractiveMode && !PerformScanner && !PerformGenerateKey && !PerformHealthCheckGenerateDemoReports && !PerformCarto && !PerformAdvancedLive - && !PerformUploadAllReport) + && !PerformUploadAllReport + && !PerformHCRules) { WriteInRed("You must choose at least one value among --healthcheck --hc-conso --advanced-export --advanced-report --nullsession --carto"); DisplayHelp(); @@ -787,13 +829,13 @@ DisplayState DisplayMainMenu() PerformHealthCheckConsolidation = false; PerformScanner = false; - List> choices = new List>() { - new KeyValuePair("healthcheck","Score the risk of a domain"), - new KeyValuePair("graph","Analyze admin groups and delegations with diagrams"), - new KeyValuePair("conso","Aggregate multiple reports into a single one"), - new KeyValuePair("carto","Build a map of all interconnected domains"), - new KeyValuePair("scanner","Perform specific security checks on workstations"), - new KeyValuePair("advanced","Open the advanced menu"), + List choices = new List() { + new ConsoleMenuItem("healthcheck","Score the risk of a domain", "This is the main functionnality of PingCastle. In a matter of minutes, it produces a report which will give you an overview of your Active Directory security. This report can be generated on other domains by using the existing trust links."), + new ConsoleMenuItem("permissions","Analyze admin groups and delegations with diagrams", "Once you have run the healthcheck report and apply all the recommandations, you can run this report to get deeper into the permission model."), + new ConsoleMenuItem("conso","Aggregate multiple reports into a single one", "With many healthcheck reports, you can get a single report for a whole scope. Maps will be generated."), + new ConsoleMenuItem("carto","Build a map of all interconnected domains", "It combines the healthcheck reports that would be run on all trusted domains and then the conso option. But lighter and then faster."), + new ConsoleMenuItem("scanner","Perform specific security checks on workstations", "You can know your local admins, if Bitlocker is properly configured, discover unprotect shares, ... A menu will be shown to select the right scanner."), + new ConsoleMenuItem("advanced","Open the advanced menu", "This is the place you want to configure PingCastle without playing with command line switches."), }; ConsoleMenu.Title = "What do you want to do?"; @@ -802,14 +844,14 @@ DisplayState DisplayMainMenu() if (choice == 0) return DisplayState.Exit; - string whattodo = choices[choice - 1].Key; + string whattodo = choices[choice - 1].Choice; switch (whattodo) { default: case "healthcheck": PerformHealthCheckReport = true; return DisplayState.AskForServer; - case "graph": + case "permissions": PerformAdvancedLive = true; return DisplayState.AskForServer; case "carto": @@ -831,18 +873,18 @@ DisplayState DisplayScannerMenu() { var scanners = PingCastleFactory.GetAllScanners(); - var choices = new List>(); + var choices = new List(); foreach (var scanner in scanners) { Type scannerType = scanner.Value; IScanner iscanner = PingCastleFactory.LoadScanner(scannerType); string description = iscanner.Description; - choices.Add(new KeyValuePair(scanner.Key, description)); + choices.Add(new ConsoleMenuItem(scanner.Key, description)); } - choices.Sort((KeyValuePair a, KeyValuePair b) + choices.Sort((ConsoleMenuItem a, ConsoleMenuItem b) => { - return String.Compare(a.Key, b.Key); + return String.Compare(a.Choice, b.Choice); } ); ConsoleMenu.Notice = "WARNING: Checking a lot of workstations may raise security alerts."; @@ -851,7 +893,7 @@ DisplayState DisplayScannerMenu() int choice = ConsoleMenu.SelectMenuCompact(choices, 1); if (choice == 0) return DisplayState.Exit; - tasks.Scanner = scanners[choices[choice - 1].Key]; + tasks.Scanner = scanners[choices[choice - 1].Choice]; return DisplayState.AskForScannerParameter; } @@ -909,13 +951,16 @@ DisplayState DisplayAdvancedMenu() PerformGenerateKey = false; PerformHealthCheckReloadReport = false; PerformRegenerateReport = false; + PerformHCRules = false; - List> choices = new List>() { - new KeyValuePair("protocol","Change the protocol used to query the AD (LDAP, ADWS, ...)"), - new KeyValuePair("generatekey","Generate RSA keys used to encrypt and decrypt reports"), - new KeyValuePair("decrypt","Decrypt a xml report"), - new KeyValuePair("regenerate","Regenerate the html report based on the xml report"), - new KeyValuePair("log","Enable logging (log is " + (Trace.Listeners.Count > 1 ? "enabled":"disabled") + ")"), + List choices = new List() { + new ConsoleMenuItem("protocol","Change the protocol used to query the AD (LDAP, ADWS, ...)"), + new ConsoleMenuItem("hcrules","Generate a report containing all rules applied by PingCastle"), + new ConsoleMenuItem("generatekey","Generate RSA keys used to encrypt and decrypt reports"), + new ConsoleMenuItem("noenumlimit","Remove the 100 items limitation in healthcheck reports"), + new ConsoleMenuItem("decrypt","Decrypt a xml report"), + new ConsoleMenuItem("regenerate","Regenerate the html report based on the xml report"), + new ConsoleMenuItem("log","Enable logging (log is " + (Trace.Listeners.Count > 1 ? "enabled":"disabled") + ")"), }; ConsoleMenu.Title = "What do you want to do?"; @@ -923,12 +968,15 @@ DisplayState DisplayAdvancedMenu() if (choice == 0) return DisplayState.Exit; - string whattodo = choices[choice - 1].Key; + string whattodo = choices[choice - 1].Choice; switch (whattodo) { default: case "protocol": return DisplayState.ProtocolMenu; + case "hcrules": + PerformHCRules = true; + return DisplayState.Run; case "generatekey": PerformGenerateKey = true; return DisplayState.Run; @@ -942,16 +990,20 @@ DisplayState DisplayAdvancedMenu() if (Trace.Listeners.Count <= 1) EnableLogFile(); return DisplayState.Exit; + case "noenumlimit": + ReportHealthCheckSingle.MaxNumberUsersInHtmlReport = int.MaxValue; + ConsoleMenu.Notice = "Limitation removed"; + return DisplayState.Exit; } } DisplayState DisplayProtocolMenu() { - List> choices = new List>() { - new KeyValuePair("ADWSThenLDAP","default: ADWS then if failed, LDAP"), - new KeyValuePair("ADWSOnly","use only ADWS"), - new KeyValuePair("LDAPOnly","use only LDAP"), - new KeyValuePair("LDAPThenADWS","LDAP then if failed, ADWS"), + List choices = new List() { + new ConsoleMenuItem("ADWSThenLDAP","default: ADWS then if failed, LDAP"), + new ConsoleMenuItem("ADWSOnly","use only ADWS"), + new ConsoleMenuItem("LDAPOnly","use only LDAP"), + new ConsoleMenuItem("LDAPThenADWS","LDAP then if failed, ADWS"), }; ConsoleMenu.Title = "What protocol do you want to use?"; @@ -959,14 +1011,14 @@ DisplayState DisplayProtocolMenu() int defaultChoice = 1; for (int i = 0; i < choices.Count; i++) { - if (choices[i].Key == ADWebService.ConnectionType.ToString()) + if (choices[i].Choice == ADWebService.ConnectionType.ToString()) defaultChoice = 1 + i; } int choice = ConsoleMenu.SelectMenu(choices, defaultChoice); if (choice == 0) return DisplayState.Exit; - string whattodo = choices[choice - 1].Key; + string whattodo = choices[choice - 1].Choice; ADWebService.ConnectionType = (ADConnectionType)Enum.Parse(typeof(ADConnectionType), whattodo); return DisplayState.Exit; } @@ -1089,6 +1141,8 @@ private static void DisplayHelp() Console.WriteLine(" --webuser : optional user and password"); Console.WriteLine(" --webpassword "); Console.WriteLine(""); + Console.WriteLine("--rules : Generate an html containing all the rules used by PingCastle. Do not forget PingCastleReporting includes a similar option but for .xslx"); + Console.WriteLine(""); Console.WriteLine(" --generate-key : generate and display a new RSA key for encryption"); Console.WriteLine(""); Console.WriteLine(" --hc-conso : consolidate multiple healthcheck xml reports (step2)"); diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 40adc97..58721ca 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // Numéro de build // Révision // -[assembly: AssemblyVersion("2.6.0.0")] -[assembly: AssemblyFileVersion("2.6.0.0")] +[assembly: AssemblyVersion("2.7.0.0")] +[assembly: AssemblyFileVersion("2.7.0.0")] diff --git a/README.md b/README.md index 54b0831..cc687b5 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ If you need changes, please contact contact@pingcastle.com for support packages. PingCastle source code is licensed under a proprietary license and the Non-Profit Open Software License ("Non-Profit OSL") 3.0. Except if a license is purchased, you are not allowed to make any profit from this source code. +To be more specific: +* It is allowed to run PingCastle without purchasing any license on for profit companies if the company itself (or its ITSM provider) run it. +* To build services based on PingCastle AND earning money from that, you MUST purchase a license. Ping Castle uses the following Open source components: diff --git a/Report/ReportBase.cs b/Report/ReportBase.cs index 2eb6593..f0f842a 100644 --- a/Report/ReportBase.cs +++ b/Report/ReportBase.cs @@ -41,12 +41,15 @@ public string GenerateReportFile(string filename) Hook(reportSB); sb.Length = 0; - GenerateTitleInformation(); - reportSB = reportSB.Replace("<%=Title%>", sb.ToString()); - + sb.Length = 0; + GenerateCspMeta(); + Add(""); + GenerateTitleInformation(); + AddLine(""); GenerateBaseHeaderInformation(); GenerateHeaderInformation(); + Add(favicon); reportSB = reportSB.Replace("<%=Header%>", sb.ToString()); sb.Length = 0; @@ -59,24 +62,54 @@ public string GenerateReportFile(string filename) reportSB = reportSB.Replace("<%=Footer%>", sb.ToString()); var html = reportSB.ToString(); - File.WriteAllText(filename, html); + if (!String.IsNullOrEmpty(filename)) + { + File.WriteAllText(filename, html); + } return html; } + string CSPScriptNonce; + string CSPStyleNonce; + private void GenerateCspMeta() + { + CSPScriptNonce = GenerateNonce(); + CSPStyleNonce = GenerateNonce(); + Add(@""); + } + + protected void AddBeginStyle() + { + Add(@""); + AddBeginStyle(); + AddLine(TemplateManager.LoadBootstrapCss()); + AddLine(@""); } private void GenerateBaseFooterInformation() { - Add(@""); + AddBeginScript(); + AddLine(TemplateManager.LoadJqueryJs()); + AddLine(TemplateManager.LoadPopperJs()); + AddLine(TemplateManager.LoadBootstrapJs()); + AddLine(@""); } protected virtual void Hook(StringBuilder sbHtml) @@ -84,10 +117,25 @@ protected virtual void Hook(StringBuilder sbHtml) } - protected void Add(int value) - { - sb.Append(value); - } + protected void AddLine(string text) + { + sb.AppendLine(text); + } + + protected void AddLine() + { + sb.AppendLine(); + } + + protected void Add(int value) + { + sb.Append(value); + } + + protected void Add(ulong value) + { + sb.Append(value); + } protected void Add(bool value) { @@ -104,6 +152,11 @@ protected void AddEncoded(string text) sb.Append(ReportHelper.Encode(text)); } + protected void AddJsonEncoded(string text) + { + sb.Append(ReportHelper.EscapeJsonString(text)); + } + protected void Add(DateTime date) { sb.Append(date.ToString("u")); @@ -122,17 +175,32 @@ protected void Add(DateTime date) public static string GetStyleSheetTheme() { return @" - - - + "; } + protected static string favicon = @""; + protected void GenerateNavigation(string title, string domain, DateTime generationDate) { Add(@" @@ -359,6 +471,7 @@ protected void GenerateSection(string title, GenerateContentDelegate generateCon +
"); } @@ -433,6 +546,10 @@ protected void GenerateAccordionDetail(string id, string dataParent, string titl Add((int)itemCount); Add(@"]"); } + else if ((int)itemCount == 0) + { + Add(@"Informative rule"); + } else { Add(@"+ "); @@ -472,7 +589,11 @@ protected void GenerateTabHeader(string title, string selectedTab, bool defaultI Add(@""" class=""nav-link "); if (isActive) Add(@"active"); - Add(@""" role=""tab"" data-toggle=""tab"">"); + Add(@""" role=""tab"" data-toggle=""tab"""); + Add(@" id=""bs-"); + Add(id); + Add(@""""); + Add(@">"); Add(title); Add(""); } @@ -631,28 +752,96 @@ protected string GetLinkForLsaSetting(string property) switch (property.ToLowerInvariant()) { case "enableguestaccount": - return @"EnableGuestAccount"; + return @"Guest account (Technical details)"; case "lsaanonymousnamelookup": - return @"LSAAnonymousNameLookup"; + return @"Allow anonymous SID/Name translation (Technical details)"; case "everyoneincludesanonymous": - return @"EveryoneIncludesAnonymous"; + return @"Let Everyone permissions apply to anonymous users (Technical details)"; case "limitblankpassworduse": - return @"LimitBlankPasswordUse"; + return @"Limit local account use of blank passwords to console logon only (Technical details)"; case "forceguest": - return @"ForceGuest"; + return @"Sharing and security model for local accounts (Technical details)"; case "lmcompatibilitylevel": - return @"LmCompatibilityLevel"; - case "NoLMHash": - return @"NoLMHash"; + return @"LAN Manager authentication level (Technical details)"; + case "nolmhash": + return @"Do not store LAN Manager hash value on next password change (Technical details)"; case "restrictanonymous": - return @"RestrictAnonymous"; + return @"Do not allow anonymous enumeration of SAM accounts and shares (Technical details)"; case "restrictanonymoussam": - return @"RestrictAnonymousSam"; - + return @"Do not allow anonymous enumeration of SAM accounts (Technical details)"; + case "ldapclientintegrity": + return @"LDAP client signing requirements (Technical details)"; + case "recoveryconsole_securitylevel": + return @"Recovery console: Allow automatic administrative logon"; + case "refusepasswordchange": + return @"Refuse machine account password changes (Technical details)"; + case "enablemulticast": + return @"Turn off multicast name resolution (Technical details)"; + case "enablesecuritysignature": + return @"Microsoft network server: Digitally sign communications (if client agrees) (Technical details)"; } return property; } + protected string GetLsaSettingsValue(string property, int value) + { + switch (property.ToLowerInvariant()) + { + case "enablemulticast": + if (value == 0) + { + return @"LLMNR disabled"; + } + else + { + return @"LLMNR Enabled"; + } + case "lmcompatibilitylevel": + if (value == 0) + { + return @"Send LM & NTLM responses"; + } + else if (value == 1) + { + return @"Send LM & NTLM"; + } + else if (value == 2) + { + return @"Send NTLM response only"; + } + else if (value == 3) + { + return "Send NTLMv2 response only"; + } + else if (value == 4) + { + return "Send NTLMv2 response only. Refuse LM Client devices"; + } + else if (value == 5) + { + return "Send NTLMv2 response only. Refuse LM & NTLM"; + } + break; + case "ldapclientintegrity": + if (value == 0) + { + return @"None (Do not request signature)"; + } + else + { + return value.ToString(); + } + } + if (value == 0) + { + return @"Disabled"; + } + else + { + return @"Enabled"; + } + } + protected void GenerateGauge(int percentage) { Add(@"\r\n"); } + protected string GenerateNonce() + { + Random rand = new Random(); + byte[] bytes = new byte[18]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } } } diff --git a/Report/ReportCompromiseGraph.cs b/Report/ReportCompromiseGraph.cs index 5932344..f8a6aa1 100644 --- a/Report/ReportCompromiseGraph.cs +++ b/Report/ReportCompromiseGraph.cs @@ -40,13 +40,6 @@ public string GenerateRawContent(CompromiseGraphData report) return sb.ToString(); } - protected override void Hook(StringBuilder sbHtml) - { - // full screen graphs - sbHtml.Replace("", ""); - sbHtml.Replace("", ""); - } - protected override void GenerateTitleInformation() { sb.Append("PingCastle Compromission Graphs - "); @@ -55,22 +48,18 @@ protected override void GenerateTitleInformation() protected override void GenerateHeaderInformation() { - Add(@""); - Add(ReportBase.GetStyleSheetTheme()); - - Add(GetRiskControlStyleSheet()); - Add(GetStyleSheet()); - Add(@""); + AddBeginStyle(); + AddLine(TemplateManager.LoadDatatableCss()); + AddLine(TemplateManager.LoadVisCss()); + AddLine(ReportBase.GetStyleSheetTheme()); + AddLine(GetRiskControlStyleSheet()); + AddLine(GetStyleSheet()); + AddLine(@""); } private string GetStyleSheet() { - return @""; + +.legend +{ +position: absolute; +top: 55px; +left: 0px; +} +.network-area +{ +height: 100%; +min-height: 100%; +border-width:1px; +} +"; } protected override void GenerateBodyInformation() @@ -315,7 +317,7 @@ private void GenerateSectionTrusts() Add(@"

The following table lists all the foreign domains whose compromission can impact this domain. The impact is listed by typology of objects.

"); Add(@"
- +
@@ -394,7 +396,7 @@ private void GenerateSectionAnomalies() Add(@"
-
FQDN
+
"); Add(@" @@ -622,7 +624,7 @@ private void GenerateModalIndirectMember(int i)

Indirect Members

-
+
@@ -668,7 +670,7 @@ private void GenerateModalDeletedObjects(int i)

Deleted objects

-
Name Security Identifier
+
@@ -736,7 +738,7 @@ private void GenerateUserModalMember(int i)

Direct User Members

-
Security Identifier
+
@@ -786,7 +788,7 @@ private void GenerateModalComputerMember(int i)

Direct Computer Members

-
SamAccountName Enabled
+
@@ -834,15 +836,15 @@ private void GenerateModalGraph(int i)
-
+
0%
+ Add(@""" class=""network-area"">
-
+
Legend:
u user
w external user or group
@@ -964,7 +966,7 @@ private void DisplayGroupHeader() { Add(@"
-
SamAccountName Enabled
+
@@ -1107,12 +1109,10 @@ private void GenerateSummary(int index, SingleCompromiseGraphData data) protected override void GenerateFooterInformation() { - Add(@" -"); + AddBeginScript(); + AddLine(TemplateManager.LoadJqueryDatatableJs()); + AddLine(TemplateManager.LoadDatatableJs()); Add(@" -\r\n"); + "); } protected override void GenerateTitleInformation() @@ -56,18 +56,16 @@ protected override void GenerateTitleInformation() protected override void GenerateHeaderInformation() { - Add(@""); - Add(GetStyleSheetTheme()); - Add(GetStyleSheet()); + AddBeginStyle(); + AddLine(TemplateManager.LoadDatatableCss()); + AddLine(GetStyleSheetTheme()); + AddLine(GetStyleSheet()); + AddLine(@""); } public static string GetStyleSheet() { return @" - - - "; } @@ -285,7 +281,7 @@ private void GenerateRulesMatched() Add(@"
-
Group or user account ?
+
@@ -389,7 +385,7 @@ private void GenerateIndicatorsTable()
-
Domain Category
+
@@ -430,7 +426,7 @@ private void GenerateDomainInformation() Add(@"
-
Domain Domain Risk Level
+
@@ -499,7 +495,7 @@ private void GenerateUserInformation() Add(@"
-
Domain Netbios Name
+
@@ -574,7 +570,7 @@ private void GenerateComputerInformation() Add(@"
-
Domain Nb User Accounts
+
@@ -663,7 +659,7 @@ private string GenerateConsolidatedOperatingSystemList() Add(@"
-
Domain Nb Computer Accounts
+
"); @@ -738,7 +734,7 @@ private string GenerateConsolidatedOperatingSystemList() Add(@"
-
Domain
+
@@ -769,7 +765,7 @@ private void GenerateAdminGroupsInformation() Add(@"
-
Operating System Nb
+
@@ -840,7 +836,7 @@ private void GenerateTrustInformation() Add(@"
-
Domain Group Name
+
@@ -848,6 +844,7 @@ private void GenerateTrustInformation() + @@ -879,6 +876,7 @@ private void GenerateTrustInformation() + @@ -895,7 +893,7 @@ private void GenerateTrustInformation() Add(@"
-
Domain Trust PartnerAttribut Direction SID Filtering activeTGT Delegation Creation Is Active ?
" + TrustAnalyzer.GetTrustAttribute(trust.TrustAttributes) + @" " + TrustAnalyzer.GetTrustDirection(trust.TrustDirection) + @" " + TrustAnalyzer.GetSIDFiltering(trust) + @"" + TrustAnalyzer.GetTGTDelegation(trust) + @" " + trust.CreationDate.ToString("u") + @" " + trust.IsActive + @"
+
@@ -986,7 +984,7 @@ private void GenerateTrustInformation() Add(@"
-
From Reachable domain
+
@@ -1057,7 +1055,7 @@ private void GenerateAnomalyDetail() Add(@"
-
Domain Domain SID
+
@@ -1098,7 +1096,7 @@ private void GeneratePasswordPoliciesDetail() Add(@"
-
Domain Krbtgt
+
@@ -1171,7 +1169,7 @@ private void GeneratePasswordPoliciesDetail() Add(@"
-
Domain Policy Name
+
@@ -1225,11 +1223,11 @@ private void GeneratePasswordPoliciesDetail() "); - GenerateSubSection("LSA settings"); + GenerateSubSection("Security settings"); Add(@"
-
Domain Policy Name
+
@@ -1256,8 +1254,8 @@ private void GeneratePasswordPoliciesDetail() - "); @@ -1281,7 +1279,7 @@ private void GenerateGPODetail() Add(@"
-
Domain Policy Name"); Add(GetLinkForLsaSetting(property.Property)); Add(@""); - Add(property.Value); + "); + Add(GetLsaSettingsValue(property.Property, property.Value)); Add(@"
+
diff --git a/Report/ReportHealthCheckRules.cs b/Report/ReportHealthCheckRules.cs new file mode 100644 index 0000000..878fc28 --- /dev/null +++ b/Report/ReportHealthCheckRules.cs @@ -0,0 +1,375 @@ +using PingCastle.Healthcheck; +using PingCastle.Rules; +using PingCastle.template; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Resources; +using System.Text; + +namespace PingCastle.Report +{ + public class ReportHealthCheckRules : ReportBase + { + + public string GenerateRawContent() + { + sb.Length = 0; + GenerateContent(); + return sb.ToString(); + } + + protected override void GenerateFooterInformation() + { + AddBeginScript(); + AddLine(TemplateManager.LoadJqueryDatatableJs()); + AddLine(TemplateManager.LoadDatatableJs()); + Add(@" + +$(function() { + $(window).scroll(function() { + if($(window).scrollTop() >= 70) { + $('.information-bar').removeClass('hidden'); + $('.information-bar').fadeIn('fast'); + }else{ + $('.information-bar').fadeOut('fast'); + } + }); + }); +$(document).ready(function(){ + $('table').not('.model_table').DataTable( + { + 'paging': false, + 'searching': false + } + ); + $('[data-toggle=""tooltip""]').tooltip({html: true, container: 'body'}); + $('[data-toggle=""popover""]').popover(); + + $('.div_model').on('click', function (e) { + $('.div_model').not(this).popover('hide'); + }); + + +}); + +"); + } + + protected override void GenerateTitleInformation() + { + Add("PingCastle Health Check rules - "); + Add(DateTime.Now.ToString("yyyy-MM-dd")); + } + + protected override void GenerateHeaderInformation() + { + AddBeginStyle(); + AddLine(GetStyleSheetTheme()); + AddLine(GetStyleSheet()); + Add(@" +.model_table { + +} +.model_table th { + padding: 5px; +} +.model_cell { + border: 2px solid black; + padding: 5px; +} +.model_empty_cell { +} +div_model { + +} +.model_cell.model_good { + //background-color: #83e043; + //color: #FFFFFF; +} +.model_cell.model_toimprove +{ + background-color: #ffd800; + //color: #FFFFFF; +} +.model_cell.model_info { + background-color: #00AAFF; +color: #FFFFFF; +} +.model_cell.model_warning { + background-color: #ff6a00; +color: #FFFFFF; +} +.model_cell.model_danger { + background-color: #f12828; +color: #FFFFFF; +} +.model_cell .popover{ + max-width: 100%; +} +.model_cell .popover-content { + color: #000000; +} +.model_cell .popover-title { + color: #000000; +} +"); + AddLine(@""); + } + + private string GetStyleSheet() + { + return string.Empty; + } + + protected override void GenerateBodyInformation() + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + string versionString = version.ToString(4); +#if DEBUG + versionString += " Beta"; +#endif + GenerateNavigation("Healthcheck Rules", null, DateTime.Now); + GenerateAbout(@"

Generated by Ping Castle all rights reserved

+

Open source components:

+"); + Add(@" +
+ +

Rules evaluated during PingCastle Healthcheck

+

Date: " + DateTime.Now.ToString("yyyy-MM-dd") + @" - Engine version: " + versionString + @"

+
+
+Do not forget that PingCastleReporting can produce a list of all rules in an Excel format. +
+"); + GenerateContent(); + Add(@" +
+"); + } + + private void GenerateContent() + { + GenerateRiskModelPanel(); + GenerateSubSection("Stale Objects"); + GenerateRuleAccordeon(RiskRuleCategory.StaleObjects); + GenerateSubSection("Privileged Accounts"); + GenerateRuleAccordeon(RiskRuleCategory.PrivilegedAccounts); + GenerateSubSection("Trusts"); + GenerateRuleAccordeon(RiskRuleCategory.Trusts); + GenerateSubSection("Anomalies"); + GenerateRuleAccordeon(RiskRuleCategory.Anomalies); + } + + ResourceManager _resourceManager = new ResourceManager("PingCastle.Healthcheck.Rules.RuleDescription", typeof(RuleBase<>).Assembly); + + private void GenerateRuleAccordeon(RiskRuleCategory category) + { + var rules = RuleSet.Rules; + rules.Sort((RuleBase a, RuleBase b) + => + { + int c = ReportHelper.GetEnumDescription(a.Model).CompareTo(ReportHelper.GetEnumDescription(b.Model)); + if (c == 0) + c = a.Title.CompareTo(b.Title); + return c; + } + ); + var m = RiskModelCategory.Unknown; + var data = new List>>>(); + foreach (var rule in rules) + { + if (rule.Category == category) + { + if (rule.Model != m) + { + m = rule.Model; + data.Add(new KeyValuePair>>(rule.Model, new List>())); + } + data[data.Count - 1].Value.Add(rule); + } + } + Add(@" +
+

Each line represents a rule. Click on a rule to expand it and show the details of it. +

+"); + foreach (var d in data) + { + Add(@" +
+

"); + Add(ReportHelper.GetEnumDescription(d.Key)); + Add(@" +

+"); + string description = _resourceManager.GetString(d.Key.ToString() + "_Detail"); + if (!string.IsNullOrEmpty(description)) + { + Add(@" +
+

"); + Add(description); + Add(@" +

+"); + } + GenerateAccordion("rules" + d.Key.ToString(), () => + { + foreach (var rule in d.Value) + { + GenerateIndicatorPanelDetail(d.Key, rule); + } + }); + } + } + + protected void GenerateRiskModelPanel() + { + Add(@" + +
+

This model regroup all rules per category. It summarize what checks are performed. Click on a cell to show all rules associated to a category. +

+
+
+
+
Domain GPO Name
+ + +"); + var riskmodel = new Dictionary>(); + foreach (RiskRuleCategory category in Enum.GetValues(typeof(RiskRuleCategory))) + { + riskmodel[category] = new List(); + } + for (int j = 0; j < 4; j++) + { + for (int i = 0; ; i++) + { + int id = (1000 * j + 1000 + i); + if (Enum.IsDefined(typeof(RiskModelCategory), id)) + { + riskmodel[(RiskRuleCategory)j].Add((RiskModelCategory)id); + } + else + break; + } + } + foreach (RiskRuleCategory category in Enum.GetValues(typeof(RiskRuleCategory))) + { + riskmodel[category].Sort( + (RiskModelCategory a, RiskModelCategory b) => + { + return string.Compare(ReportHelper.GetEnumDescription(a), ReportHelper.GetEnumDescription(b)); + }); + } + for (int i = 0; ; i++) + { + string line = ""; + bool HasValue = false; + foreach (RiskRuleCategory category in Enum.GetValues(typeof(RiskRuleCategory))) + { + if (i < riskmodel[category].Count) + { + HasValue = true; + RiskModelCategory model = riskmodel[category][i]; + int score = 0; + int numrules = 0; + var rulematched = new List>(); + foreach (var rule in RuleSet.Rules) + { + if (rule.Model == model) + { + numrules++; + score += rule.Points; + rulematched.Add(rule); + } + } + string tdclass = ""; + tdclass = "model_good"; + string modelstring = ReportHelper.GetEnumDescription(model); + string tooltip = modelstring + " [Rules: " + numrules + "]"; + string tooltipdetail = null; + rulematched.Sort((RuleBase a, RuleBase b) + => + { + return a.Points.CompareTo(b.Points); + }); + foreach (var rule in rulematched) + { + tooltipdetail += "
  • " + ReportHelper.Encode(rule.Title) + "

  • "; + } + line += "
    "; + } + else + line += ""; + } + line += ""; + if (HasValue) + Add(line); + else + break; + } + Add(@" + +
    Stale ObjectsPrivileged accountsTrustsAnomalies
    " + _resourceManager.GetString(model.ToString() + "_Detail") + "

    " + (String.IsNullOrEmpty(tooltipdetail) ? "No rule matched" : "

      " + tooltipdetail + "

    ") + "\">" + modelstring + "
    +
    +
    "); + } + + private void GenerateIndicatorPanelDetail(RiskModelCategory category, RuleBase hcrule) + { + string safeRuleId = hcrule.RiskId.Replace("$", "dollar"); + GenerateAccordionDetail("rules" + safeRuleId, "rules" + category.ToString(), hcrule.Title, null, true, + () => + { + Add("

    "); + Add(hcrule.Title); + Add("

    \r\nRule ID:

    "); + Add(hcrule.RiskId); + Add("

    \r\nDescription:

    "); + Add(NewLineToBR(hcrule.Description)); + Add("

    \r\nTechnical explanation:

    "); + Add(NewLineToBR(hcrule.TechnicalExplanation)); + Add("

    \r\nAdvised solution:

    "); + Add(NewLineToBR(hcrule.Solution)); + Add(@"

    "); + object[] models = hcrule.GetType().GetCustomAttributes(typeof(RuleIntroducedInAttribute), true); + if (models != null && models.Length != 0) + { + var model = (PingCastle.Rules.RuleIntroducedInAttribute)models[0]; + Add("Introduced in:"); + Add("

    "); + Add(model.Version.ToString()); + Add(@"

    "); + } + Add("Points:

    "); + Add(NewLineToBR(hcrule.GetComputationModelString())); + Add("

    \r\n"); + if (!String.IsNullOrEmpty(hcrule.Documentation)) + { + Add("Documentation:

    "); + Add(hcrule.Documentation); + Add("

    "); + } + }); + } + } +} diff --git a/Report/ReportHealthCheckSingle.cs b/Report/ReportHealthCheckSingle.cs index d16d1ad..fc1d691 100644 --- a/Report/ReportHealthCheckSingle.cs +++ b/Report/ReportHealthCheckSingle.cs @@ -22,10 +22,10 @@ namespace PingCastle.Report public class ReportHealthCheckSingle : ReportRiskControls, IPingCastleReportUser { - private HealthcheckData Report; + protected HealthcheckData Report; public static int MaxNumberUsersInHtmlReport = 100; private ADHealthCheckingLicense _license; - private Version version; + protected Version version; public string GenerateReportFile(HealthcheckData report, ADHealthCheckingLicense license, string filename) { @@ -55,11 +55,11 @@ protected override void GenerateTitleInformation() protected override void GenerateHeaderInformation() { - Add(@""); - Add(GetStyleSheetTheme()); - Add(GetRiskControlStyleSheet()); + AddBeginStyle(); + AddLine(TemplateManager.LoadDatatableCss()); + AddLine(GetStyleSheetTheme()); + AddLine(GetRiskControlStyleSheet()); + AddLine(@""); } protected override void GenerateBodyInformation() @@ -114,29 +114,32 @@ protected override void GenerateBodyInformation() void GenerateContent() { GenerateSection("Active Directory Indicators", () => - { - GenerateIndicators(Report, Report.AllRiskRules); - GenerateRiskModelPanel(Report.RiskRules); - }); + { + GenerateIndicators(Report, Report.AllRiskRules); + GenerateRiskModelPanel(Report.RiskRules); + }); + + List> applicableRules = GenerateListOfApplicableRules(); + GenerateSection("Stale Objects", () => { GenerateSubIndicator("Stale Objects", Report.GlobalScore, Report.StaleObjectsScore, "It is about operations related to user or computer objects"); - GenerateIndicatorPanel("DetailStale", "Stale Objects rule details", RiskRuleCategory.StaleObjects, Report.RiskRules); + GenerateIndicatorPanel("DetailStale", "Stale Objects rule details", RiskRuleCategory.StaleObjects, Report.RiskRules, applicableRules); }); GenerateSection("Privileged Accounts", () => { GenerateSubIndicator("Privileged Accounts", Report.GlobalScore, Report.PrivilegiedGroupScore, "It is about administrators of the Active Directory"); - GenerateIndicatorPanel("DetailPrivileged", "Privileged Accounts rule details", RiskRuleCategory.PrivilegedAccounts, Report.RiskRules); + GenerateIndicatorPanel("DetailPrivileged", "Privileged Accounts rule details", RiskRuleCategory.PrivilegedAccounts, Report.RiskRules, applicableRules); }); GenerateSection("Trusts", () => { GenerateSubIndicator("Trusts", Report.GlobalScore, Report.TrustScore, "It is about operations related to user or computer objects"); - GenerateIndicatorPanel("DetailTrusts", "Trusts rule details", RiskRuleCategory.Trusts, Report.RiskRules); + GenerateIndicatorPanel("DetailTrusts", "Trusts rule details", RiskRuleCategory.Trusts, Report.RiskRules, applicableRules); }); GenerateSection("Anomalies analysis", () => { GenerateSubIndicator("Anomalies", Report.GlobalScore, Report.AnomalyScore, "It is about specific security control points"); - GenerateIndicatorPanel("DetailAnomalies", "Anomalies rule details", RiskRuleCategory.Anomalies, Report.RiskRules); + GenerateIndicatorPanel("DetailAnomalies", "Anomalies rule details", RiskRuleCategory.Anomalies, Report.RiskRules, applicableRules); }); GenerateSection("Domain Information", GenerateDomainInformation); GenerateSection("User Information", GenerateUserInformation); @@ -148,11 +151,34 @@ void GenerateContent() GenerateSection("GPO", GenerateGPODetail); } + protected List> GenerateListOfApplicableRules() + { + var applicableRules = new List>(); + foreach (var rule in RuleSet.Rules) + { + object[] models = rule.GetType().GetCustomAttributes(typeof(RuleIntroducedInAttribute), true); + if (models != null && models.Length != 0) + { + RuleIntroducedInAttribute model = (RuleIntroducedInAttribute)models[0]; + if (model.Version <= version) + { + applicableRules.Add(rule); + } + } + else + { + applicableRules.Add(rule); + } + } + + return applicableRules; + } + protected override void GenerateFooterInformation() { - Add(""); - Add(ReportBase.GetStyleSheetTheme()); - Add(@""); + AddLine(TemplateManager.LoadVisCss()); + AddLine(@""); } protected override void GenerateBodyInformation() @@ -164,7 +156,7 @@ protected override void GenerateBodyInformation()
    -
    +
    0%
    @@ -173,15 +165,15 @@ protected override void GenerateBodyInformation()
    -
    +
    -
    +
    Legend:
      score=100
    -   score < 100
    -   score < 70
    -   score < 50
    -   score < 30
    +   score < 100
    +   score < 70
    +   score < 50
    +   score < 30
      score unknown
    "); @@ -189,8 +181,9 @@ protected override void GenerateBodyInformation() protected override void GenerateFooterInformation() { + AddBeginScript(); + AddLine(TemplateManager.LoadVisJs()); Add(@" -"); + } + + protected override void GenerateTitleInformation() + { + Add("PingCastle Network map - "); + Add(DateTime.Now.ToString("yyyy-MM-dd")); + } + + protected override void GenerateHeaderInformation() + { + AddBeginStyle(); + AddLine(TemplateManager.LoadDatatableCss()); + AddLine(GetStyleSheetTheme()); + AddLine(GetStyleSheet()); + AddLine(@""); + } + + private string GetStyleSheet() + { + return @" +.map_view_tooltip { + position: absolute !important; +} +.map_view_tooltip { + pointer-events: none; +} +"; + } + + protected override void GenerateBodyInformation() + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + string versionString = version.ToString(4); +#if DEBUG + versionString += " Beta"; +#endif + GenerateNavigation("Network map", null, DateTime.Now); + GenerateAbout(@"

    Generated by Ping Castle all rights reserved

    +

    Open source components:

    +"); + Add(@" +
    + +

    Network map

    +

    Date: " + DateTime.Now.ToString("yyyy-MM-dd") + @" - Engine version: " + versionString + @"

    +
    +"); + GenerateContent(); + Add(@" +
    +"); + } + + + protected class NetworkMapData + { + public List Views { get; set; } + public Dictionary> networkrange { get; set; } + public List DomainControllers { get; set; } + + public static NetworkMapData BuildFromConsolidation(PingCastleReportCollection reports) + { + var data = new NetworkMapData() + { + Views = new List() { + new NetworkMapDataView(){ + framenetwork = Subnet.Parse("10.0.0.0/8"), + order = 1024, + }, + new NetworkMapDataView() + { + framenetwork = Subnet.Parse("192.168.0.0/16"), + order = 256, + } + }, + }; + data.networkrange = new Dictionary>(); + data.DomainControllers = new List(); + var latestForestReports = new Dictionary(); + + foreach (var report in reports) + { + // select latest forest report to have the latest network information + var version = new Version(report.EngineVersion.Split(' ')[0]); + if (!(version.Major < 2 || (version.Major == 2 && version.Minor < 6))) + { + if (!latestForestReports.ContainsKey(report.Forest.DomainSID) || latestForestReports[report.Forest.DomainSID].GenerationDate < report.GenerationDate) + { + latestForestReports[report.Forest.DomainSID] = report; + } + } + } + + // store network information + foreach (var report in latestForestReports.Values) + { + var list = new List(); + data.networkrange.Add(report.Forest.DomainSID, list); + foreach (var site in report.Sites) + { + foreach (var network in site.Networks) + { + list.Add(new NetworkMapDataItem() + { + Network = Subnet.Parse(network), + Source = report.Forest.DomainName, + Description = site.Description, + Location = site.Location, + Name = site.SiteName, + }); + } + } + } + // tag the network + foreach (var report in reports) + { + IEnumerable networks = null; + if (data.networkrange.ContainsKey(report.Forest.DomainSID)) + { + networks = data.networkrange[report.Forest.DomainSID]; + } + // collect DC info + foreach (var dc in report.DomainControllers) + { + foreach (string ip in dc.IP) + { + IPAddress i; + if (!IPAddress.TryParse(ip, out i)) + { + continue; + } + if (i.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + continue; + data.DomainControllers.Add(new NetworkMapDCItem() + { + Name = dc.DCName, + Source = report.DomainFQDN, + Ip = i, + }); + if (networks != null) + { + foreach (var network in networks) + { + if (network.Network.MatchIp(i)) + { + if (string.IsNullOrEmpty(network.DomainFQDN)) + { + network.DomainFQDN = report.DomainFQDN; + } + else if (network.DomainFQDN == report.DomainFQDN) + { + + } + else + { + network.DomainFQDN = "_multiple_"; + } + } + } + } + } + + } + } + return data; + } + + } + + protected class NetworkMapDataView + { + public int order { get; set; } + public Subnet framenetwork { get; set; } + public bool HasData { get; set; } + public int RecordCount { get; set; } + public int ForestCount { get; set; } + } + + protected class NetworkMapDataItem + { + public Subnet Network { get; set; } + public string Source { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Location { get; set; } + public string DomainFQDN { get; set; } + } + + protected class NetworkMapDCItem + { + public string Source { get; set; } + public string Name { get; set; } + public IPAddress Ip { get; set; } + + } + + private void GenerateContent(string selectedTab = null) + { + Add(@" +
    +
    +
      "); + GenerateTabHeader("Overview", selectedTab, true); + GenerateTabHeader("Viewer", selectedTab); + GenerateTabHeader("Network list", selectedTab); + GenerateTabHeader("DC list", selectedTab); + Add(@" +
    +
    +
    +
    +
    +
    "); + GenerateSectionFluid("Overview", GenerateOverview, selectedTab, true); + GenerateSectionFluid("Viewer", GenerateMap, selectedTab); + GenerateSectionFluid("Network list", GenerateNetworkList, selectedTab); + GenerateSectionFluid("DC list", GenerateDCList, selectedTab); + Add(@" +
    +
    +
    "); + } + + private void GenerateOverview() + { + int id = 0; + Add(@" +
    +
    +

    Networks are big and it can be difficult to have a visual representation of them. This report displays what is called a Hilbert map. Indeed, fractal functions are used to compress a 1D space (IP addresses of the networks), into 2D for a visual representation. +Each square represent a network. It can be used to detect non occupied space or networks which are overlapping.

    +

    Put your mouse over the map to display its legend.

    +
    "); + foreach (var view in data.Views) + { + var ms = new MemoryStream(); + if (GenerateHilbertImage(ms, view)) + { + ms.Position = 0; + + Add(@" +
    + "); + AddLine(@""); + Add(@" +
    +
    " + view.framenetwork + @"
    +

    The network "); + Add(view.framenetwork.ToString()); + Add(" does match "); + Add(view.RecordCount); + Add(" networks. This information is coming from "); + Add(view.ForestCount); + Add(@" Active Directory forest(s).

    + View +
    +
    "); + } + } + AddLine(@"
    "); + } + + private void GenerateMap() + { + AddLine(@" + +
    +
    + +
    +
    +

    Legend

    + +
    +
    +
    +

    Domain Controller

    +
    +
    +

    Network space without network discovered

    +
    +
    +

    Network discovered

    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +

    Filter source

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Viewing network

    +

    Scale: 1 pixel is ip(s)

    +
    +
    + + +
    +
    +
    +
    +
    + + +
    + Example: 10.0.1.0 +
    +
    +
    + + + +
    "); + GenerateJson(); + } + + private void GenerateNetworkList() + { + Add(@" +
    +
    + + + + + + + + + + +
    sourcenamenetworkdescriptionUse by
    +
    +
    "); + } + + private void GenerateDCList() + { + Add(@" +
    +
    + + + + + + + + +
    sourcenameip
    +
    +
    "); + } + + private bool GenerateHilbertImage(Stream stream, NetworkMapDataView view) + { + const int order = 256; + var uniqueForestSID = new List(); + var subnets = new List(); + foreach (var key in data.networkrange.Keys) + foreach (var subnet in data.networkrange[key]) + { + // keep only ipv4 + if (subnet.Network.StartAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + continue; + // keep only networks that are visible + if (!view.framenetwork.MatchIp(subnet.Network.StartAddress) || !view.framenetwork.MatchIp(subnet.Network.EndAddress)) + continue; + // avoiding filling all the space with larger networks + if (subnet.Network.MatchIp(view.framenetwork.StartAddress) && subnet.Network.MatchIp(view.framenetwork.EndAddress)) + continue; + subnets.Add(subnet.Network); + if (!uniqueForestSID.Contains(key)) + uniqueForestSID.Add(key); + } + if (subnets.Count == 0) + return false; + view.RecordCount = subnets.Count; + view.ForestCount = uniqueForestSID.Count; + using (Bitmap bitmap = new Bitmap(order, order, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) + using (Graphics g = Graphics.FromImage(bitmap)) + using (SolidBrush drawBrush = new SolidBrush(Color.Black)) + using (SolidBrush dcBrush = new SolidBrush(Color.Red)) + using (StringFormat drawFormat1 = new StringFormat()) + { + g.Clear(Color.GhostWhite); + foreach (var s in subnets) + { + ulong a = convertToN(s.StartAddress, view.framenetwork, order); + ulong b = convertToN(s.EndAddress, view.framenetwork, order); + for (ulong i = a; i <= b; i++) + { + int x = 0; int y = 0; + d2xy(order, (int)i, ref x, ref y); + g.FillRectangle(drawBrush, x, y, 1, 1); + } + } + foreach (var dc in data.DomainControllers) + { + if (!view.framenetwork.MatchIp(dc.Ip)) + continue; + ulong a = convertToN(dc.Ip, view.framenetwork, order); + int x = 0; int y = 0; + d2xy(order, (int)a, ref x, ref y); + g.FillRectangle(dcBrush, x, y, 2, 2); + } + bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png); + } + return true; + } + + void GenerateJson() + { + AddLine(@""); + AddLine(@""); + AddLine(@""); + } + + ulong convertToN(IPAddress point, Subnet range, int n) + { + var v1 = AdressToLong(range.StartAddress); + var v = AdressToLong(range.EndAddress) - v1; + return ((ulong)n * (ulong)n * (AdressToLong(point) - v1) / v); + } + + ulong AdressToLong(IPAddress a) + { + var b = a.GetAddressBytes(); + return ((ulong)b[0] << 24) + ((ulong)b[1] << 16) + ((ulong)b[2] << 8) + (ulong)b[3]; + } + + //convert (x,y) to d + int xy2d(int n, int x, int y) + { + int rx, ry, s, d = 0; + for (s = n / 2; s > 0; s /= 2) + { + + rx = Convert.ToInt32(((x & s) > 0)); + ry = Convert.ToInt32((y & s) > 0); + d += s * s * ((3 * rx) ^ ry); + rot(s, ref x, ref y, rx, ry); + } + return d; + } + + //convert d to (x,y) + void d2xy(int n, int d, ref int x, ref int y) + { + int rx, ry, s, t = d; + x = y = 0; + for (s = 1; s < n; s *= 2) + { + rx = 1 & (t / 2); + ry = 1 & (t ^ rx); + rot(s, ref x, ref y, rx, ry); + x += s * rx; + y += s * ry; + t /= 4; + } + } + + //rotate/flip a quadrant appropriately + void rot(int n, ref int x, ref int y, int rx, int ry) + { + if (ry == 0) + { + if (rx == 1) + { + x = n - 1 - x; + y = n - 1 - y; + } + + //Swap x and y + int t = x; + x = y; + y = t; + } + } + } +} diff --git a/Report/ReportRiskControls.cs b/Report/ReportRiskControls.cs index 68f3409..ffd31e5 100644 --- a/Report/ReportRiskControls.cs +++ b/Report/ReportRiskControls.cs @@ -23,19 +23,6 @@ int GetRulesNumberForCategory(List rules, RiskRuleCategory public static string GetRiskControlStyleSheet() { return @" - +.indicators-border +{ +border: 2px solid #Fa9C1A; +margin:2px; +padding: 2px; +} "; } @@ -136,7 +128,7 @@ protected void GenerateIndicators(IRiskEvaluation data, IList rules)

    It is the maximum score of the 4 indicators and one score cannot be higher than 100. The lower the better

    -
    +
    "); GenerateSubIndicator("Stale Object", data.GlobalScore, data.StaleObjectsScore, rules, RiskRuleCategory.StaleObjects, "It is about operations related to user or computer objects"); GenerateSubIndicator("Trusts", data.GlobalScore, data.TrustScore, rules, RiskRuleCategory.Trusts, "It is about links between two Active Directories"); @@ -199,12 +191,12 @@ protected void GenerateSubIndicator(string category, int globalScore, int score, protected void GenerateRiskModelPanel(List rules, int numberOfDomain = 1) { Add(@" -
    + -
    +
    @@ -279,7 +271,7 @@ protected void GenerateRiskModelPanel(List rules, int numbe { tdclass = "model_danger"; } - string tooltip = "Rules: " + numrules + " Score: " + score / numberOfDomain; + string tooltip = "Rules: " + numrules + " Score: " + (numberOfDomain == 0? 100 : score / numberOfDomain); string tooltipdetail = null; string modelstring = ReportHelper.GetEnumDescription(model); rulematched.Sort((HealthcheckRiskRule a, HealthcheckRiskRule b) @@ -290,6 +282,11 @@ protected void GenerateRiskModelPanel(List rules, int numbe foreach (var rule in rulematched) { tooltipdetail += ReportHelper.Encode(rule.Rationale) + "
    "; + var hcrule = RuleSet.GetRuleFromID(rule.RiskId); + if (hcrule != null && !string.IsNullOrEmpty(hcrule.ReportLocation)) + { + tooltipdetail += "" + ReportHelper.Encode(hcrule.ReportLocation) + "
    "; + } } line += "
    Stale ObjectsPrivileged accountsTrustsAnomalies
    rules, int numbe
    "); } - protected void GenerateIndicatorPanel(string id, string title, RiskRuleCategory category, List rules) + protected void GenerateIndicatorPanel(string id, string title, RiskRuleCategory category, List rules, List> applicableRules) { Add(@"
    @@ -327,7 +324,9 @@ protected void GenerateIndicatorPanel(string id, string title, RiskRuleCategory Add(title); Add(@" ["); Add(GetRulesNumberForCategory(rules, category).ToString()); - Add(@" rules matched] + Add(@" rules matched on a total of "); + Add(GetApplicableRulesNumberForCategory(applicableRules, category).ToString()); + Add(@"]
    "); } + private int GetApplicableRulesNumberForCategory(List> applicableRules, RiskRuleCategory category) + { + int count = 0; + foreach (var rule in applicableRules) + { + if (rule.Category == category) + count++; + } + return count; + } + protected void GenerateSubIndicator(string category, int globalScore, int score, string explanation) { diff --git a/Rules/RiskModelCategory.cs b/Rules/RiskModelCategory.cs index 4e2438e..a11e741 100644 --- a/Rules/RiskModelCategory.cs +++ b/Rules/RiskModelCategory.cs @@ -25,14 +25,12 @@ public enum RiskModelCategory Provisioning = 1003, [Description("Old authentication protocols")] OldAuthenticationProtocols = 1004, - [Description("Unfinished migration")] - UnfinishedMigration = 1005, [Description("Obsolete OS")] - ObsoleteOS = 1006, + ObsoleteOS = 1005, [Description("Object configuration")] - ObjectConfig = 1007, + ObjectConfig = 1006, [Description("Network topography")] - NetworkTopography = 1008, + NetworkTopography = 1007, [Description("Admin control")] AdminControl = 2000, [Description("Privilege control")] @@ -41,6 +39,8 @@ public enum RiskModelCategory ACLCheck = 2002, [Description("Irreversible change")] IrreversibleChange = 2003, + [Description("Account take over")] + AccountTakeOver = 2004, [Description("SID Filtering")] SIDFiltering = 3000, [Description("Trust inactive")] diff --git a/Rules/RuleAttribute.cs b/Rules/RuleAttribute.cs index 6f4acfd..874e720 100644 --- a/Rules/RuleAttribute.cs +++ b/Rules/RuleAttribute.cs @@ -40,7 +40,37 @@ public RuleObjectiveAttribute(string Id, RiskRuleCategory Category, RiskModelObj public RiskModelObjective Objective { get; private set; } } - public enum RuleComputationType + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class RuleIntroducedInAttribute : Attribute + { + public RuleIntroducedInAttribute(int major, int minor, int build = 0, int revision = 0) + { + Major = major; + Minor = minor; + Build = build; + Revision = revision; + } + + public int Major { get; private set; } + public int Minor { get; private set; } + public int Build { get; private set; } + public int Revision { get; private set; } + + private Version _version; + public Version Version + { + get + { + if (_version == null) + { + _version = new Version(Major, Minor, Build, Revision); + } + return _version; + } + } + } + + public enum RuleComputationType { TriggerOnThreshold, TriggerOnPresence, @@ -143,25 +173,47 @@ public bool Equals(RuleFrameworkReference other) } } + public enum STIGFramework + { + Domain, + Forest, + Windows7, + Windows2008, + ActiveDirectoryService2003, + ActiveDirectoryService2008 + } + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RuleSTIGAttribute : RuleFrameworkReference { - public RuleSTIGAttribute(string id, string title = null, bool forest = false) + public RuleSTIGAttribute(string id, string title = null, STIGFramework framework = STIGFramework.Domain) { ID = id; - ForestCheck = forest; + Framework = framework; Title = title; } public string ID { get; private set; } - public bool ForestCheck { get; private set; } + public STIGFramework Framework { get; private set; } public string Title { get; private set; } public override string URL { get { - if (ForestCheck) - return "https://www.stigviewer.com/stig/active_directory_forest/2016-12-19/finding/" + ID; - return "https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/" + ID; + switch (Framework) + { + case STIGFramework.Forest: + return "https://www.stigviewer.com/stig/active_directory_forest/2016-12-19/finding/" + ID; + case STIGFramework.Windows7: + return "https://www.stigviewer.com/stig/windows_7/2012-08-22/finding/" + ID; + case STIGFramework.Windows2008: + return "https://www.stigviewer.com/stig/windows_2008_member_server/2018-03-07/finding/" + ID; + case STIGFramework.ActiveDirectoryService2003: + return "https://www.stigviewer.com/stig/active_directory_service_2003/2011-05-20/finding/" + ID; + case STIGFramework.ActiveDirectoryService2008: + return "https://www.stigviewer.com/stig/active_directory_service_2008/2011-05-23/finding/" + ID; + default: + return "https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/" + ID; + } } } diff --git a/Scanners/ACLScanner.cs b/Scanners/ACLScanner.cs index 206aa5e..67476a9 100644 --- a/Scanners/ACLScanner.cs +++ b/Scanners/ACLScanner.cs @@ -1,6 +1,7 @@ using PingCastle.ADWS; using PingCastle.Export; using PingCastle.Graph.Database; +using PingCastle.Graph.Export; using PingCastle.misc; using System; using System.Collections.Generic; @@ -34,6 +35,7 @@ public void Initialize(string server, int port, NetworkCredential credential) Credential = credential; } + Guid LAPSSchemaId = Guid.Empty; public void Export(string filename) { @@ -41,6 +43,14 @@ public void Export(string filename) using (ADWebService adws = new ADWebService(Server, Port, Credential)) { + string[] propertiesLaps = new string[] { "schemaIDGUID" }; + // note: the LDAP request does not contain ms-MCS-AdmPwd because in the old time, MS consultant was installing customized version of the attriute, * being replaced by the company name + // check the oid instead ? (which was the same even if the attribute name was not) + adws.Enumerate(adws.DomainInfo.SchemaNamingContext, "(name=ms-*-AdmPwd)", propertiesLaps, (ADItem aditem) => + { + LAPSSchemaId = aditem.SchemaIDGUID; + }, "OneLevel"); + using (StreamWriter sw = File.CreateText(filename)) { sw.WriteLine("DistinguishedName\tIdentity\tAccessRule"); @@ -108,7 +118,9 @@ private void BuildUserList(ADWebService adws, ADDomainInfo domainInfo) if (UserList.Count == 0) { UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier("S-1-1-0"), "Everyone")); + UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier("S-1-5-7"), "Anonymous")); UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier("S-1-5-11"), "Authenticated Users")); + UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier("S-1-5-32-545"), "Users")); UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier(domainInfo.DomainSid.Value + "-513"), "Domain Users")); UsersToMatch.Add(new KeyValuePair(new SecurityIdentifier(domainInfo.DomainSid.Value + "-515"), "Domain Computers")); return; @@ -143,7 +155,7 @@ public bool QueryForAdditionalParameterInInteractiveMode() ConsoleMenu.Title = "Enter users or groups to check"; ConsoleMenu.Information = @"This scanner enumerate all objects' where a user or a group have write access. You can enter many users or groups. Enter them one by one and complete with an empty line. SAMAccountName or SID are accepted. -Or just press enter to use the default (Everyone, Authenticated Users and Domain Users groups)."; +Or just press enter to use the default (Everyone, Anonymous, Builtin\\Users, Authenticated Users and Domain Users groups)."; input = ConsoleMenu.AskForString(); if (!String.IsNullOrEmpty(input)) { @@ -250,9 +262,6 @@ bool MatchesBadACL(ActiveDirectoryAccessRule accessrule) } else if ((accessrule.ObjectFlags & ObjectAceFlags.ObjectAceTypePresent) == ObjectAceFlags.ObjectAceTypePresent) { - if (new Guid("00299570-246d-11d0-a768-00aa006e0529") == accessrule.ObjectType) - { - } // ADS_RIGHT_DS_CONTROL_ACCESS if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) { @@ -293,6 +302,14 @@ bool MatchesBadACL(ActiveDirectoryAccessRule accessrule) } } } + // ADS_RIGHT_DS_READ_PROP + if ((accessrule.ActiveDirectoryRights & ActiveDirectoryRights.ReadProperty) == ActiveDirectoryRights.ReadProperty) + { + if (LAPSSchemaId == accessrule.ObjectType) + { + return true; + } + } } return false; } diff --git a/Scanners/AntivirusScanner.cs b/Scanners/AntivirusScanner.cs new file mode 100644 index 0000000..ae4e6a2 --- /dev/null +++ b/Scanners/AntivirusScanner.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace PingCastle.Scanners +{ + public class AntivirusScanner : ScannerBase + { + + public override string Name { get { return "antivirus"; } } + public override string Description { get { return "Check for computers without known antivirus installed. It is used to detect unprotected computers but may also report computers with unknown antivirus."; } } + + static Dictionary AVReference = new Dictionary{ + + {"avast! Antivirus", "Avast"}, + {"aswBcc", "Avast"}, + {"Avast Business Console Client Antivirus Service", "Avast"}, + + {"epag", "Bitdefender Endpoint Agent"}, + {"EPIntegrationService", "Bitdefender Endpoint Integration Service"}, + {"EPProtectedService", "Bitdefender Endpoint Protected Service"}, + {"epredline", "Bitdefender Endpoint Redline Services"}, + {"EPSecurityService", "Bitdefender Endpoint Security Service"}, + {"EPUpdateService", "Bitdefender Endpoint Update Service"}, + + {"CylanceSvc", "Cylance"}, + + {"epfw", "ESET"}, + {"epfwlwf", "ESET"}, + {"epfwwfp" , "ESET"}, + + {"xagt" , "FireEye Endpoint Agent"}, + + {"fgprocsvc" , "ForeScout Remote Inspection Service"}, + {"SecureConnector" , "ForeScout SecureConnector Service"}, + + {"fsdevcon", "F-Secure"}, + {"FSDFWD", "F-Secure"}, + {"F-Secure Network Request Broker", "F-Secure"}, + {"FSMA", "F-Secure"}, + {"FSORSPClient", "F-Secure"}, + + {"klif", "Kasperksky"}, + {"klim", "Kasperksky"}, + {"kltdi", "Kasperksky"}, + {"kavfsslp", "Kasperksky"}, + {"KAVFSGT", "Kasperksky"}, + {"KAVFS", "Kasperksky"}, + + {"enterceptagent", "MacAfee"}, + {"macmnsvc", "MacAfee Agent Common Services"}, + {"masvc", "MacAfee Agent Service"}, + {"McAfeeFramework", "MacAfee Agent Backwards Compatiblity Service"}, + {"McAfeeEngineService", "MacAfee"}, + {"mfefire", "MacAfee Firewall Core Service"}, + {"mfemms", "MacAfee Service Controller"}, + {"mfevtp", "MacAfee Validation Trust Protection Service"}, + {"mfewc", "MacAfee Endpoint Security Web Control Service"}, + + {"cyverak", "PaloAlto Traps KernelDriver"}, + {"cyvrmtgn", "PaloAlto Traps KernelDriver"}, + {"cyvrfsfd", "PaloAlto Traps FileSystemDriver"}, + {"cyserver", "PaloAlto Traps Reporting Service"}, + {"CyveraService", "PaloAlto Traps"}, + {"tlaservice", "PaloAlto Traps Local Analysis Service"}, + {"twdservice", "PaloAlto Traps Watchdog Service"}, + + {"SentinelAgent", "SentinelOne"}, + {"SentinelHelperService", "SentinelOne"}, + {"SentinelStaticEngine ", "SentinelIbe Static Service"}, + {"LogProcessorService ", "SentinelOne Agent Log Processing Service"}, + + {"sophosssp", "Sophos"}, + {"Sophos Agent", "Sophos"}, + {"Sophos AutoUpdate Service", "Sophos"}, + {"Sophos Clean Service", "Sophos"}, + {"Sophos Device Control Service", "Sophos"}, + {"Sophos File Scanner Service", "Sophos"}, + {"Sophos Health Service", "Sophos"}, + {"Sophos MCS Agent", "Sophos"}, + {"Sophos MCS Client", "Sophos"}, + {"Sophos Message Router", "Sophos"}, + {"Sophos Safestore Service", "Sophos"}, + {"Sophos System Protection Service", "Sophos"}, + {"Sophos Web Control Service", "Sophos"}, + {"sophossps", "Sophos"}, + + {"SepMasterService" , "Symantec Endpoint Protection"}, + {"SNAC" , "Symantec Network Access Control"}, + {"Symantec System Recovery" , "Symantec System Recovery"}, + {"Smcinst", "Symantec Connect"}, + {"SmcService", "Symantec Connect"}, + + {"Sysmon", "Sysmon"}, + + {"AMSP", "Trend"}, + {"tmcomm", "Trend"}, + {"tmactmon", "Trend"}, + {"tmevtmgr", "Trend"}, + {"ntrtscan", "Trend Micro Worry Free Business"}, + + {"WRSVC", "Webroot"}, + + {"WinDefend", "Windows Defender Antivirus Service"}, + {"Sense ", "Windows Defender Advanced Threat Protection Service"}, + {"WdNisSvc ", "Windows Defender Antivirus Network Inspection Service"}, + + + }; + + static List customService = new List(); + + override protected string GetCsvHeader() + { + return "Computer\tService Found\tDescription"; + } + + public override bool QueryForAdditionalParameterInInteractiveMode() + { + string input = null; + customService.Clear(); + do + { + ConsoleMenu.Title = "Enter additional Service Name to check"; + ConsoleMenu.Information = @"This scanner enumerate all well known services attributed to antivirus suppliers. +You can enter additional service to check. Enter them one by one and complete with an empty line. +Use the name provided in the service list. Example: Enter 'SepMasterService' for the service 'Symantec Endpoint Protection'. +Or just press enter to use the default."; + input = ConsoleMenu.AskForString(); + if (!String.IsNullOrEmpty(input)) + { + if (!customService.Contains(input)) + { + customService.Add(input); + } + } + else + { + break; + } + } while (true); + return base.QueryForAdditionalParameterInInteractiveMode(); + } + + protected override string GetCsvData(string computer) + { + StringBuilder sb = new StringBuilder(); + NativeMethods.UNICODE_STRING us = new NativeMethods.UNICODE_STRING(); + NativeMethods.LSA_OBJECT_ATTRIBUTES loa = new NativeMethods.LSA_OBJECT_ATTRIBUTES(); + us.Initialize(computer); + IntPtr PolicyHandle = IntPtr.Zero; + uint ret = NativeMethods.LsaOpenPolicy(ref us, ref loa, 0x00000800, out PolicyHandle); + us.Dispose(); + if (ret != 0) + { + Trace.WriteLine("LsaOpenPolicy 0x" + ret.ToString("x") + " for " + computer); + sb.Append(computer); + sb.Append("\tUnable to connect\tPingCastle couldn't connect to the computer. The error was 0x" + ret.ToString("x")); + return sb.ToString(); + } + var names = new NativeMethods.UNICODE_STRING[AVReference.Count + customService.Count]; + try + { + int i = 0; + foreach (var entry in AVReference) + { + names[i] = new NativeMethods.UNICODE_STRING(); + names[i].Initialize("NT Service\\" + entry.Key); + i++; + } + foreach (var entry in customService) + { + names[i] = new NativeMethods.UNICODE_STRING(); + names[i].Initialize("NT Service\\" + entry); + i++; + } + IntPtr ReferencedDomains, Sids; + ret = NativeMethods.LsaLookupNames(PolicyHandle, names.Length, names, out ReferencedDomains, out Sids); + if (ret == 0xC0000073) //STATUS_NONE_MAPPED + { + sb.Append(computer); + sb.Append("\tNo known service found\tIf you think that the information is incorrect, please contact PingCastle support to add the antivirus in the checked list."); + return sb.ToString(); + } + if (ret != 0 && ret != 0x00000107) // ignore STATUS_SOME_NOT_MAPPED + { + Trace.WriteLine("LsaLookupNames 0x" + ret.ToString("x")); + sb.Append(computer); + sb.Append("\tUnable to lookup\tPingCastle couldn't translate the SID to the computer. The error was 0x" + ret.ToString("x")); + return sb.ToString(); + } + try + { + var domainList = (NativeMethods.LSA_REFERENCED_DOMAIN_LIST)Marshal.PtrToStructure(ReferencedDomains, typeof(NativeMethods.LSA_REFERENCED_DOMAIN_LIST)); + if (domainList.Entries > 0) + { + var trustInfo = (NativeMethods.LSA_TRUST_INFORMATION)Marshal.PtrToStructure(domainList.Domains, typeof(NativeMethods.LSA_TRUST_INFORMATION)); + } + NativeMethods.LSA_TRANSLATED_SID[] translated; + MarshalUnmananagedArray2Struct(Sids, names.Length, out translated); + + i = 0; + foreach (var entry in AVReference) + { + if (translated[i].DomainIndex >= 0) + { + if (sb.Length != 0) + { + sb.Append("\r\n"); + } + sb.Append(computer); + sb.Append("\t"); + sb.Append(entry.Key); + sb.Append("\t"); + sb.Append(entry.Value); + } + i++; + } + foreach (var entry in customService) + { + if (sb.Length != 0) + { + sb.Append("\r\n"); + } + sb.Append(computer); + sb.Append("\t"); + sb.Append(entry); + sb.Append("\t"); + sb.Append("Custom search"); + i++; + } + } + finally + { + NativeMethods.LsaFreeMemory(ReferencedDomains); + NativeMethods.LsaFreeMemory(Sids); + } + } + finally + { + NativeMethods.LsaClose(PolicyHandle); + } + return sb.ToString(); + } + + public static void MarshalUnmananagedArray2Struct(IntPtr unmanagedArray, int length, out T[] mangagedArray) + { + var size = Marshal.SizeOf(typeof(T)); + mangagedArray = new T[length]; + + for (int i = 0; i < length; i++) + { + IntPtr ins = new IntPtr(unmanagedArray.ToInt64() + i * size); + mangagedArray[i] = (T) Marshal.PtrToStructure(ins, typeof(T)); + } + } + + private static void DisplayAdvancement(string computer, string data) + { + string value = "[" + DateTime.Now.ToLongTimeString() + "] " + data; + if (ScanningMode == 1) + Console.WriteLine(value); + Trace.WriteLine(value); + } + } +} diff --git a/Scanners/ConsistencyScanner.cs b/Scanners/ConsistencyScanner.cs index d3a41c8..596d866 100644 --- a/Scanners/ConsistencyScanner.cs +++ b/Scanners/ConsistencyScanner.cs @@ -17,8 +17,8 @@ namespace PingCastle.Scanners { public class ConsistencyScanner : IScanner { - public string Name { get { return "consistency"; } } - public string Description { get { return "Experimental scanner which tries to identity problems when retrieving AD objects"; } } + public string Name { get { return "corruptADDatabase"; } } + public string Description { get { return "Try to detect corrupted AD database. To run only when requested by PingCastle support."; } } public string Server { get; private set; } public int Port { get; private set; } diff --git a/Scanners/ForeignUsersScanner.cs b/Scanners/ForeignUsersScanner.cs index 60ead04..3123e5b 100644 --- a/Scanners/ForeignUsersScanner.cs +++ b/Scanners/ForeignUsersScanner.cs @@ -126,7 +126,7 @@ public void EnumerateAccount(SecurityIdentifier DomainSid, int MaximumNumber, St NativeMethods.LSA_OBJECT_ATTRIBUTES loa = new NativeMethods.LSA_OBJECT_ATTRIBUTES(); us.Initialize(Server); IntPtr PolicyHandle = IntPtr.Zero; - int ret = NativeMethods.LsaOpenPolicy(ref us, ref loa, 0x00000800, out PolicyHandle); + uint ret = NativeMethods.LsaOpenPolicy(ref us, ref loa, 0x00000800, out PolicyHandle); if (ret != 0) { DisplayError("Error when connecting to the remote domain LsaOpenPolicy 0x" + ret.ToString("x")); @@ -136,11 +136,11 @@ public void EnumerateAccount(SecurityIdentifier DomainSid, int MaximumNumber, St DisplayAdvancement("Connection established"); uint currentRid = 500; int iteration = 0; - int returnCode = 0; + uint returnCode = 0; int UserEnumerated = 0; // allows 10*1000 sid non resolved int retrycount = 0; - while ((returnCode == 0 || returnCode == 0x00000107 || (retrycount < 10 && returnCode == -1073741709)) && UserEnumerated < MaximumNumber) + while ((returnCode == 0 || returnCode == 0x00000107 || (retrycount < 10 && returnCode == 0xC0000073)) && UserEnumerated < MaximumNumber) { Trace.WriteLine("LsaLookupSids iteration " + iteration++); List HandleToFree = new List(); diff --git a/Scanners/LAPSBitLocker.cs b/Scanners/LAPSBitLocker.cs new file mode 100644 index 0000000..b83f2c3 --- /dev/null +++ b/Scanners/LAPSBitLocker.cs @@ -0,0 +1,140 @@ +using PingCastle.ADWS; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; + +namespace PingCastle.Scanners +{ + public class LAPSBitLocker : IScanner + { + + public string Name { get { return "laps_bitlocker"; } } + public string Description { get { return "Check on the AD if LAPS and/or BitLocker has been enabled for all computers on the domain."; } } + + public string Server { get; private set; } + public int Port { get; private set; } + public NetworkCredential Credential { get; private set; } + + public void Initialize(string server, int port, NetworkCredential credential) + { + Server = server; + Port = port; + Credential = credential; + } + + private class Computer + { + public string DN { get; set; } + public string DNS { get; set; } + public DateTime WhenCreated { get; set; } + public DateTime LastLogonTimestamp { get; set; } + public string OperatingSystem { get; set; } + public bool HasLAPS { get; set; } + public DateTime LAPSLastChange { get; set; } + public bool HasBitLocker { get; set; } + public DateTime BitLockerLastChange { get; set; } + } + + public void Export(string filename) + { + ADDomainInfo domainInfo = null; + + using (ADWebService adws = new ADWebService(Server, Port, Credential)) + { + domainInfo = adws.DomainInfo; + + var computers = new List(); + + DisplayAdvancement("Resolving LAPS attribute"); + + var attributeAdmPwd = "ms-Mcs-AdmPwd"; + string[] propertiesLaps = new string[] { "name" }; + // note: the LDAP request does not contain ms-MCS-AdmPwd because in the old time, MS consultant was installing customized version of the attriute, * being replaced by the company name + // check the oid instead ? (which was the same even if the attribute name was not) + adws.Enumerate(domainInfo.SchemaNamingContext, "(name=ms-*-AdmPwd)", propertiesLaps, (ADItem aditem) => { attributeAdmPwd = aditem.Name; }, "OneLevel"); + DisplayAdvancement("LAPS attribute is " + attributeAdmPwd); + DisplayAdvancement("Iterating through computer objects (all except disabled ones)"); + string[] properties = new string[] { "DistinguishedName", "dNSHostName", "msDS-ReplAttributeMetaData", "whenCreated", "lastLogonTimestamp", "operatingSystem" }; + + WorkOnReturnedObjectByADWS callback = + (ADItem x) => + { + var computer = new Computer() + { + DN = x.DistinguishedName, + DNS = x.DNSHostName, + WhenCreated = x.WhenCreated, + LastLogonTimestamp = x.LastLogonTimestamp, + OperatingSystem = x.OperatingSystem, + }; + if (x.msDSReplAttributeMetaData.ContainsKey(attributeAdmPwd)) + { + computer.HasLAPS = true; + computer.LAPSLastChange = x.msDSReplAttributeMetaData[attributeAdmPwd].LastOriginatingChange; + } + computers.Add(computer); + }; + + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(ObjectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))", properties, callback); + DisplayAdvancement("Looking for BitLocker information"); + foreach (var computer in computers) + { + WorkOnReturnedObjectByADWS callbackBitLocker = + (ADItem x) => + { + const string re1 = "CN=" + + "([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\\\\\+|-)[0-9]{2}:[0-9]{2})\\{" + + "([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12})" + + "\\},"; + + Regex r = new Regex(re1, RegexOptions.IgnoreCase | RegexOptions.Singleline); + Match m = r.Match(x.DistinguishedName); + if (m.Success) + { + computer.HasBitLocker = true; + // + sign has to be escaped in LDAP + var date = DateTime.Parse(m.Groups[1].ToString().Replace("\\+","+")); + if (computer.BitLockerLastChange < date) + computer.BitLockerLastChange = date; + //string guid = m.Groups[2].ToString(); + } + else + { + Trace.WriteLine("Object found but didn't match the regex: " + x.DistinguishedName); + } + var d = x.DistinguishedName; + }; + adws.Enumerate(computer.DN, "(objectClass=*)", null, callbackBitLocker, "OneLevel"); + } + DisplayAdvancement("Writing to file"); + using (var sw = File.CreateText(filename)) + { + sw.WriteLine("DN\tDNS\tWhen Created\tLast Logon Timestamp\tOperating System\tHasLAPS\tLAPS changed date\tHasBitlocker\tBitlocker change date"); + foreach (var computer in computers) + { + sw.WriteLine(computer.DN + "\t" + computer.DNS + "\t" + computer.WhenCreated.ToString("u") + "\t" + computer.LastLogonTimestamp.ToString("u") + "\t" + computer.OperatingSystem + "\t" + computer.HasLAPS + "\t" + (computer.HasLAPS ? computer.LAPSLastChange.ToString("u") : "") + "\t" + computer.HasBitLocker + "\t" + (computer.HasBitLocker ? computer.BitLockerLastChange.ToString("u") : "")); + } + } + DisplayAdvancement("Done"); + } + } + + private static void DisplayAdvancement(string data) + { + string value = "[" + DateTime.Now.ToLongTimeString() + "] " + data; + Console.WriteLine(value); + Trace.WriteLine(value); + } + + public bool QueryForAdditionalParameterInInteractiveMode() + { + return true; + } + + + } +} diff --git a/Scanners/ReplicationScanner.cs b/Scanners/ReplicationScanner.cs index e5f539e..11a82b5 100644 --- a/Scanners/ReplicationScanner.cs +++ b/Scanners/ReplicationScanner.cs @@ -4,6 +4,7 @@ // // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // +/* using PingCastle.ADWS; using System; using System.Collections.Generic; @@ -288,3 +289,4 @@ private static void DisplayAdvancement(string data) } } +*/ diff --git a/Scanners/ScannerBase.cs b/Scanners/ScannerBase.cs index d6bc99e..90756c8 100644 --- a/Scanners/ScannerBase.cs +++ b/Scanners/ScannerBase.cs @@ -42,11 +42,13 @@ public void Initialize(string server, int port, NetworkCredential credential) abstract protected string GetCsvHeader(); abstract protected string GetCsvData(string computer); - public bool QueryForAdditionalParameterInInteractiveMode() + public virtual bool QueryForAdditionalParameterInInteractiveMode() { - var choices = new List>(){ - new KeyValuePair("all","This is a domain. Scan all computers."), - new KeyValuePair("one","This is a computer. Scan only this computer."), + var choices = new List(){ + new ConsoleMenuItem("all","This is a domain. Scan all computers."), + new ConsoleMenuItem("one","This is a computer. Scan only this computer."), + new ConsoleMenuItem("workstation","Scan all computers except servers."), + new ConsoleMenuItem("server","Scan all servers."), }; ConsoleMenu.Title = "Select the scanning mode"; ConsoleMenu.Information = "This scanner can collect all the active computers from a domain and scan them one by one automatically. Or scan only one computer"; @@ -59,7 +61,7 @@ public bool QueryForAdditionalParameterInInteractiveMode() public void Export(string filename) { - if (ScanningMode == 1) + if (ScanningMode != 2) { ExportAllComputers(filename); return; @@ -197,7 +199,17 @@ List GetListOfComputerToExplore() computers.Add(x.DNSHostName); }; - adws.Enumerate(domainInfo.DefaultNamingContext, "(&(ObjectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2)(lastLogonTimeStamp>=" + DateTime.Now.AddDays(-40).ToFileTimeUtc() + "))", properties, callback); + string filterClause = null; + switch (ScanningMode) + { + case 3: + filterClause = "(!(operatingSystem=*server*))"; + break; + case 4: + filterClause = "(operatingSystem=*server*)"; + break; + } + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(ObjectCategory=computer)" + filterClause + "(!userAccountControl:1.2.840.113556.1.4.803:=2)(lastLogonTimeStamp>=" + DateTime.Now.AddDays(-60).ToFileTimeUtc() + "))", properties, callback); } return computers; } diff --git a/Scanners/Smb2Protocol.cs b/Scanners/Smb2Protocol.cs index 17d0239..ac0ff41 100644 --- a/Scanners/Smb2Protocol.cs +++ b/Scanners/Smb2Protocol.cs @@ -328,7 +328,7 @@ public static bool DoesServerSupportDialectWithSmbV2(string server, int dialect, return false; } - var negotiateresponse = ReadNegotiateResponse(packet); + var negotiateresponse = ReadNegotiateResponse(answer); if ((negotiateresponse.SecurityMode & 1) != 0) { securityMode = SMBSecurityModeEnum.SmbSigningEnabled; @@ -342,13 +342,8 @@ public static bool DoesServerSupportDialectWithSmbV2(string server, int dialect, { securityMode = SMBSecurityModeEnum.None; } - if (negotiateresponse.Dialect == dialect) - { - Trace.WriteLine("Checking " + server + " for SMBV2 dialect 0x" + dialect.ToString("X2") + " = Supported"); - return true; - } - Trace.WriteLine("Checking " + server + " for SMBV2 dialect 0x" + dialect.ToString("X2") + " = Not supported via not returned dialect"); - return false; + Trace.WriteLine("Checking " + server + " for SMBV2 dialect 0x" + dialect.ToString("X2") + " = Supported"); + return true; } catch (Exception) { diff --git a/Scanners/SmbScanner.cs b/Scanners/SmbScanner.cs index e9bd43d..87169f3 100644 --- a/Scanners/SmbScanner.cs +++ b/Scanners/SmbScanner.cs @@ -132,11 +132,11 @@ public static bool SupportSMB2And3(string server, out SMBSecurityModeEnum securi } public override string Name { get { return "smb"; } } - public override string Description { get { return "Scan a computer and determiner the smb version available. Also if SMB signing is active."; } } + public override string Description { get { return "Scan a computer and determine the smb version available. Also if SMB signing is active."; } } override protected string GetCsvHeader() { - return "Computer\tSMB Port Open\tSMB1(NT LM 0.12)\tSMB1 Sign Required\tSMB2(0x0202)\tSMB2(0x0210)\tSMB3(0x0300)\tSMB3(0x0302)\tSMB3(0x0311)\tSMB2 Sign Required"; + return "Computer\tSMB Port Open\tSMB1 with dialect NT LM 0.12\tSMB1 Sign Required\tSMB2 with dialect 0x0202\tSMB2 with dialect 0x0210\tSMB3 with dialect 0x0300\tSMB3 with dialect 0x0302\tSMB3 with dialect 0x0311\tSMB2 and SMB3 message Signature Required"; } override protected string GetCsvData(string computer) diff --git a/Tasks.cs b/Tasks.cs index e9e7192..b04d1ea 100644 --- a/Tasks.cs +++ b/Tasks.cs @@ -422,6 +422,8 @@ bool ShouldTheDomainBeNotExplored(string domainToCheck) nodeAnalyzer.FullNodeMap = false; nodeAnalyzer.CenterDomainForSimpliedGraph = CenterDomainForSimpliedGraph; nodeAnalyzer.GenerateReportFile("ad_hc_summary_simple_node_map.html"); + var mapReport = new ReportNetworkMap(); + mapReport.GenerateReportFile(hcconso, License, "ad_hc_hilbert_map.html"); } else if (typeof(T) == typeof(CompromiseGraphData)) { @@ -433,6 +435,25 @@ bool ShouldTheDomainBeNotExplored(string domainToCheck) ); } + public bool HealthCheckRulesTask() + { + return StartTask("PingCastle Health Check rules", + () => + { + if (String.IsNullOrEmpty(FileOrDirectory)) + { + FileOrDirectory = Directory.GetCurrentDirectory(); + } + if (!Directory.Exists(FileOrDirectory)) + { + WriteInRed("The directory " + FileOrDirectory + " doesn't exist"); + return; + } + var rulesBuilder = new ReportHealthCheckRules(); + rulesBuilder.GenerateReportFile("ad_hc_rules_list.html"); + } + ); + } public bool RegenerateHtmlTask() @@ -688,11 +709,16 @@ void SendViaAPI(IEnumerable> xmlreports) void SendEmail(string email, List domains, List Files) { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + var versionString = version.ToString(4); +#if DEBUG + versionString += " Beta"; +#endif string body = @"Hello, This is the PingCastle program sending reports for: - " + String.Join("\r\n- ", domains.ToArray()); - SendEmail(email, "[PingCastle] Reports for " + String.Join(",", domains.ToArray()), body, Files); + SendEmail(email, "[PingCastle]["+ versionString + "] Reports for " + String.Join(",", domains.ToArray()), body, Files); } void SendEmail(string email, bool xml, bool html) diff --git a/changelog.txt b/changelog.txt index 2266b8e..cda6b25 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,36 @@ +2.7.0.0 +* added a network map inspired from hilbert curves +* fix a bug when doing a map with very complicated data +* adjust the krbtgt last password change when a replication set it to "not set" +* added the rule P-ExchangePrivEsc to check for Exchange misconfiguration at install +* added the rule P-LoginDCEveryone to check if everybody can logon to a DC +* added the rule P-RecycleBin to check for the Recycle Bin feature (at forest level) +* added the rule P-DsHeuristicsAdminSDExMask to check if AdminSDHolder has been disabled for some critical groups +* added the rule P-DsHeuristicsDoListObject to check if the feature DoListObject has been enabled (informative only) +* added the rule P-RecoveryModeUnprotected to check if any user can go into recovery mode without being admin +* added the rule A-LDAPSigningDisabled to check if the LDAP signing mode has been set to None +* added the rule A-DCRefuseComputerPwdChange to check that Domain Controllers don't deny the change of computers account password +* added the rule P-DelegationFileDeployed to check for deployed file via GPO (msi, file copied, ...) +* added the rule T-FileDeployedOutOfDomain to check for deployed file via GPO (msi, file copied, ...) from outside this domain +* added the rule A-NoGPOLLMNR which checks if LLMNR can be used to steal credentials +* added the rule T-TGTDelegation to check for TGT delegation on forest trusts +* added the rule P-Kerberoasting to check for keberoasting (SPN for admin account). A mitigation via a regular password change is allowed. +* update the score produced by the rule S-SMB-v1 from 1 to 10 +* fix the scanner command line when targeting multiple computer (single and multiple were inverted) +* added the scanner antivirus +* added the scanner laps_bitlocker +* fix a bug in the graph report when multiple files were examinated in parallele +* add a transition msDS-AllowedToActOnBehalfOfOtherIdentity to the graph report +* fix a bug when computing msDS-Lockout* in PSO (time were divided by 5) +* fix rule A-LMHashAuthorized which were not triggering due to a bug and improved its documentation +* improve the healtcheck report and added a comment to locate the NTLMstore (certificate section) +* fix GPP Password for scheduled task - only 1 out of 4 kind of scheduled tasks were checked +* fix support for missing well known sid S-1-5-32-545 for rules +* fix a tedious bug when using LDAP and when requesting the property Objectclass - it is not available in the result using the native API +* fix a tedious bug when reading SMB2 data (input was inverted with output) which gave inaccurate results regarding signature +* PingCastle does now have a default license and can be run without the .config file + In this case, the compatibility shims are removed and forced under .Net 2 engine. To run under .Net 4, a recompile is needed. + 2.6.0.0 * fix a problem for early version of LDAP undetected : in ms-*-AdmPwd, MCS was replaced by company name * integrate many hidden functions into the "scanner mode" to be more easy to use diff --git a/misc/RegistryPolReader.cs b/misc/RegistryPolReader.cs index 597f5cb..3a53e99 100644 --- a/misc/RegistryPolReader.cs +++ b/misc/RegistryPolReader.cs @@ -14,6 +14,7 @@ namespace PingCastle.misc { + [DebuggerDisplay("{Key}: {Value}")] public class RegistryPolRecord { @@ -160,6 +161,19 @@ private RegistryPolRecord SearchRecord(string key, string value) return null; } + public List SearchRecord(string key) + { + var output = new List(); + foreach (RegistryPolRecord record in Records) + { + if (record.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)) + { + output.Add(record); + } + } + return output; + } + public bool IsValueSet(string key, string value, out string stringvalue) { RegistryPolRecord record = SearchRecord(key, value); @@ -200,5 +214,5 @@ public bool HasCertificateStore(string storename, out X509Certificate2Collection return false; return true; } - } + } } diff --git a/misc/Subnet.cs b/misc/Subnet.cs new file mode 100644 index 0000000..0d425f8 --- /dev/null +++ b/misc/Subnet.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace PingCastle.misc +{ + public class Subnet + { + private int _mask; + private byte[] _startAddress; + + public IPAddress StartAddress { get; private set; } + public IPAddress EndAddress { get; private set; } + + public Subnet(IPAddress startAddress, int mask) + { + _mask = mask; + _startAddress = startAddress.GetAddressBytes(); + var endAddress = startAddress.GetAddressBytes(); + ApplyBitMask(_startAddress); + StartAddress = new IPAddress(_startAddress); + ApplyBitMask(endAddress, true); + EndAddress = new IPAddress(endAddress); + } + + public override string ToString() + { + return StartAddress.ToString() + "/" + _mask; + } + + private void ApplyBitMask(byte[] address, bool setBits = false) + { + int remainingMask = _mask; + for (int i = 0; i < address.Length; i++) + { + if (remainingMask >= 8) + { + remainingMask -= 8; + continue; + } + if (remainingMask == 0) + { + if (setBits) + address[i] = 0xFF; + else + address[i] = 0; + continue; + } + byte mask = (byte) (0xFF00 >> remainingMask); + if (setBits) + address[i] = (byte)((address[i] & mask) + ~mask); + else + address[i] = (byte)(address[i] & mask); + remainingMask = 0; + } + } + + public bool MatchIp(IPAddress ipaddress) + { + byte[] ipAddressBytes = ipaddress.GetAddressBytes(); + if (ipAddressBytes.Length != _startAddress.Length) + return false; + ApplyBitMask(ipAddressBytes); + for (int i = 0; i < _startAddress.Length; i++) + { + if (ipAddressBytes[i] != _startAddress[i]) return false; + } + return true; + } + + public static Subnet Parse(string subnet) + { + IPAddress lowIP; + int bits; + var parts = subnet.Split('/'); + if (parts.Length == 2 && IPAddress.TryParse(parts[0], out lowIP) && int.TryParse(parts[1], out bits)) + { + return new Subnet(lowIP, bits); + } + throw new ArgumentException("invalid subnet: " + subnet); + } + } +} diff --git a/shares/ShareEnumerator.cs b/shares/ShareEnumerator.cs index 86d4259..78dbc4a 100644 --- a/shares/ShareEnumerator.cs +++ b/shares/ShareEnumerator.cs @@ -27,7 +27,12 @@ private static bool IsEveryoneHere(FileSystemSecurity fss) if (accessrule.AccessControlType != AccessControlType.Allow) continue; if (((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.WorldSid) - || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.AuthenticatedUserSid)) + || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.AuthenticatedUserSid) + || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.BuiltinUsersSid) + || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.AccountComputersSid) + || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.AccountDomainUsersSid) + || ((SecurityIdentifier)accessrule.IdentityReference).IsWellKnown(WellKnownSidType.AnonymousSid) + ) { return true; } diff --git a/template/bootstrap.min.css.gz b/template/bootstrap.min.css.gz index 5317e6a1806d5c26eccb1ce16470c69e06bfea7d..161d98ca23452678fa9e48f5841e2fc6c50d452b 100644 GIT binary patch literal 21302 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

    K!)0^h55$ADV2nAdB3I!Ef zxCr4J5P_hiBqS+?qcoueTA41|Z_cwv&M2Viumwd$4RT_W9MCj7H8ZuNvG?f5kGPMS zV~tcE$j7<$c$oiac;kbsidE+$;>;~win3g=0Y6%FM2{I)s-*kZpO?W8Lm@<7{GK&j z!o5j~Q3YEu{+_ZE=kZizle)A#Ol?Q4DIwSAS)?rU*$P;Hhd;^i!0!wdz0h5UL6yCd zrQM{>DrWm+DT77D-QdfTakm2K5*w((zorDu z%bVRRfQfnrJ7(0$j3+Mp6KLYwgW$U_(BSH)U2G>I~N9$5)h z?jANM0(u1w1;K)Q$(9^t%j>G_SHl-kqdQgfDl!8`Tscj&6#I$dh$cCp>ipm}ft!<5 zpmYS;K+XBS2Y1rE;wLHO|#U1C;%$4G| zywu@6df|Vb;bAm3XSvYpB%S#xl`=}l(seolQgiDMSQ*dxv+*gDi{U)bTSv}(rZ!i0 zwO-$*gM&VT04O`~U9f#Q?2DxAx{|;)S{y~NE6gIOI=AkU9D8|c;ExnxSU`U{L84Do ziER?VSE%;o+n9eQ6`ZSv;t zN;dpLc;Ag97d4x9r#$>e#RAW*u6}K2XQNH?%-f}(N z^5TBvZ8MELt{wkyJ6>n&zZ=M|2D7Y1P4j7_NS*%?4wl(PE#1w1v|HdTSfEKLbveniUD?*WL8pg5FPb;)(?)Xvd0MYGJQ=zadF2h z_Al8<{JbD|cYUD74?8yim^>egcv_#1GdyQ@xx4S@5=kIx3`;p=7%Ll<$F`ZoND$Ngxsv9`#7~>Es)j(99RQ2pJ=Res~vhFfvWX~7y}isoNf&^dct}2 z0q$3Z#JVZFLLn}^C*Yu8_`hqyNajzXh5yD~U+Ywa)9FpD`alJIfGKxZ;qIQxaszPZ zh6r5Scl(^U2RKWjws=D{afmLAVI~6j=WQ`c!T&a~j>Y@3Uznz#y9s27D4q(5h_cwt z5gOa*4fr!r!dCY3w(-*$sA~Y0q+X>0cKgVJ>>@Yfo~I``%74&NqFsQm%{^0*p`ZrEqiLL? z)C7+8u~C=K2UlI3c;cA!jf=Nht?n8L$r%U|Vv#OQv$22hi!u3Z)o1H?z18%Wx` zr?t$T6TNXeO4pI>UV6T=UXE#kW#<*sh^H2bk&LQdS`X2{a&!#F0=I7E&XfZ0S$&aGgM4*xWNI!12rDv}DgE}zC!gXTL-NPZ zVr$Qr1}@2Xxqbh6DMpIMoIQHKGb@L}YJff-*_sr z$nOE3)g^DLec(dWR?_lEm)!L6(hr5H_dx0{9y%_{lv8%j zg|E-du8aWVxA%BA!XDr!Tp0g00@3`hu1-fw3Wdh=+j6S(?G0|+zKBNm|0hyc;nTSkKm)uBEcH~Gv zHPaFivY2V*0h>B$NH(xvw}b1zQ&8Ymg=+qjjC#riQbu6fk(rk+lyz!1J*5#X=FV>| zZ}rs>U98r}W&PXEgBH_u_K(J}@wpb$#C@1R7NEQuBSu(`t1mNWtIhETmdC~|zyPeA$3gnaLDOC5 zOyz?`-Lc(+gbtbm@@YE}u+@93-hbYAKxR8w1wzbfLQvo8a5N@`-T2$5N^#xYU{``G zrw5LA!KTp;AxWo_30*qPf{!m4)Mo33FgW0@cD(UZj{qFvCIxEjD^NY_9bZ|GC=)Ru z8)}HAgJY*1lU)#dM|96UJ?mC7qxsrnP#B6gC()E!DA)PW$bigaUd?!I1fHR<&d8`6 zE!?j86J2zI@W(7}<8;QBf3wVHH9K^Ov_3WcqGYE=72@{}goVh}n}^GTy?jj_QQ^Bb z6(^Eqx5BmL{My`6z)7(V0fO#a=~1v2MziSoYzTWfuBwH{zPyFT z{!DGA8@+%w-!o%nJ;a7KP0(Wn0`bZH`BX}7Gw!@N8P7Z^or+*#v{FsXie?MRg_Hj$ z`^LG2LI%q7P0!cCeBOCO@0?7JK4nMsx}*y3vSbJwxNXRYkJipT^w5Jl*@&ju8A&*P zL@4HFkc~*|A$In0%mPCh@`Z8<5L>Tc#ZU-#o_~K?fU3 z#1aFt)AA?xJuy~5>2a}UWdOs&m~DkiS{~4*Y?Ecklr_(3<5P>60ss zy60PM*bQ+dJT>D3K{9X)gWGQ)as*E79f+2JROAmtgxoZpngcXI=h4M!I!_^nIqlGBZJAU ztOGPlSIE(!8xWMjjtH@j+LfmJ^J?J7O3bm3)*wr=KkrKKoh%Py0!G_;-R+>0HmRdJ z7JmJfv!z^ol}XSV#|Jh|S^v8jT23)ZfS8v}iSPR$im{}au>ce%%~DUN1BV}7*rlZV zx3FILgKq&Wz-IX6|N5_9>S+H1+$xix}wMBc!cAl55XxnRlD+N4Y^9x zcIymOm93kL9_qRkQ@Yd6dsm6MbdpT@;8XuTbz*0W`f6jO%69%X`7DRko!Lnm>tOkr z-r_aUtj}bOa;YB3hHn95cJSknk@IpS*%wmMJGEXNUHP8Hp_ZJSSDStyHkU1)=*%)BWB!T1$m1%fl}hsK}6L^ zrYX%9sg@(2#p3Zmpc71cxcVg+iR-xolEu6x3N~MuLxyZ=flMoonLaaq-JmpY zRe$dAJhPiLhrx8Z)atl$7OG^R!#uI>l&o}`O%i}ImYTb%BS+$b!Yv{dO`!EJ&2ti# zpxt<5$wOF$&>6A~H-le1Fl*90@q}?zexdBndWm7Bw1M_0Qe@w@al;racUqa;0IBTH zPq{Yk2ivE!udKTkJ}*83f`pn?=8C!C@2tRU^QU|)p+j*w)h zsi~p*-ra{P%*xopltGs|W*23xYH3`scwyZV;_!$3DervjsA+wyn;z@P*wP0X>C&p9 z0I~?F)7R+p<_t$?NcaLvS?|Tt9pwji+oW9?10v1Y$n=cu8Oe1OD`m3;iC7M{wsJn%FfQAvq2YxCd`OOycI4-vulR*vY&d=$o7>n+>`9vwa4v*o% z=f8G)YvojKbNxxp7U0Z&bgL-!E`&rbY9tHqXjD-lV$i@p*lK|^su6-C4a=1f=$u1O zdQ0J|`eYNtMnqyDy_z9=P-w*Q_m6c`hDakhYgN3tLseW_nAJ~^V;Gfium$X(%;q(z zvGvlVrMHE=#Sa$>=ulNk$Zx~cpbF)P|Dp}Dj_ImRrN#l2h!LjW@1O&L83DcxAAQId zB7_#F2LA?)v*I`EcymXpc(ib7&!OhhD&cVpxIvlie%OR*Av#gK(MFRzp2=Z?*d&18 zgC)sf|IH|!(LF%!xkVFAG6$M zrpu@1oLbnQUqTwR@M*_e)mm7ZkMBlNEJN*B(=~fjA~QJXJsX>9GY@|Y+mvSHrg9d) zpJQRT#-gw(+G=~|z{u437}@1+HR7l?gKO&+I9c0)vd|Z|QPJ?BZAD&!MTH5Jv13`+NE&AuA-5o*D zt=ay5aq0?$z8DB1leej{TXHFt30%6mYnE*8?=x*a7L|v?Ow3v&?h~Xy;$$#5QeVY%)6B7#zaCtE zUKNr~nVt@7ojT=CU=pQ1y*pnDHF`b+tSk1Z=%W1kawL<>qpw&&%V(kBradhWboa zaHYXQBUxtf*rf^ymSqZ?Gi}$4SwfI3**&p=PR$d$1M*T zO`a9oqyDwlDe@V*wp{X@`SIr!i?;gY3(YItF*V3T zdL?=^Ded@M981%d-&s5Es1Fd=zGX2yzm!SIe2Y#4oPAo!`=S!}1Y?)VFW*nWBrRM` zbIY`mdIZJoDdUBD^a^TZUjVE&pB}`H5fIJ z`R6*9M{B-dB_o7Zc;aza;Y!=}?C?)>E!LI|vMctN8(gI5g+nDVrnH8xU|u=L8a@z# z%yI_yxsELdK^HAw`)F%tW#1Zkq!W>86HUr}h-FxtlvuaOpE3m1nAgL6`9cuJ_%anI zBN#}!?if`|369*FgnS4N;w4dE`b$gKMD0m^>%qCfhjF3lunKhO0aHV$U1)Kcsd?(- zGkB8Vu&^l}rhnR`=5sh;k(q}dBp)x>v~YHn^{|(Fx`_LrVt7k2E;3Jp`FR7%1gKp!83a@r5uetLiyEech_r z)|p%jbzkRLa~B=s+KP)L!X&h@4cQu4=dv1Wfsg1uf`4BTe^~f})6zWyVB9WEMV|w6 zmc#)@eMS}arPi5hqV-Cle7be#X@6s#dO=C4`>-QbcC`C^G*3pwiwvLS{sug-8-d@Z zz6SN8z!q(`!savnODJQ)Du8x=)`qp0+K>UebSgmnjIKyeRqnscpcFQM7DY9UzA=&} zh)>NrR68N%9Z!E9L)(cJhoXwoLv)J=RX;_3-xNMev(8}T)**Npe+lylr#LtKZG9N` zBYcGvQVX?I=)1hEiQj{Qy%TYP5>q%E`on(G^>vuQXsCUZBep)~2W2ECC77-?d+eHA z-;@zl$u!T>0Ee9{nf0l6q1!O`){urm$@91PX#MoL`pt5J@Z&O{i2Y=Uq`@YrC}wbv z8iBlh$C^b8F+Bq2h=BhBIJ~#Hy>VZ zs(^;~9{%F@-K;l0C@9|pN~;0qtHW9ZO>ezd%_c2hPhIt$bB&##@@|DScGMhvhg0w$ z$18Vs)%b>WOTFrt@78gSsA+XMa&;U9?83~~aM_qiw#%VaBsft0ZH5Qdl!-Qv^!veX z>}R-Y#^HUAU@VCWxTc>Pk(wjZsDD8SHG~=asuy;7I4=V}3NnOkuTH2#hGVtaLeMIv zd6*(&L_^CiGFi5?Duy_C(S@Ykyyg;Ul_s}3w#`UfLhFs}@sTtVj3w#O1dVa^260kU zo8uBtmNU@<-f6;w%4x9nvO>q*HTv!wf<;yP zN7E;kf;Oq;n^Iv10|%|87sW%Z>~r4fA(9&{v<`yO5~KjI1wM>00G@S}zMUvvJzAsC zHWOE-{x7O%Nqecgf-&t(THgG*a);uI*OYQ+_8Fgc6TKglXZsbB@^4U4zgN??-`z^q zy{6>{-Q)6tCu1F-r1WhFc@Zbnz_kN%vcNng7W(hOg8|~@EcX|~OJaHcu|{II2W!z4 z)qKzaL8Gc)?ErC_^tbvdUEgeR+kXDOQ;s z;UX?NM}f@FW?u_{c$C0~frGJKWQ{Mf@Z`;ooZdRLY@filyDfp-;#uRk856ax{$M&4 z%Da9=kfbUZXbwxMWB9wP?5FpJM(5l?&N}?*Zg~)4uxk#jAD#YA=A2V*tB0CZy!HU^ z+iK2rV6$ra8)mFc<+Es1qD!T4eH7-4_|!JRx`p?0i{oyZ|7_x`Z_#0ca99X)%gS=} zh`Fv{ZLYz7w_DVxqRV%FPs{s!KVtss!I``8EmcNog_xxY=Gd8&X|cwv3w>O!T_+0- zeZ0g7CKE_WgvV@1*67sW!D*mCv|XT1q97F4MvhnvSouICBr)6X6rtnmG#ZcKAuNVq zqLq3FU_zQMI60yJi6~iRv59zAgIUNzPwl$0gaO5BWLCz9=gNsQ&~#ex1z#9MO^rYK zME&4FK@e3Z1aVu926XH_)N_$q`WhDGk`H}8Lm1nN%E>LDA&|Kl0v$h_!(3A{m3O}3QGz|v( zPPVfmbK{U4g}}oUN<)aa0Jn6NKn@?+Tw++$U-x|x&zD9b>e32*EZ#M79GK+EX~&Ar zbDOb){F1f{%#r{c?cmU+1?~#&-8D-zS2o%cg=jQ$L-hIM^cl}ZwUr)eR^>Nwt6BKiTd*6my>B-8i>+MG*5g7Ro7i8OFb@BuPP#3iKTBr7qn#lK92 zcQ|mWegL8n8}2!eY6$E&V4=%4qH+cnl7&}WsrOu?VY^d0AAtI?{b~o~4YoAV->=dw zSIQ`W{IH|DS;W?}9%U9hH@cii5mm&1y|m)HTm)((mCzv**~$=zP6 zW0f)**MsKyRMGu5vhuH1QRwy5{QEUzR(zen6?+*)0~49EVt<{=|EG`ZjAnyv*ofH{ zHPRY2N{ymsjTl8st4dMSsJ)3*dsJ&vyS38VYLgfViP$xZB1S^fm_-S-YxCwg-}j#9 z`}O_3&vl(^{k-pUt79&VAgK?I#I!UX@JEh$J;@&^&Kh`wrTjEk$a^@QA%vQ)3I9!K z_~MQfjuke$2%27XyFGf?q#fqP;~52)uGH}$l)-k+MplOHR0xWA{H-DmGu)4HKs>hR5v zRS0$`y|=*LIpI^f#W!H{z`>?c3|_y>dv4ci4_T&_Y-r`oZ_U*+))Pcqwp(l=Ihsqs z_gOtr!(v|WhG2{YfSF&>lqN{g$>gkI`+67IUm6-pDxd2bpkZR4KVzk8?6?E4@WPCzu4rKEUk^|wy9(Q){dI%*mNR5xYC#~m4 zpyBM}eRoexk>nkpxz_+bauB{&%mTRhNPwBi&^p1cD=R`;npVRitotKXO(k1{x2hpa zl-;YV6a8+6t%*qe zrd>$hYHxB@%t!9j8P{WYa#a+Se$Qvf3no!Y?`#=XBO@2ipPR+jG`p6qw z)V0QTt5;!%G8;gCMh~gr(9q~H5Av2}pmScVh+l$Ka9UItm$2Nd_jG{d1BeRp;(X_Z z1Eur<=lc-4s*dL}lA&AjzBeSs{HbdbLq|JNO#u3mi_@{rb_6GV^ib${=kFq!He#h} z8?obA8?oNxdspbDDWK`FUAFLa%gUGgGJr7@=PbloalQ|az*(DcLc$K4BF?=0QGsE9 zn?WI?1kd@ub9>YQmw8?zzcf$gTHZmg!s<@{#B7E9i3gyN?Fzi7n<;>%OKwnS=vu~U zAmIA0=agP;Zs7?r697N70y*zcwE1kKQ6>yY;Lv5Y^$rt;jiHd`%Q4TmzN}ugYZ~2L z&z2SLW^rX+e*HN3s-U{nQghj(MPw_!W*6ona@iOt;-#-2-2Et1zWHS^ zJY|qG(0DdA&ln_c2tt~G%Byqtj4lWv`p4zmmt($?F$nBC{uO-W64|k)H@5UqV zMOIleeLoL^te10Zd`9#^NaKH)&K0f8wSzZBR&_FcQwBj#mOV~IRvj_}dksPIFF`_; zxjEIjhDQIe9WoCemy0pVWW}irMUCvv*Ppc<1ZKqnhoZW7o94`Bzp@NurM} zaOHS0#w$ZneY^8;Y?teW!uqRn|fs}F-F0EDD!o#9tF$lPQfP1S#eTBQ9Zko z&cXVo+!|SN&RLiI8`oZtQQPt!@vBZfln7Zb9j%3Y$Pje!qSvtz2WpzYU4vigxlTJO zT~x)gkOg!w_M9*b47xl@8_O!kt+hV6OZMefVR7t(+DC^9&luU3$$={S3{-@35fz(g z8l#mWoC89&jWvRaPd`%&U*yS|6mY=fd<(vTjC%PF!&Q_2ZYXz}n;X%jsQJD~sB53~ z9i37Rp~;q)wnJN-%B_$>*p4tBMi z4Ky(fC+t{D9ITbsAISwMpPq*CyTYB1d_{v1Iu=q=6<=l`b7?#h{l*E8EC1eegMQDJ z$fn*Z`kH#!;gy(v7*}h(RidEMnnm;K7vn0YSD52S<33x?hhcS`I9Kwsy^!zn&2pz? zU)r81S4 z1$p#IRtT$hv1dgdSHT&FeJz!yjh_cvQjZ9XZ1mogzt*UdD%27u7xM^IHC}&TGjnfk zDbWD?%mcLtPT>fHApF0?WLAr z37a0D_MvThq}Tsdep zn50EJ6PiC$=n=>2qe1e}!OE%h*>pqu1Td(1Xxn4T!rY4<%(jjwe0*3PXGnC&+AJe2Rp5N}dHy<9{GNbq=v@E=`Z>+Ig(4g2k)sZR6 zo>A8IRpy*^(DUiLpiy#DvX_xg%4$NGlGE_-eAPnqzJ6K!@fh2ok^V8(c@c$dK3a86 zx*~R+NyjnTbMD)&k=&;T;t@7frUFjnl(`zI7cs+pbf;fsqncA;o#E%yTh?6^brgPd zta%#g>=bf|HQdU>Qhz`px>9!in9}PtWYs-lQQ_&or6SVI?sQMAj$0hY#R|T0LoKGZ z+G<@da@4FY(qZaK|1JGg%lX4&jyjGCxSBz}Cjq$^9I3!7s>SnU0#a3ikrWx0RL^=l z`Y}>iMNXhc_(~SAhVtI~;&eCZ_y#kFPyDV)U3Sqp%2Ys)SbVXgrl>xT=E65WfuwP> zQg$J<_$6Dt{ZHyhw%B4JZQ#*Em`ZY{T`Etwtvl`gePfA7ZX5s!Brd95Re80{Ebc7# z+Jno;?8r?irZ(sDf{!y#u%Up+N0`3#9VQcK~V98%ARzCH11`C8WEzT7BhZkuF1w|N?D!1{u z?(^%|Qv{hi->9N!56`Pg!^xW`2S|fCZJM#{J5^ix&R<0~Tw~q+0M4&eiklcvFX=`l z{Hil>CVk0oGdy*|1Bpq$rUH37w~eRq!*jo>DUl&1-u1D-ozcC)o`hqX5TT`?Haz{; zM4GP;Y9A%hXJ_uxiQez0m;gPd9n*|`*(qOF!gYA@bpn%Gl|onNFI5ZNp%--!U115B z9HFNU9Oi9F`~?lKwRxxX2XNy`>RUvRPWZr8gFn%!(_=sH^3+qDoO-=wZ?>Nth!l=3 z&XHB9-?M7xS#Hdg|5F;+s`9OsP9!DdSSZ1l{AV>g4wP6X76hX$~+ zE9T{s4j8nht~s(>4{EKCfJH8-6RRNk2+zZ6tOpBoYR!yz%d_lORp_s+ zOG-FZ;;(czc|xU06k+2=z!KJ;5(7Hi2!ajR+Cft4t7t5+zTJfYlZWEfCQmdCh$ka& zaA>S61;T`ErkeBP30HAM^y_nW zeV87?M`i94>D=i@vlTLiI2ycka-S2o_sL@^ggGFO=x>I)xOEez*WjZjCR94=O+5fCWcv_h}Q1CK`HJj0SSg zTj&ip`L=mp#TD%J9k^x?pB||cpz@P=_LGTi3l_EPOD7I}2dv6Vo&F*D-U}i36mVb- z)PqcK6>=|cSUUXOdtvMhAC zyFooML?h+Jz5}b*FkIT}^F8n{w~*Y}h~IhEOqo}&x_>m&{QXU}5rHts0#eooks6wNxv(Gn9f1iyg3 z^bguhK7BW&sW z0H`jX*`b*v2HJBOuRH8e;t>b@6iu_ht)>>n$}p@uuv9XIo!@#@_dZ7NgpqQ!qYdW#Xc?3%yk>D1KUFn9pVu227LS3vBSXbD}8R8^;%O8#B=@wF8urn+0I z9hzso`Lm;2*>}SDX6o?ML8`pUcAc>vxs}R$%G9giV^G1QkoMhll<$ zf~z$uwY?a;aJVj~+gy==FsqZjtBSn+sBc!C(i2ylq>f`K44n6&RxKxO#Qe7M%Q!{D zP-Ml{e;jNur~d7pR-C?A%xR|?w@l0bp_7}nvaLgm08q^s!{uWhSv zMes|gD$8)m*L8Gu+nVwwJH4!pl`b>?AFa#3kwqe?PL@0GPi#?lx69L^iUsrYQ^I8S zlcGv*ZzlbDT>hdcum`%;ZxZ*jByDY}w(UNfoI4WHxD{0B*k!iOY}%kTT^4;W1y42q z&^K1}mfs2v~s*jTT3bwaWr#!BsNezw@&3SZ$d>~V z3Us?6-!=! z)Rhr-paONWOTG5a5+7tl+wgdyf}Dc_8y8CeP>SK%CC@>$b;a8D4*|7F+L!wawP-x* zecDG-7L`rQa~g4j;T7#c2$xZz7D-@SDaKj+qit>I&TCuh1wiS7jvtKLa&RbbL%BO6 z=f}p6qg>^9>?|G&bb3XQ z8$SUufbN#AP(A7lO;9y@fF59Jkv z3wrNYj-Sj1!i*598!mfhYGxfNl9fw~Aw}?Ydq?d4c#Tu?GrK`4Q3MfXQ9Npk6$7yv zJ%^0ummNuY%VNoS)KX6K86&DtF?U$sBv2NX14sWDi=~8dtE~3!?`qc?7Gav>63N;^ z%FPHxl~%o~iQhS!ci`{BAVR3a={rOT^t8^Py(fE>!Q6uOUz??+`NfOS*nAHg<7g5= ziI)g$gjrK{0185x2`J}~G&_^G%}Nvtlc9OQC%5}#swzo9PZA|QV+h7`9B0}oK@&kF z^R(F-49Bo8;vFI&vQM7)4Sg5 z1W~-jsS|Sfw*8BBHR^_Kdco1_0uZg%J)gG^2S(j&asJYzgeKnU?zf7IjcCxExE^LS z%n7iCAtSql+(#d{8C5n{OmV4zv4gG^)PLw5h=%C8x$)p-FdNP`Dm`z&Qv#M7@TmG~ z|M|+I9X;|k%CWytZML;w&`mqrarMbeFtxB1?GXMxHv6LRy|-G4y2y2|tyGe@6x{ z?GT}^%6wJ*z^v&WzhtIC&1MATZ4sB_vi6y}T(F4MBzTKzg+-GU*zxM`gx16g4~;AB zySPkb8Z}uhwf9?FtVVx{fLUyHIdZ!79(}v5=YlDmt#9z)t%!LNp2rw=H*?%@tUP%< zRF0+A9w&sC#jKM-c|K(32tIjXAOK%L6RmySW$@C1)-&?Ot@D%*o@#6qNonmmhJ}!z zad=RIb$$@S|CedcD1iok_MQLvJ$UK%<$i(hj|K|eZ#%Ww>&<>2Y6c+3mC_OyI)ZH%Tv{E8!9rA6r@7|h9HRJg{~JQo_@dl!D|9_- z8poc`H31L2;geDdW_{eHX8~=4MZv5gH|7$!F{AW1IbfXR23^6HRLUR~=+b-IN+I*B=ls&IX>B4jR;gOy95c(EmW% zPX9j5TfSd12%efV=DRKx%dEmv!p_ z*@%H%E|%y?L}RzuvD)lm-m%sY4@x2bD6q{el5JKlFcI5Q8vEGiILN&#h6uucZ#=uC#olM6^^`grggx;B**!AIyg{R8>uim+6`Hi{yP4CO1luj({2p!nB{Ndb_FWQJr zHD?^KC#H`z_QSOq`)3FQCz{sRF*Ckd+Q|fcQeu->lH>BK*_p^W;0t*dgdPq4m78(6 z#?MOS1PRJ4i@{EEol$`?s3lLPwL>;>J9?S4JTwZB z$#-=T0O}6gUZkfhdT9Ah_a9vt`(zzn`-VPS^5~eBNJVX?c*-SBZSdPd97T(YHe_8D z{?Z~A?r|7L!Jqi?;>ngg7Td%L6m3kO%;hbo(%m+&J%X#Pl-lk~x~ zUnhItk@ksZb#Oft=$;VDsgHy=$6%(f&kxx9@lp^3NZI`^k|nm{4b+L0-dw%2Xsl>} zXoP5pXpCr(XjDi9QQ^n&lnC=b7+^NyaSC!}1S&|CU`rv`f>eYrCrs}QbFoN+ep3+4 zlVB(z(t=0@*>b{VBtySibUm`n1VfB^OhGZuf^*p^MdgK`iMwsZFE?4@(EjW#vo&u^ctWr?m*7}SM>nI(6Do+X2T zdya$smFQR&WryKo9IZnnn-(RwPHFU1)f!*A^HP^AIFA@(XyMPH%`D4~(?&ShM2py( zF_PR?qBhL93_XF1@yGBqa3XHxV9?5f!ytSL35zw?%D z9TC8S7{PWZ2G~!8dw;qP#%_@g(k7v3ed-pLx`e9NNg>f*jAD$qQSL-aC~HOzIf5P5jFDdr?et$pC~-y)>i@}d#|}HL;}l`io)dLa--rB?_0|Xr5BRZ^ zSj3up+qcF{ulDAz&+D6wai;|{i|{(}wsQcsvO)&F2JlzGL7qwO-ZNrgSTMjlr0 zE*DGA)0CqGLyjaAIpRRXkR##$WjhX>Z8>(lLhB5evWXdakQlScuxAS}@nSIKl3>r~ zVdBJK$|gdcDE>$Pvt?s%4Q-FfTZG8^YeIbU3q;NOlg>q09@xY|Vd=f@oF=?qK^r5Y3^)8}3vU z%c12myzzNAP$+)9AW?I6aGF)@3!1 zs1|B;)o)b24F^1;wkk-va_ zn}AN7qd0;m>D#qo#Jvb&;ORpqI>Zgb(*ncMb44rF-!2xqJ3a^p zu|l%SL>UG=tw;?p#e=J;Cn<;)I(&h76&&yHF~sA@f`$u(zM~D>Mrr zBqBfxnIIjj)BUS3C$`tEwXdxQFjakh1Tsogzwzfv19J&@+bH1afLJFe7|o&~6s^_A z?O$D)ay$rc?w1&RsOA+T1}Tee5@`YARo6g-8s$U$M{Q- zJJ%nDHGOoEJ0dcuTs3)rmWgD1F_=VRLSi+_xGyp2kcV8KuP&$70P1}6H72m+S!a$? zGKuaR{Hr{G&6ZTh!Z62>8qmEEo9wsDw{vd)UXo{9Pu1GqR!kGC7gRO|Uu4>w&t|Od zE-t(6UBS$2Z}GP@Ncr&wkfx%Q%v$<|3z}%}pT*@QIQ0)$-J*x(i)D~rv>LW|f|>@J zW>2Nz_T!N@mcdz-&PswR>@?*FYH;f+Cz5LZ^5AV6nJnPnOSVf#X<3>?(0+l=- zG-<94I|vNGM9-Sn|W=~Gv@y2(>k3I=38GSF^7xKu#Pzwlwqog-n4k;7FaQlph1 z-+xac`p9feQbHb^<4M`Swp~+GE>A|7mnhcwbZtk|It!<$qiI?7q-%7t*OG_yUV{*Q zRleT58hP;mip3qwFn#x@Ub;I;PNsp|thvQgNuoTgt&B4LZ7z|X-9~${2av4J);+V zsFD|Z$F_$}q*F+^NLX?pNqBZ?n@_5p*s_{(l?ptVIO(xo>14TXC~vVns7&Ion^`75 zlfWttG5b|kwD!9UiG&PW&<^E)swM7>ci094Y$AVH<-aSxV$rsM6Fh8Zh4K(4?j<_# zXA1*3a08`x>q}qs5&{nnzfZzxB;eg~_Jyht8BR^KJ$sVUQ0(8}B!^0cHzNWw!f3y> zI7n{7#ISewBM+|RNN99E+DC@T3ERyh^Q^*R$2j&EhGIp%)SYsE&U%KCQw!N+zZE+t z1esUOnK?~zJV4`cDVO%xtQ8qFW5)LTq~CwfOdX3^YDk+@f>T#WA9yTGJp#bdW0(e@ z&nnsFdib)f=C!q$pVeJKzO6jLd$?t=gAhwvO`Md$$rVaX)F5Ac+`Sr$KZY(W>cC!c z8GdDiazG@yvMI4(!^?p1M~bnCRf;rf*ga}BcJHa#=`r3tlG4j-3|_d6$)5B?NV`U? zpXmJfie#C#Wy`^b1^I>l-2A#?feOx3#m&F5a(Zv$?Zv&r=NF8e%gUs`(AiOF)>K{o zoXes>>xx12;rqmXW6b=p{2tFOOv@iS9?q%LK$!ko&+?g3JP$?mm9K+aMln{!{)k@8 zk^l0D8GK=uTr)o|Suz=6JfRW2BAbpBtmlO-C_fk7kaT&nwnjI&!?{GWP+*O1wE`=e z=_RjwK1gdc=8DSd2F>bT_0u8uAfCMGP`uhVt=RLFGk-Wb(juut&|sZE?KRS23|XhE zU+0CFfnFJ=uFUGqFlJd+EUujCiZLf+q$Rx1uK%#&i&$sSxr(EJbrrq+4F>=c|MQ0hWmYmJAye`zkf9Jij!(&BbsnF8T|bx)+CMmN zhr4NX2!r;4 z%g7RDO!D_CDUxc*kR~2o1j={%6!$rABGcPI2MDA%xX$z$*SWkE02qxeaLdx=|CZ*< z7-xnB1khzed8j7YjFK$3e4;ew%dO>PRD1zfd}aGOPhu<0r4V%Y6e4fA>kIz3uyiB8dc| zkD&Jg9qe}L13{FkcK{mvZCWFU)51CU(&uN1K={fkbZ#fE#Pr$=XBbc1yVlu?{jb9N zlY$CCC?e3oV$%t+`wj*2Z_Bt~d&N-&bT8J3oVQ_YsUZmf{=WX-S}x{Xbezr^OKa3f zj0f4jeqSwZ)Vsz1ieDmY{N2~DVL6^k7x(YCLyMGK6=;!Nzm<38kCup#JQTd320{pk zLK*6Xc+U64nXu-1V@b@_mlJ`h^K=nv zNO8p$>!F3pMY(GS&&GqHtb$K8?~F0c0sM%~G5fhNAp|MzpP@Di3VE<-O~{nJcla0`XM{3D&C2s4bAUo#< z(muH|&k|&(9t^`?yajdo7hhiRmpK;Qp(d!HeNVvP*=ip4D5D`;L^LKj2#lfna|G{- z1|{^~HJ@f5_%cnjiF{Vg6pWKE$>qJkr1)VA{OJ4Ly zlF){SAX0uTMT$~(zIr& zCO$8`KX)2$yAvG|-bPuscWwMvdKLjnLQM7nV^ze5wT|Bgd+Z zE!t~VhE^GOuBo)fc&9E4BQCCrq#K3O-Lu@y2_#dddR+2gbZ)ytwr1|_qb_w0-X*(4 zZ9|;i@8yp3vkUZky*BQD3HpbA9b2*Yd(XngDDz21m}?6h%4b4!^#xEJ0tF`IFmorS z8T8a+njI8vQ`?PTB1H@^OBxAe8L?@@Qu;BD0A8`i!NS;F4f~;{DBTHmDyVdWO{Oa-&{JsluuqQBRXh`wEE;tZ z!2+!&jr2L>fCc5DVmPG*=&|O8vNwlJ%gL>JLthcjOO%L*Pi;iTx&)Vo0d8#EY@F^|bu z1Q+cBASNi`y}>lml=7*?jR@3F0qlDQ$qzFS9|y?LTu5eMoazm@di6=R`eC(5egJ?5 zjJehK$mo2E%VDgq0D&wxXD`}2Iq_JnTA|mk0IrRZdQ|yXT$$)%01i4mh(^H8xzErp z!~<6&M(N!sN;Xl*yJgJ!Uk)w*PYzddaCcyH)sKaHJekCfY=CNup$aBpc(JJ!uB$-v zY2`z-Pe^`%6DTj+$KMNw$xQf_z1ym*N3KHA>|%WgSR{wM<5ZP{y)VB~xpZ zkFA%l!}U4L-f{TI3O7tExec`MwQs6-SPiUYOo~n<$c;d+n`MUn@p?E9Z#_rDPRdVZ zt*xtPC>|VD9ay}lhnrMZoW4I0Ft06jnO&==4;oJ~$_as`+XT#$N&)>I!Fu-NPuiS? zMAn(yp#K73ZX?%X--ZP7rJkg&7`H=Lza7_NKtDUswBu~DK=`AkN$ORSC+!o@c;4d) zN2Zk&{ze&(giiVIjwK8&*>8{iJ5qL{`Q)_qc*VowhACAzb4*|A2PD%ujVVBz%o)pi zjs_YmnYy!ga$qv@x4*h)GVMVgt5h0k02VOaD0`byzm7B8IRPLi1fuDlW)r4PcaCGf z6!hhQ>?=?7ZjgIqG66D)mzVT-$AJ6Bpv%3C3GJDlk-tl6Xvd#gnF5px0dDuoXbw%B zULC`JCg4l%p?@l_PCOaJ#{+g9JRRz=uO54RZ_JE#4p__EMl=%Si#C*;edILXPKQ}V`DR2YfwYp39?Ea~Co$dC6 zyD(PpFkj{aF1>yC__F1%=7Ifb-&mfpcM@v@9=!fCFWSKov;H73bwSTseYRg5I^Ff? z6Ro7zaqs$Y$5GWXQ_LY%ruTT2aPdjZr9bNMZK=CD{jqP0PU!Gdch=Gz1py;BcRv%% zJD0z4rFoLHTV=xM_bZMbTjq) zr&rk{+;OJ~u8nK(yH>+Y#gcitBfc_5Ms|kgIo@lv>(}cnm9EHvk5)SQ@2i_(<>J!2 zTJ;*yHO|z6+wumocu&}E2fwJ0%+QP&t?O)h-LH$^m+6h|qV%l&(A+&hPnGE`c4LDH z*NqEf{|e;dC-j?4Q~d8emi3^aK~ohE`|c70`^rVmrks**R4lcuVr;yvts7BXc?E+} zX-|-v$R;`@y*h&IxOjr>IJ<$ZbK-ImGvAT}i<6l!z6emsE6Ra?SpOD&*7+hF5>5z^ z8MiPp=b>4gq;1ozjNThFl=m22^-n)CB>R;(p)~!H;ldOssfp%nD7I-$@e`eE)ET@K z&DDy{6HL|5Z7i5I1QeBiH;-koJ(R%@+nfM77;hLU1c?*aHyHqXt~Vp=Wcf^<9uFHo zk37wI^OR1y852Q787mLo%x0h!C(77U-4~s^o6Z=wM6(5;E zTRIfHVwy+tW|u8%sKxO~<^A>dSFXLTPB{u*S^jSGB$e-;=p0UT?BpuzN04^QweHpK z3R!4H{d`qpb;bsMdd4Q6W&T)n=XKY1ej8yZ5f}U6p1yb?Hl-fg#+E?KRHgOBw#lqR zv@9i--yv(E1TBh~9f)Zt1mhi~iziUIJsjDL!7;7l}H7V?%An=i7CH#dO6>S09m5XWzGPkDKwSvrF5BDkL9tEga(m z&oI0G5@O&{K+xcCf^+NLpkp~{=>C~7nTf5_Fz|;Bm(>Sf^h*0jz3;+0w~yGa+Ed_m zdU0UJ;8EyAj*e#tT44oS?7{*KvKVMb^Q@HUJN;Ei4dB~xE>%tq-pBd-oUTdyjY+pi z#m@bzrd;|%oxn$+$z|ln8s`B@B5p@mny;Wb+>L^ZD+9icC)}TjHo-9LJ=rc6UBdn;-F13E#^|rYN@28X=Kjojk$1yr^jI}A*ZKVB<;ew9#RN* zOaTaIwlrGRQ_b27Pj*4?53wUa+qluO^b4{oeF51I=W*=`@90 z^-YaoDyd1kSQ~IPDPGWlj&_Q)9c=`ZZ>!mL_><*lOV^LepQDsxrSv92s4_4bi6ZMI zlEg9Lrx=sunz8~C1Ux_ujANZ~`0$O&-sMBAq{3n&m<4jt4degJA0n3v9IlMtoPBc% zE_Hj>`^N6eP#P(nY7S@P%J<%?UsO(;s+(n^U#fZKOr*S+6e+#Es zu$%(oz4LjN#+R*pDxI~Q5HXr=V}qkkOOP)kJwvWFL!zNeU23nc_fA2P1CO>1U&GX5 zVhJ06U41)Lg=h8}?jLJjf^`{(&dq~&@z;n!yyiN@XbB9gz6J$RI=vk5R3M;S-F?%hd7 zjvi{uC%daR+uT|T4627r?G0uz(+(;kUcjeBvd8$vp7CQW*wGX0#_x5Y#}Ij51bk_} z9zu|7>55924=zoYPDyi?B5tC02VIk_GwTYix#Sm1ry5P3x+MF!vPz#Qa%$0<%mtCc zQ=dsSOn6AI6b4w5*jsW|GY`+RlmknT7GS!4{-z5vxIJh*B~AumQP}c;3t|nN2%{2F zWi$}p+&?YP@LBw~FahT8T+Zc85r2mEMKkxWix|T}dj!6AH7VibwYVU?*>thDWL8yi z<>xL&ia=McLU!pML2K7ZyF>6s}O81bc+xKp10WG*R=jzKIBc@mM1s+yqw* zX+Lpi_WZmTjqaabI>Mrd*mFYlZ5m&5TA4c-HD?}XtZyci{jgf!1dckl%3l7G?ex~Q zL2#j+FUx2|KrdE1#Y}ls&3*hFa*Dazde?>;yGSn*4Gn8RV6EB?jGq)P)bOv&iiUO$ z>+QcizScU6M_&5i zS=J6xTT~*Z`^;n?$!{CXt|R5v9B}wb10OcYdb9!3QOD7-RUCM3`Cq8=BLwzYviH&Sr)!JgH7ZAGWYf~y^+XuGvbO$v%d<7W zic;@dPlS?qWt+b?-J)y2eB5NGEgf(ycaG= zX00Z3D@Uq}+}3SY5{A~swi28xRUPo*ZFsMSgjCmLLdOK zS9i7`@S9C^%;k+F)8|)w)8j=zMZ>1j@$m5= zk}eSAhcXdjv0F*sq=+(%KxukS3R1jP?4UNMq9hooK^g}bL6rDl2q>}lYH{SwQfqE( zz@EYfxLXY{j@(B0(c4*UHqi{dn(V{@m5f!^hn@_8&e1Ddea}9*2RS+q$pFl;5C8|3 z`D2bGvUr|y4gG*C#2GBbF6mL9hvNNR%fwh&@@%9wzCrmZr<%_nU51RrV3CHAwNe#K ztO<&4liF6vPwui>bq_46iF2E{5tIC>kp+mRxbd46+^V?ib1Xvf4H+yY!)C_qYl1B`- zARt%9%L|1@$U1=@zD4}FDgKo?yVdxq=#ATIVJ~D>b7|_HiWFhcNBwANGpEoCs-sVC zWgTCOb#&G;P+lD&(;_dTNXC4)Yzm+l!*1w35D`7M*W~VbJax9l_av+epB1E8+|DAU zHQ=61vo0NN(u@CN(iqZ?WSWaCrh?P70}jFfsq5VbeTqDp#nY02C7?vPIFO*#y}BZazjryUUu8#VOB3$z&lClGfP86>(CoPn(I9GAl07=ok8krvI)P`*`2>r>ph!Uu6{_NJDo#`M5{^L+6{j>jF8mE1KFbPSW~Jwf zBYzB`K8+N{`lN{)FG!3j6wLXy?Xq@~Tr$A9%LlFlBhr zF$6lA^#*mM_%ZCYr~a`a4Tpsnr{Z03MfOE50qi=!e8|wPQeS`4OrY$ z9gynNB(*Ks+kr3~8e)`+(_hZM&!XTRk5Tp9hg9?Qk5l@r-^H$Y7I=nB&P|6<`6SV( z+1C8KlZ(g3i(T{_mOo406S)zzZQcZyZ{Gy=&fW}j*^_7iFLcX))fZCmYWmfpO4|k> zI2r@#3!jW#SW}$DA}V>X{Dj*TpQWTA=~AK_E!C)c`VhlQS}{We7$vI!lacow7D9LG z_eLP#cv?Z>)P^AnCI*WoD@-x80AGC&MDXwJdokGoU*xeJAspEiX+`Tu8h=v2p zoTt_+rT1O^{yKL%aN7a5)`;2kTTYIPRlo?nk$9RUVpjn0jv6jw_!h~e}7 zNN=wFYchXemAxRIcrnXdAb_N$>VmuBXsLe%G1|8fYjm_A%Ygfa*&4TP!g19QTvd*!l0X#F)2USj3|K{a!4l? z;}oGn%rNqhSB?ZW57D>h!FljFC;*3ris7?FH{}$u0}OxS%24kEo@Xpi%zz$4k3S*w zBCXv~58Y2b^A6alzh&}59xx4xbKg12@5j>9NJFFGuyr*VZeo*w_2-^du`atz4D%#^ z;Nli4qnckBO5urvOK$VI`}36d`A)7K3HZ64_ttW1!ld(DC0{wSjO!k1E?e=JCH&vE zFLzE$cP)SU3-T|EA$BJ1!iAK5{F+XJj83+tyy$k3|K#Hf*KGAOi`nzq{z!qLUHyg)D8D4v=eKvv1OT#dZOPcrsGHqdr z-9NhJ3A7eQuA{nipY6pj<(*dl)%HCSGWg@ZQey~C6Ddt$0fk0ZAkC)s9jJpJhJO~l zx!q#}Jo&bsDFzvh)Ss%)N!Kk)>#TR)+X6k``#R3^5F2U<;AqyFjtZF;W4I2~ckr^X z6~bFA=YiIJt1zzkXE6k-Q+9)FSR=2?RSOp&LYqYM4HT5B*$yldu=2A)&{l`Mxpmpa zV|tOI6MQoX%F-o0)E#8U%G;JxPTG8HaS@LfS#=hTy<&V>_X+EOQKwD2C!ha97m3tq z=v`m_+(MTXt_AkM3H6rKD2 z-KS_HOgJQ@cl#CcYs)^1mJ@cMT(M3>uAZO`PF8N#^>dy1GFt=|3(N9omAFH&IkQFP zYT)_7>mri^!%sVsE-|jd$SaN@Vih{?s#Cr_TlbVrsit+21oM*Y1ym4C>1-fx`qwb* zYuwoRHA3fpER^7%duN(m`6rjrh0tHk;eQV|dlV5*OyiJ*d+sUV$3zTICF~Q$Af~<+ zfvg|VM-F|Kihykkc{%%3GCQ4D0=gqmCdr?2&M4I z=Q>>Wu$zYzXa&sA#Pmi1(;HZx4GUCT`vP9}hz=}i+ByYp(x#8mlS%O6L*~kxE4@63 zN{IX{K<3tNatJj`Ye0xYShLKofO8#(!4}v|<7lF|BLR(nyb^9@Dq)rIXAWYkxgL-O zMta^rAC^2)Kt?l2_0QaP>#5;&YvTw=r>NJ5!<}qhft8VU-8l;xdPNkShW`ldr!&P_eT8zOcGc1$PXW7!_3SzCpL9srv@R-#<+E z06{QWCDRz!=Z5eIeaNwC69pC=00@`@n<}s{_6mn+-K$pl3ZYm84E=t{g{?4gMwTqGnmASu?@5Eqs_wN7c+L)dYv-LaL{mU%5}2$G;*Sn9V97oVi1j z+jFPP6UW?iV-3`-m=LN@%i?kCs#>zyb1$B!<7rv}9y4^(29?9$9nyYTQFZb0XPK+%2EPNJ#Dvf_+ zE|##$(Gw}WP^T=Pk$iXt5Y>~7)iIuQvRNY@*?r%u8s&ereKxNnHa_8auW#&LSgc8- z88O<=t?(}g9!+J$sA{#@BH}c>yWDxBicaDes1EYn(Yi(Hh{>}|-bPEybPxq>bG}`4 zzMi%_sSDh!t~#Z(qsa{BZsfOipMWpRT2d@IRp%Oe$WOD$@bG~r3Q>{Vt!!O#p1w=t zxtmWGscJ7XDML1Fe>f!{acVtn$geqzZ4mK~Ng)wKzrPt_0s_GpMIj@&tmo=#I|GSy z3P299@f4gQoi~Uz*jwC}Ls$CYX%&|(^Du*-wyB5_z;BJ3*{%3TEx+y;9-Tcd<0t09@q{MMmo1t3QWc0h5Na(gk0c zyjk^hV>Mijdhl*A`X?Z%404o7lh^_AgcB~cP^@vLY zu;ij}a0#!~K)rM2LQ@?<_KwtU4Tl+?z^HS1#9=qh4dp2Z!=|OiNpjvLMWO* zsQtk`+93pyiT;zFu~YX4?t{Uh;0YK{OicI792k%(TN6Mu6vQK98^8HS2nWL>;^lWg zCTRpxjs)>&2j@c~7XIhYFe?C43UmqN&y!g%=`jBdSgyFVZhIGlz(0~2GKXF50%7KD%urMGzu<*&i5 zrAKy{;k_=Kijd;Dw5W(7T>uCq5dI!8I5=1UYkp8cS`qrl0cd}0gP7xQ&|IeGv&|6Cp6|Pe$esf3AfN>cnuOhu0v6KPwrhi$K^w!^`BC3z-FET zL1}H#DDY9WA|5}kOz5GnZM;Anbm!;G_Hqy{WPYYv+@KRpdpH}NJ*!I|99(ZTUAuP7 za-dGIwml8e!F~<58FOQw%UyJU#ED}U0kj^yy~L@8wI!W?-qxd{&7b-d5kif#(9VoN z8htZclEFDy-n8gyp*H$d+wn}(H_HzNZoi`H!`U9iOI!IsAK@M+6+H2ww}tDs&1dRf zTjaLWOfjc*T-Y@AcpmfUz8j`SvD4VcV-C-^bQym)mGskCRBTv=k z)pK06PHxlgJ#0}9)_~01Yk+jB!TO>|$hZjT@%M!S_sxYt&4+T^UnTvjN7A6@0^2uk zld6cJv*WKV3ZG9_nw}&#AF%Ge_)7Jh?v4MDqJiIGbfT2zqnH+@xr<;J-_<8h(0l8x$b@r6V2lEIbmRH_j4@oO_BZ zSxCouN+B6*K+*}M46EO)?W@2ZrTroL(+YJS2yk+lUi6IqhGMi3Vcv zOBlx|0cpp7zB0vqARhfnh)gnywM>K%iu2H<)ezVSK{PUj@o2(M4y*_%41Au_kz)Kx ztbr1;)W`wROqzhR!n&%>j@Ss%3uqwX48l{&2Z)}P#tE+v;!y3|p2l%Kl0`f2SI&n^ zZv3fwhmT3jJ;zekHeIz)O90!RO@Tw@fyV*QXN0GzMY0FcnL zGM<^B$+b1=5El!btmGwU1l5%yS>#)ivXcaC*`%~1%g8sFV7T!-6dU)#c~HSDF^Ob3 z6^daPRJ2>%b3c_A6OtM)lcj~%Am&&nCZNGsx2e$lhhgTDSu~{}i6_Q6zT6z$hY`jvhiy*}o%r;Yzk`MPwk9dTS(oHssIa-Q1 z4}M@@QL4d07`6rXxOFvpT={ndW8ZEt#@I}Exk>9G6!viw-1s|;jskBK?QlIjIC&yE2m4mte6%-fBjmRBo;W z5wOmZPjeX7CyWS@DIQQWJJ>VH$DEn}+~vY%7cz>9%9rklb_I%ip~fO>;>AW!XdiuLDvBcIM&axwU0;zue4+gu5WDAtp8F z(xE3DBVw?;F-&MT8iA!3A+muqCk1g5)|Mjk3!dxKo+3Apf^DRMTZ-*bI@VW~yR_2| z;KC=6Z(^al&4NTv6FH1!PMUfF)ja0FS$~cI!6Xr}EwhcaKjLlFW$aC;2w&wdVa6{xbPf zDE-YjShxIJ*5uX9u;7(8H?^WkXQY~mTy2(g7}z?&RkFJ$J{swy5g6&@0i)HTh4QqY zobLE}Me{jtC`;g;%z95@p5VGHhOA-U@jV>a>D-6Rc}r<`EJ#2}ml6!h(cPEZAJW{$jo? zbXd**v7<5*SQbPxBA`Rd&z*MlQw1wZiixKQ3K7<1)#*jqGHV7J>+G-iYiWd}>`mY)>Wkw2vnG2=MXfJxQBh*Ag@8SSYFj*PrV=t$6eKG5Y$c6doP*0m)&;c74=X!T+e$>ysnzF$L z3%pnPTx{cFAYQXrSXxfq>=Y;_VuJ$d@K)QVAa)ebK#6=pN!W`OiIL1e^NVEoYiw=4 z6BVh#ZF3%<5=i(M&>B3K)8w-Ew2S!Lb8WqGMsDq%2ixx_RGp4KK+va1PvT%pMjsKt zQ55A7oLE!`84H2|bPpc^;1Tyf!bGySn;j^mjHA(1-#6DGzNLjXUmXv4Wdmo}%!2Sj z20&ZkZ`7p*Y{JU0(i>wRB+lNQgcCdnDORSx%#S5T9(m>z3R(GYra6O8wsbg0(=G7m z;=o+5LijDP-s31tfbQH@y1shigSKKu@~6(N{H7HD(=h_jJa%LEkE+w?zcYz>-Gb^q zgs^X5rLrIO7V)3yPJ2%r>^CxVb0d76Kc~7|HE9;yij>@ngorAIX+Vl$ zK&oy-dak0X`Mpvz?7Y5OItWD_Gp`!Bs7mxdR;oe%qpF(!tVl)m^`@j}ZEtoPACw?& zN-Jziizri=1gGi$M=fdjY91C3YmbXu3_m8#^nb)S&JyVm3SSa|#VN6r>;V?S0nqYB z{Zv{yhdVhv7q{?6V@2G5n4e8O&Pvk$bK0aa@Eu((E~6F^aL=>G!+8d+(q3&-1*V&+|Fw{XBo2^EyXDZM(|eA6h#2 zn-OY{NQT*ZxxFrlNlClOcU;88`V3#>dW}H#+iM+<+SyxRB#*6y$GP~p^D1|drkeNr zU@x#r#^dvsdDmF+To)BWaP~{^CwKX-YvL3P<(TpNC{XltOr&_+o>#Wbk1nk}$MYW( zr6;6B(G|1HNy($ui^B50!ivd(U(`G` z>J4Aql+S3>QdmBDQ>EhkD3*IAbMg46h?i5?V`8p83a>8rD6hNU{7|LQDE1xDSEvv9 z1-ks*LQ|qU%CyHeeMxmI@K5Sh!D@+SXm!`bv_xPo(yC~pwny>f9dMp7t!zSd;6A7_ z2Beg8x&_ucvpsY=m=N9&!`hQ_6Krpo7L#@t5x5`V|J+7Oz#ct6^Adxz;j)p=9_n{p zx6>I^2bSHSq*{4(^*bxWb-|9 z%C;QgTJ#IetHe-*?!BP&_gbY6EGnh9Tagp(Q(Mu6m@%UTY;M1%S1Ryxd3ZOuf6P9W zxv9j^wI(OuO10Oq+VV11I`tH)l2d`Ghm!BtVAiZs*dk(ot3-x`Nj~?WC<#bTm0BFE1VPT{W zfwWUuRZb5{8#prHmb-Ag9&tSFRv%``PDO2?#EJa99IX6Lwkk~J^q9Y@J@>7ox-csDv6#X$EwVOlEsxou^e>4`G@a9xBZJJ}$CSHL$~6a_*QwqIUTc2uxx2 zTbVZMjR90r_rl@%l>-GL2gd|Qq+8y(H+hR6?5>%06|%-Zbj~Ml*v0T4yEAbpgI@5t zedL)<_CmEq@}Tcyjk@bp0hOQtEiqZn2OvOCt-zH2RD^L*H0yO~HNn`sM{@;Y-Bh&B z&pO5+Op-D|eT+PF-;Tu8$?>)2Ra%dUw{rr6FwZ&@_~y+G2YCF}p}HZ&Wc7MiDirBm}!}!{fH1ziC(!zr2ll3L=tNVTt{^+ii{L+$ z;fpM~3yu>0$>R%DqR7{=>i1mzL<49?%n|JkX*ZuTp;8I39t)DhzR1&mpu4E<{A=u1 z2>FUD24RU6^$?#OeE^feY_GPQyLkya{_eaWJlbuW!b549wH;18YqQ*Or_L%dw=mj?{feRka3z?{} z+JQoYL7DtQ-;EGrM7#!#Tc@f}OS<6gjUgQGm0gjuChrHrDYrVgd4V8z?(vSL(Ea<4 zaui&!UOD^O&{1{3!3+g##15hmkSKWyirav0W`xrNWU zJv`}gxuX1Nr;`RcCX&ZYUFsmO)4!CszskkGO8TKmi7n|@+dleY8A*>56MSj+oS&%q zIaq2ab_K-B9T%ladtQ|)>2O?|dTW-uE{V76Jn;Uh@i((9KbgAtd+3s3Ny@@Omko8d zRaZa3nJ1XEC`kFa*e^oop9o1hYo80uaeBVRpN_9pw&ix`7SAy~x|eBoUU4(t%P2sG z!c#P;h20l&=T3-9!w=hOD)KbM-%E~q>wXm@l;iROa3MYFUqZlN;h_w(^8lhQV=;JV z^nJ{}=F~jold9>l0sueDTi9GHM%z*5j`)_FUyinHuzF zkF$ww53k*xhNl48yH{W65zU%Jd7fO-P*?weN^?5cC$4JIVPh93C&#P0b8?Hlp0;CT zStcR%SyBDZ-Jg{*>v90xjWU@RA3f;HQW3uZ`=4lkSRsWqJkPey0&t30#jANQW~i-N z(kV+G)@+@QUF6>WHH8lE4;Ajz--$x)C)$>&;bulM!%JoNHUW)T6Zu$ex8wefxT&gl zTNjgYI$?^SJkcfU{k%*4e>4Vm6&|leJYd#&$&#;%cAscrywT&-PyA<;svO*1F7t>P z>pb_5lmLD4CKZij>5AMVrN~mbyfEGdAE`(I8>K4eKDWy}YQ{SH4d#6%}6a~5-XtS@73T027(*L7W?Y-z5 z$P;A@zWboy(aAOHdSv{`b%BlKrvcG8NsaP(!#g(J3?~)f0x&0EM=ax=+I2kA3B?yK zkVcyQzUJxRe7k{Obeg2QY|2dih^8b_gyAro)@l}t zpOA!kk2QP^U&S|BGH-fq-(U`#!)18NS%N=EpE$?~z$#}t_Hs1cSO-Np*95PDkl_`H zbPn?O+bBbVhrj3aC2m4UcTaT?2YE5;n=ygPi=y1MCxmd26M%n_8-vg|p?o@!G=}9M zZzR^1+A)WP;WDgZRzIHp7kLW@`3QESi0*oTgsjM=|3yv!R(sR2fgEQrnxbADWVo{y zo*&e^!_9G-aw1TL&=i;aAh~d1dduDR?s&RuWTUifKI>*A9rOiMOF#Kr*9*LD$yDQp z*BnDRhDa;NbRa+H%)VW47zaDSSkDt@R!ax+!b7^#od}?XTasun z%$C^%v0{FHI#uU=&p?QG`02|uNxs^d1gGEPH(c#Dcy|tjRrBTf8}GwS;QQ>|O4hc# z9S}}h`Dqn_r&Djg-_~wLQ`%SItDRNVL1PmX6quMor*e5x3Zcy1>2(gVEO!Fco0Dh3 zd4(RuMr>X|I}n2HnEn8g?z3n*(5~@QWRo4UQWH0fMO+{xaeu%XX4BaX3{$43<{(@q z-Hth@jiS6TWU?%YHjSuhH;X7b6*{u5-R4O-@E&{0H=~h=7`;HC;yz%_@boG;r=Vje z97bRz(C4oHyDOa7XD|&8t47nYQ=EBNFJ^h_GacGEUBM7uk)(hBo1rIXf4_NbnNJ#r z;7ODDEdM+bQW)(JACY$n4qKG@mQ4?cZj{#Gv{av|^hv{(S-BIF@}H(u>NBPO2v-Mv z^?lX&pMnQT#5capD2L2fexq2QTDt2a5)y!-@N`5quIzyE|0i!{797>Mr=&Mq%0f63 zEp^VzqN}Fla2R-49F@ zWl*^=l)^;j?nmaEphgLIV$#4M%U`k?`L>(2tzcUPZ(gat5XqcV^2bRc$w+QLN`lrP z0!CaCWz6Yf!18z|l)x!?$%$A+-;QPa=Qiu|KWCog#8&e>h(P+~0I&}agBm)E)$C-c ze7Y9kK&D$DlGy*SGPsDaMt@2Nn|m;*cfy@W_5Q*c6GY9UX5t$?+9@J%{pdy_liY&C&h0F)t!yO}Yyakt*Xqhb5{qT;rPe)<2H}Hm(Rm{CD1!#r4M; zsE?4)P1YtRCOLSXIMLakk|2U6cq4nCaGq;O2USm=E zyQ>qM7!y?ISxwSZ)Rzd$LCCEW^RJ7;U~+1rJ|B2WvKF=)R7XV~j^qihfy_>*MNHro z3}l11u7#x~q~F13M@^*kt?*T~Mahr+Fn1utjEoJGqL01zZ0DmKHOaeVdcz=Q%d>hZ zvw10`mnz-Qmw8sE$5U>p6K#2EK(5zZ`UVndx{BG z-={k|+3OuymOfwvK66XqA7OQWv-vNn4@R>eD(+RT0wwEjNZIL@191?N&iNfT z{&h)!w*2yE;F`;+(XGJ9mJ3f-cfp zQuVfAO!;bxbWKBpm7+*xJG;>;tpxmoQCLIG6_?Y#jXWSHS5PHJSC-A{a_B3Wc?tb# z?~koL%}*+tA|2RFKlRa{#OxYga2Z@VmdFtO^?m3-~;? z#2$pIC<5McePQJ+-L#EvvvoIxi?sfBXY%N7wp&n-Utp6=CNq3hn+F{!EJfVJ1~B?&b=nnN`lkkjsK6t+`Bi-um|J z`k$b?OS8NEX-|IOEx<#ox@b zTov2f0WLTak|)8I8=PjzJmp;;S}2{ccHmYm z7c$7Vx6S`3o+e%M8QugIeLx*vT0u7ey4ySq6;q2v!P60mR*bW!B~{UH-{B%=kmYhkWr0*uNuF}9=DBQvAc62`mS-wM0RszE>OKIXs+^dHS;?GY zhaGhk=VPE+s=_g;9FsYd^EBOJD~KC4=vEtTkT++BZX~h@bJA(pYzqOFbon41XZ^?N zU6vLg2hKQ3Y8b2V8AwDPjYJAc9hQ6PM9{p(DKCb(m?>a5r;1cIXh{g6JOWYl;^D+Y zh^&KfspxQ)QNi*X$<%!y*+;hc)kLjcQj)x085dDKyJBADym;H z&$PivwsqLCWR0~AkqG95x6$>h1kxb)=Va{QUg!EP;s~;`mfc#M|d7AA-_+k!Mp2>8)dhgouun z@z@-M9;7I%^bNlXOi;~0CNV`3ZPqz$*x2h0mbbgSgQQd zDfo(f?Gqh|Ai6GZqn$3neuk5ZMd(h=Pbs-m&_n#ci8G{H5sTo!YRni$qG3^l71h~G zET(`^xdDQwF~$VJW^rEdFVZYlKrYRyZMhP?SS+%x_65Z(6)FU;Pi_tdfL+#l4*3V~ zg&xjb4G+Azhfgvmh4 zW2xteVqM@4FjTIZ+F{Ic`m~Hx)(1JrvB4FSXsVTN z|Cb`XiTC~-?2>&!ZVm?kc%D7*{3I@TNQ^Z|{?K~G$GtbRFavS&LWKn(YLaIUEgb9R zIk2Q)>g(CeP2JsTw=QSeKLtCxVs|HK0e|-*ft(0=#NGmX!JaUC0RdD-G8l<=i1sP* zl7h=l`ja@x9$sbHy5g@a%zaod?;1z6Bp<+2i zh?0n|YO4semrD)Zy^VY#!|Eu}hHkCWTP0kX5pQ0{If-Rw9F1her}H#c+|yqs={Vx| z$Z!F26mfb}cl87IbO+?2g0?vY1LtGFVw)52v?;GkbqpLBr$|7#Oc)_6Lo{DdkLQd&RQ{2SwlvgNd-CZj7RySZ}}>rdybK8=$2j$9SMqQA|Zq zc>8IIsWJ-Y2%gy(lr^sqXfgU{(L$aO3X1kXZ=T?I)DFJyz3RhO>Ev}iT?q8eL$F+e z(u!q>`|Ok+w;ROhyDYkMqa9UZNOq2DutR4xZbB5v3?yJ3B%7^C-wj2OZou}=(YV-b z)hjbhL%>5Sww~rVtV6nv*Ld!G7C?Yj5GCoOcHOSk)4uzWp0JA7rPs!j-w$XXxPsO0 zc|h-AqJk-B;DLay@YSQ>hJ7nL%`_mp3pBYPhR(LKR6fWyppntieqBb!qQbe+|c zmniT=odgiq8yBWTQBpB?p~DMFc^Yuj(GqKMOsDBaez>kOJ0{? z;T;y^T*Q0ea|%pg=s#JQQ1DZAH87gv#QGmX{$q26(aJ|3RU9TT%*F%9#(p~P`@dN%H(SZs&OQXC5 zX0FymXbxHcSaM31mL$*VG}2{=C{yt2YUlz0udfbgbpKbQlR}d2JNxFCri`t_!qoa@ zwoa)X@&m|~>wP(ZAl7SowIP6_aKpkJ^j zf#~3ZuKf}JfPzBYWpSS1bzG5(gty_gb`}G&b}#{*&jb39$1@yO*?c&Gk0pIwK@x;= zKE31l;?5lKNl@YpY+qnd1FY>>6#D2)0=HCXI!YvZ)e?bOylgM&4=j-UjSA==EI^G5 z=(j9L`3QQxVF8vS;Ojf~)IUEvg~N{~AlUtiV%s#a^-b@m)5{+&+5ylE$3uyI-(J4% z_j(uCy;tpEielx+>uTh@cl>?NjsPw98te7t>yxkg*T=7}+kqyv04-Cik#8?yiW!(@ zl^Kwy+HNj2Jy(}!xUivVmT4(ly4=|7tJTPShW)3nj=#LMYbSX;UM+ood3;)l!D$xb z8oRHWk9&G(Nk6Y@*Y{=0@e{K$q<@wWdqVN)o)z|w^?tJ(@6@}^wtZReHk2`y-&xBa#4aaR^v z&%z%d#G5HUfE)Q<6ynZLhRfSLli4@qK>|{{_Pv=J5V(Lhka!F4!@A3D4J%Ti9uXWN zGzFoi1?}b^lmCaA63=TlKbZ2=74?A7=QKhnpe1ZBtoU45Jj2If%0oa)D15~EnYEwL zHz(3$UPv|0f0^pT`g|brUoS^In~F+QTmEu@yEePvYPC?fQiXB@!HyO0;-tC~wO5@` zSr%4?8weqK8&$U9_O9bQqnE8)DU&$L(tVtPyE~H!e-kz!GZ*Wj`mA(~u8g~P2pV?; zn+y;&RU|d-Hg`$iMVKYf_2i5LgLp4Q%&86VNyeBWm4x1Ca!BZXCX!Y~%M3e$$s^VM zq5VgvY6F6nRAV)nEH@lQ*2x`>!mkK@=7{neq4j9t?V#!L=4=irzOLCmU!*U_o@P() zOsb3%tf^kb(hw@`Uto#!HmcQy!gH$VYhV}fxLcq8gP1kk4Cs1jd{>0^*yYsdh<$iK zRtmgd2kRUhxf3cnJ3O`?9r%6_h-mQpJ}JI+IFoYubAzAGBq*m%TAOY-d30+tS*@nj z=J1R$Zw0NRo6wi}Z{rGPGNQNeWmen_kLqvWJ-svHv@%0M>EOE-C8_cUn?>ngGdE=g z2F7>R(vR=k+p)S)Q3laN$T-z~kp;lQj$e)&bfuRE)h9wy4gY>@`-@aKvsEj{nu2jj<)L;EgdY-k!H=_-ab6yjYBSAccp)vX!~vcqQ&^C zyO9BR2Hz}IwODE_@v!XIH?4c&K)5Ay<>nsZ3OlMj-Mtq1<-0L0LJJQ+;0X^kqURwRvlV58m>U4 zZU(fPZ3$fT5^tufq2P*XcKWNsJ?r}nM)|A(wvk&3@-~r6d z77IApWbl;}$cnvCgO^FfskF|+3KnL&powi*)-F5PEn(I*q4kHtL4n zfPK1p=Ek2JTr3)K(h+I@hiRIpvNr&Jv;Ip5{Q%!Wb1I1QqJgQ&RN%Uz%_Hp#(U8iTWUg1BoKT+#QP8MK4<+$I_@d)qafq7NBAh#o+6?#vQk>i;-!3xP&=3U;udcK9yKw-InfrU=@YM2DB;UPw#fLyY9tg#XVSN#qZ!D`yLIvP8G&wE8jmijZIQr-(J^6pB zD^mk)QEh;3eW44`c|!sEyLtgSS2R_PTLp5nm-Tk0R}w3IQuT)*enx^TH>V(lB4}jz zx>pAT3AydCpP%BgbQFV~b2?;Dg;b5R-Q z1;Z>&<5|H2I#If;Tz1Trt}9Oz9Vw>K4b^}qianW&{P>U6dn55T_(KgAyuGu_BawiI9ng)SmEVDQGj<@zIE$12 zn4~^hoB)fXUqc5G)nV~@W%RtJVVF@9g4k#sflC);*T1b!XDb$?R8(9L;njp8y;j}_r(+>oyG@GV?TJd_s&CDkT zL7m|nTN$C{7fnlou%8glC#aevyd=IHbW?v6Ls-jtccb^TI=X;f@vFt_!^!+xf|{%H zgdogJRx}2QbIG2RfxFo!jTr%rH>=0Q~KzS?7;0?YfwZDQd;lv#- zzV%!ICF+WtUQ*pS9kI8&S+%Jf*-9vo?0ib25F%jZI}NhK+c` zI*`Y2Bpux;1K5Y?LtQ#}5o&mZU5j;$AO&++n*;vKHOh6a7C-~hexw>lB%slY9q)j& z4(+AENpn^>H6(Of|1SAf8QHW-*6_#-Hv+X>gzfogDB@I0Pd)%^ZXVl);$J$Mvj7=| zY9e6yvoQ$>wztdnGIJDf7Aw>Nj9Y(!cLH0lfHD_9r}rNq9&cx$Lg@PVXcV=Cu=4S! zWuZ|^d?#g%4&ccD9?Tbvky!@-ViVEPDHBjdYzG|TEvg8+}7XotE;C43vx4S2^P6mSH#Oc+^$?Ev2gz=96 z#C3tTuRWj*i@H$V*AS}vYJj*FfBQ-xbGGL21!-e2+G-b!;Tk--?^iG9rV>I3W#*#N z`0W_SK3YLNE`P+PP1pYwGO0UrY_lO<^DnN6`ekfCq)AkWUU7!P5 z&5vlOcYe`4Vq6J-id<4jS$(qAFI zU}QHeQRyb2Yhe7FGzL4FTz2(S~l62Ov@c!#%fJH779R zd;K=tVNoA7Aizs=vmS{+Aq9MbM~~|zc~&QFKD$Hp0V|vJ0h8zLz{vq8Mw&1@>4(*Uy4+M>?3)sW~vuci&m-7?ditN5}T* z;729tdvI=7^AKjT0n70?WfJ-G#KTCcl@L`M7CQD0B;YQH0N%VQs9brm(*nO*t(AAE zjta2d3Nfc}RG&+@%ocg=+};IXl>HYQNc-Dh^p z7`^2(cPKhE=Z54m+7+I$78&uCBw7-2)i+a1FlSVkWkS5-vt(`FN!P3e)b{gPFdaL* zPXsSR8Hn#w+3I9Y{68SXCb9>TNA`zr%0dYsqC}+3m0yP!MfO8>%O0w-=W;sG*9)n0 zHTf(|TtX-3qPbG+jU}-ssin_7rNz@s%wdyj3>Cb5sDuupZv*Eg+*7kP$qH-3nD&v2 zpoi3}vfr^<{Z#nd)YfLi;Fb%0<)gpuKj(%g^rr2x35p*i2rhOD5bdeHHiB%0ZLVXW35tYF4r_rhk(I!wNm9C~~DXy=rJ>AmWhLrDHJ5`-^^IDW?QMh6WcCZ-qv{=1ro$K{bg4(qvxVMv^mVNAHJ$Bk?TgUwZ*1_) zYNe^!eYZnR)U%vJM8)mkN<3&{cAb(bNWFVN;smVI<}Ho*Q69D! z;xm?2&TpkBjyu*^0jTMT<;h9+@xPPNSz4ox+R(F6RA}g0@z)quwh<~TxU<5b~Hyf zxC-L1jvV19C*5K&^0%Q<@O1bvug*;r$}nNP01LmpP&bVYu0MXtbzq$Xg6YD>h|;`p z3~zGHvBuEYR@fcLxGvBBo<-%LbS<5N8*6<{+h1l>sx7$_g5!Wc18^CUo9IiQN4(@? z^eV~%N`35I_2SM!@>QJT?_5r9#!N4I9lfMq^tjLbafv}1#;p+~BgQk8zWoLoy7FQB z4R(DO$Q`rayp&l28W)8Ba4D4W#ykn3EH_f{QfO1nOEA?G0`d&!vyXr`BINGn>r#x4 zKM?#;iMgsyYa-hgk(tYM;-0ZD_lUW}eT$F~J&TYKo<&HAz#=4sP6~Mgiqvq0E_`_9 z(T$hT@j}$a58bs{(HoEM+<5fJ*ok&bQcu;uvbJqvZAD%qm5~70);7{7?{eO6gnb3g z!ro%WD-aMg(Rf;ezkW*6rWEZypG00S7f`w1R@${o_^d6fFdr0u zFKo)?rLY#RUF4YVkYMF-MW}<|o)em@R4v-7-n3QbYddwHu%F%Hn|KHB&G`ZITbqpF zv8a!z68Kq5AjfxXmT=4->J@wuq>ZI=HkQq^MCotsUEz1z7c3Qn^d!iP-CS1}fk5_Z z<@whadu6o{7fmlJo&+lgyz<+HeTG2-*~Hjqsj_j2 za{B9>v)?O=0{zw2QMGK9>xIxrZ-{7u`$g3Gv1I=0Km>P84-Zzc%`k=#(<4jyZVYL^ zFn{-9SdN@_lcG zH>|)nf*sBq)=x9<-quJ^guN%MaY-bQwQM~ROeu5=cLQh$?3`dpI#KjUKJ9Vauy1YI z@4So{2g6Omc(JY(4jj8!BO#lzn5X>h?&3F^W0Uc<{_IOZW9K~vCHN0oQ4GjQW?-S>CJrgs z*u-0IUbu6a;U98=rO!9(9II0TJ)zAf{+>JD=1>UQitrMx2$iuFc)L(! z+u`Ma49C#>?d@1i`+sS&EK|>r`|1v)%X)M7-sLN+m4^W#qAI}06M-Oa8g$T!g=OJd zuwQ|W>9;YQ6{18&3m;*JHW!VpNpBx;G^qhZ=~e|w);mBRG$T9)KAiXl%We&a-FEZ% zf@L5g8WL#Nq68V8iK|#<)9Ho!$6Z1J;Z>J0McBFMoAd>#=fO!#Mm;s1{PByC`=z7( zS${1eyJZ#G3m0ZDtdw+lG>Tajq`k%QR(z3=U*)$Uq$nshqtG)8*6+I!!=eW&_nDb=35)zH~s zt$ac4J*TJ=%~EqmUdtuzBQwyC+S%@Pq1Ox0ZMAEp)zd21$?I4O`}@n4Q;R_c&RpoRM;s<&Mjts-+i z9Z)*qcE1M;||kT5v@{Z*m^E%>r}9Y zdper_x+7qpk2)x!h$JUpTEg9|J{Fr#p?WP{r(XX_R0d9>T%JT8-b1b2522N>Iw?9~ zpW5}Ug}Vewj>YLabH&S(`7@45^XV)fWRpp0lg2K_^$}JgNaF*q1k(8J6uG)ffy)3i z02Z$gmk(i?L54jFE`!ZXKphE+-+qog)k?-)5euh8zpPiUWB3XvoZz7(#iGl<2qyeI<@Fy>yoO??O(+U zFVE)n7F<4--^guW210#^18Dmwl2bGP&WNIAxjE=Ojdj+SPi-F#)WCWq%<7n(Xx5c? zIW(@C*<)s94UZowJs8f5e!~=Ix2zDA86`6~E^4}hz7X2nqtl8yJ9%@In8qqsc3zc> zx|GGNu#IGKpMA(5q?-RFlU(ywvkF*`p?RyVNLToM@s(64%zb`OdzuiWTto%*2LB6S zVai&3KE;cia#f1Lqe?NNb!~w@B|b$bK6-hhN55IRPPz6?V>Wh4E9Rzrj!jzPx@g=` z1~tZ*HrhDcaln&Fz{Ylzs>rB{GiK{X1mtWZ{KDL7Yd&DyI5DikxPgl4te|ZLks(Hj z(r!pSAE-oI2jh)`H|b#Lq8SC@b3g~aY96*%e8gbMF$yYgM&9U%7#`^FFP#%pS zs2pc(oW6Lwa`w%=)%=WP@Rhh{niiCKiJ2qdWkTtw^M=m0FZj<0f5H2K_;W?^FCa$z zLvQf+dc=S4^WSgz?|1z7d;a?a|NRkvbN+MF@0`j`WrNbxuH4-zUw3C@z&{XG5BblC zS3l-IH!#)wVVE)g_x3{k;aOoWVJz|Kmj7VZDcPdw>Ys711OIcYx!&X^%LipKd07r8 zB^^w_#THFElhZ%H>a6y@Jo^SRp5nvgUXrZ#b8>_Q;J(2k@|&zrU#!B@#m#Wi&tFbn z4QU^K`ja8+Ps;q|%gI}MF-#^HHof3EZ!pKr@cRjDPE5Q>CfV&(t`ffkc*fHTaMuQV z^CBD5AHOFU<9%)Ar{&eNOeq@jXT;^ho^~gykB*Z z$D`KDd&Ks^{!^gIP;-mZW{90cD0-+jLLJG~>+%MTk%Tn>Ub$PRFied7+(Gp#5X%Bq8(vx<1dI%ru!B2?|o*)5F(PIY>a zDV8DhZ-lAv!6gu@<5TFx*<^xC?=Iv`o`)a~uVqC#s zc_-Q67H=0A4Pc8xvI{<;`njK z@-n~Uxa=CRIx-p~qQMUt;I!9(y5aqX^Ma}oC+W=)IXc>6oFH0sZOg01>XI#NKDna6 z9XICff{25RxIrs*DVKM?K$zOlL?>2jY&M{ms_}H>w%k=D$7W_ECJZu~v9dx4Xwk#qDkO_eu^T4_~~qxU<=P=^ZKBg2l`#S1<)$M9(X=p z;VWBakiTE66(EDBwm5a9SB_c_YhwPF?(2z%XEV{V+sfU7o0iM&f(Dx#5x6ZMKKx^ zf@^Lr7n;hZlV4G-GGn(WUElO;Jmz?`{1b-*vV9Br8!#No9bwI76z57AVM)F7{_#kF zX%tz}+m6JF1}nah4~&|EW`jN_biz_Ap&q9exld`5Vjry0b$ciEe;PC1pAw~oDPc3S z*8t{YC)$afF#(Vk&bijFcohY~u#ni<0w#GEq)C@y#4vv3)eES9LyW6Pn36usrFNY_ zf)x-3sjN2H?Mop7^TP?lF8-F6*BjNuIO#zG--aCFL&+Tg@XeWuLrO)SwCX15AMfLP$NQUeYepj)xWtc~E)+)|2L!HLtdI!oH`WZyXfJZ0y1O>2du zTa>_&ZCjfLn2m#+Kl$v6i_Lmf17W(yLC#he(Acxv12*-3llmsSU?7g-n?eSzLUUbt zRA?BaIv%YZj#la+L`emaN+kMpKrw#SE&Rh8T7Zlt1EVF&7)!@2rBWtxrx3x$1+qvN zTL89L327MUrZx>)&gp0k=+LI;K-;scXV)3r>ka|g+xbbJ8p}rI-V~XB)m&{jWa(%- zAQQAEf;T4c9YjE-}WR@hI+ia?GDkCjeXF^Eo`k1&Hx$76*5CF1X(NqpjQ{~ zb$-mJN4L6u+M2?{{ZLc{4ew|oqkDNx%}oiK*6Mko#sN4f>>=!U^P9*{ylZg3Q~_}i zjO{Z}=_pb(bYLBmg39B}%R=Jk@S!1$qACky(N%#s1QnJFqhf)kuz^Jz+7pVl^SSl0 z(P%*`M)TPM7V;PL8iI5eBWvs(I(SMrxz_n0>+AeR=eMAD4CqP0wz%w}Q$68Lq)c5c z;z+#^P$f=9$rSXN&bI~7K5})iXrYtU@!aT}Kc6Ntz?T>3I2)T?K%_9i&Biy{{NW#mRtKez|7nbKMbbV^6HqYX>QDNZ8pc!J= z5acN71(A0$_e4q&gLa}r4Vgh58pEgTvrzm2{+Sk!nvL^s*!O$rtJ7cT8Qem=N&DfX zzeJLMc`u~s#JGQ`k5^~vdajLAzayoOa*||GS3W_HVyl{MQ;N7__J)|ebsBfZ9`8PcWVyIF>jyjoOXg-x&rhn{}DeRP~!6%dJlF25K z-EFu8k{9b+%{1*Eus({Xjyes#Aa$w?IDCT!VvrZj~f%F%Hx@>V z@r*BIp2>ET=P5GjsAQaCxo`hpQw&4o%&~}e5#Z=gCi70)YpXfk@8T(XE+06?=5UW4kTgw3llZrr8??!6cj zMZ^n|%8QBA5_&>)bW<2M6LYAvZjf-}D{>a=lQGP`^fJHNN`tAZFVeIv_yPnWE;}m3 zQulG-E38@3Jexf^*S#AD$SZx|g99k4PE)*F(UnF>u6Qop0eObv`$&XFjL)CY2Y7GK zXDOmDtcJczvBO?{b6nY|YQD58WvkT?N-V*?8b$QnuZX@IR7C&On&>8aQvUf=&iAO! zi2|@twhNO@(F&ogyCy4MU4k_qntWQo9Ll>*7x75=TByU)xKT1rrDKHHM{=qfv0fc>&@0g zgVXsBr$;#J*1veCby4!n~>R6!%oEW)26pd z#yR$s;Yjk@gLB)+j**xE`5)EMm0&3Ulhc%sEShp{y_u@~+QUGhpPJ2~sCxU_`}q2)+AjM642}KQBzmkeN$fnr!Oo ztuo6cs$3Pw9t7qGU%6f$VA*k(rORjmcD`peCTFYf>+QcFCy>d zt1Nj3Urlxi-9o4doOz^QO_QEkFcr%10-$0vBeKg(h{%X+Gq3Kq!#nKy9ovd-s#7uA zJlKAY+&b;vz9$U8I^~?92Ngr#4*ek}nx7$W7!tqBq2!&}$wy4qqvU_5XA*#!48X9` z5NxTUR{ff!Yk))TfiO~bBRam0#jDC;RLq%2#aw+9 z6q)+wDfL0yVyb!f;%o$(PZ;Y+7 z6com2UVjGm!bv(2F-9&M zE{_O1Q_-EcwsA7crVdia#i+E50!=CqoGTQ5rhbMk)l15o9JGd0Y_@BFFn1A&L>fMP zGR*q)`GGjYJ?X~vrm5Wwcw+-xFH@7repX(?_I_t;y;+$^NLkLxmDu_y0=VdyRDCG% z61>9@!W9xbKtuFF;9g~Xcy`$rB8&#nC`�m+1;Zogok?%-tU1YE(PK*m_zjlxS_f zQ|nDfSUKxD`wKG@waph4vWs}KS7-?Hp%%lE-9yj*voi8D_1T7|P|oxzTV<{1Y{?EdRD zXV9%y;vtcHu&NPWmV~jyN-|j48Ton{lhA#HhJ8I1(Sqha+{K+6tB3!(%n14WEa}Be z>#A%qssHqhRX9B7463tUlTK^{h|)YB$3vEI&m}P@LOHdWH0FKJDq6R1Y99noM#-~9 zB2ryrNImk|G|>_?OI-Ysgwad_!#KUv#v9mvLs2w*CCLNv&6KcD8kujvY7T{1ATl3{ z1QSyG_Fs<5H;&oo<9ggO&LLnZ2C>}k!aA}pxx`T~Sjd?f6YMgD`6ysM;~0z&Pnzw)boK#wAdJs(ZHM87+CZ*BikW1##_O+D6@-*YzU6UTSlG;js=3r zK}|BS!5ql|fQ)1SUoFW10zx5qHfmgk*Htb9MktKP(+Zbi51v&_wurpa@{Y$^-f?L2 z2;TX*4-XNn3O$CG2dzQgdeG9a-&j2e5020oY1zPGJ{Y)AtO;2ke|eb^`@Fig)k{_i_-Ey8iN(WOc_d5`HBIG4aTy4$GGUA2wzm~ z53!?E*;hozdw7=@y%27=qJtW!cg#8uSvOB(wBfnQUegV9q=72t^6(r%Q%`e4vGExg zYlEKyCZbi&3=y7?tnz_30#80wkxKe;ZVtn7LV|%)vV<&UUPBtt@E->(YzkzIhpzMm z;5e6{GIbMaxas;Hx5oo9MZJTpIvlh<98Y}XVU2`{aqbxgG$t#1^=6amIwpgp!vl{j zewBagy{iXkN>tloRr()RZoCBR4wvGRT6fY>+(8Y*%^(cj1o})JvkH${!gsYY5yah$-aED>5+ke4`Vq^eJT2@CN!+75#*f zUqbZhuM9PCvN5`m5~QkpI}ya{CL_3NT(qA2o66QMuLCoZP@gZaIcp?m>9)FA0=N(=k|Bj&bmEd32U%gjHltxXBd zow$%NN`eSwdDy#=LsB-0sazK&i#r9vfeFMb36eK=f-+F`yH@PhcQsu2yF40f|3W&q zdrSWrk!Ps1M}qepZX{Z|>n5rRgBB`{c+El#H{c#NAlxHpU}z*HrT^JNz|p`#&g4N3 z9c&6KXjS7o&>rD|7SC7<(u7_SdU#-|aGQ;`3Of7L9znjFl4M~oM8Pf5+{K1P0D1f| z0`Hg;BQQ*}ZdywzEJm6;&|8^1t&;qV8FC(<4ZyFg*#;nBUfGN|3g!$Gp*e96A-OF@ zG)+><<>9Y}Snwc*xEIC{>OwRZ7I^gE-sPX3Qrq-XYJN}Z2~l7Dd-NN&<{!{-3n@FX zd6}MNZfHYA2c^uN9ikE~YK`R@-axIXqMtM3DSc}8R&d<=%{~)<&fa7oq05J*@|rkq ziJIE-Z|epE&9pAiH0N?~3-r>FU6kUgH%l1*7BCk` z{MUb)Ot_gkm!kayPYA`dZ_n26PbS~5^Q20@=Im2ew$RC0sXF!Me*35P{no`T!pCFS zHW@fQ`@X%_UIQ|m2RJ(}^FewJ4;uqID$^l6ZQhn;!P2MjxRs5#0(3ek@eRY64*`!| z;M=xjHKsq~n+O>9hQi}hp#q+c8N57Gs%z;sz8)dX&mWMIgmr?CLKPkj2BZeGm}89C z4{dsn-mK5h=@_cGMOG(l^ua9>tK8T87ptpZwm_|WbV>I?;TksU@!?*2p+>5Glf(Rs PQH=ICC(K^x*x~>H7Qygp literal 14057 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 ZrYGdhIZj9tpCq%q??3T#s~rCU006a?l|cXi literal 342 zcmV-c0jd5UiwFp9BOY7=0CHt>aBpsNX?A6FWo>Y5VRU6KXmo9C0Bw?AOT#b}#oqdqEGH&flIFNGo`ng#Dtd`Jn z-&MCaWxSz7)5wfWmL`NM+z?LL6C>0uI!`RKk+