Correlate Exchange IIS failures with security accounts
This commit is contained in:
@@ -42,6 +42,7 @@ internal sealed class AttackScanner
|
|||||||
ScanExchangeIisLogons(attacks, errors, since, configuration);
|
ScanExchangeIisLogons(attacks, errors, since, configuration);
|
||||||
ScanIisFtpLogs(attacks, errors, since, configuration);
|
ScanIisFtpLogs(attacks, errors, since, configuration);
|
||||||
ScanFileZillaLogs(attacks, errors, since, configuration);
|
ScanFileZillaLogs(attacks, errors, since, configuration);
|
||||||
|
attacks = ExchangeIisAccountCorrelator.Enrich(attacks);
|
||||||
RansomwareBetaSummary ransomwareBeta = RansomwareBetaDetector.Scan(configuration, errors);
|
RansomwareBetaSummary ransomwareBeta = RansomwareBetaDetector.Scan(configuration, errors);
|
||||||
|
|
||||||
if (configuration.ExcludedIps.Count > 0)
|
if (configuration.ExcludedIps.Count > 0)
|
||||||
@@ -181,6 +182,7 @@ internal sealed class AttackScanner
|
|||||||
Target = "Windows login",
|
Target = "Windows login",
|
||||||
Username = ReadProperty(eventRecord, 5, "[unknown]"),
|
Username = ReadProperty(eventRecord, 5, "[unknown]"),
|
||||||
Source = "Security",
|
Source = "Security",
|
||||||
|
Service = ReadProperty(eventRecord, 10) == "3" ? "Network" : string.Empty,
|
||||||
InstanceId = 4625
|
InstanceId = 4625
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
53
src/OCSentinelCli/ExchangeIisAccountCorrelator.cs
Normal file
53
src/OCSentinelCli/ExchangeIisAccountCorrelator.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace OCSentinelCli;
|
||||||
|
|
||||||
|
internal static class ExchangeIisAccountCorrelator
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan CorrelationWindow = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
internal static List<AttackEvent> Enrich(IReadOnlyList<AttackEvent> attacks)
|
||||||
|
{
|
||||||
|
List<AttackEvent> securityFailures = attacks
|
||||||
|
.Where(IsNetworkSecurityFailure)
|
||||||
|
.Where(HasRecordedAccount)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return attacks.Select(attack => IsUnresolvedExchangeIisFailure(attack)
|
||||||
|
? EnrichFromSecurityFailure(attack, securityFailures)
|
||||||
|
: attack).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttackEvent EnrichFromSecurityFailure(AttackEvent iisFailure, IReadOnlyList<AttackEvent> securityFailures)
|
||||||
|
{
|
||||||
|
List<string> accounts = securityFailures
|
||||||
|
.Where(failure => string.Equals(failure.SourceIp, iisFailure.SourceIp, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(failure => (failure.Timestamp - iisFailure.Timestamp).Duration() <= CorrelationWindow)
|
||||||
|
.Select(failure => failure.Username)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// A shared NAT address can produce concurrent failures; do not guess between accounts.
|
||||||
|
return accounts.Count == 1 ? iisFailure with { Username = accounts[0] } : iisFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnresolvedExchangeIisFailure(AttackEvent attack)
|
||||||
|
{
|
||||||
|
return string.Equals(attack.Source, "IIS W3C", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (string.Equals(attack.Username, "[not logged]", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(attack.Username, "[unknown]", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNetworkSecurityFailure(AttackEvent attack)
|
||||||
|
{
|
||||||
|
return attack.InstanceId == 4625
|
||||||
|
&& string.Equals(attack.Source, "Security", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(attack.Service, "Network", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(attack.Target, "Windows login", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasRecordedAccount(AttackEvent attack)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(attack.Username)
|
||||||
|
&& !string.Equals(attack.Username, "[unknown]", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(attack.Username, "[not logged]", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using OCSentinelCli;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OCSentinelCli.Tests;
|
||||||
|
|
||||||
|
public sealed class ExchangeIisAccountCorrelatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EnrichesAnIisFailureFromOneMatchingSecurityFailure()
|
||||||
|
{
|
||||||
|
DateTimeOffset timestamp = new(2026, 8, 5, 4, 0, 0, TimeSpan.Zero);
|
||||||
|
List<AttackEvent> events =
|
||||||
|
[
|
||||||
|
IisFailure(timestamp),
|
||||||
|
SecurityFailure(timestamp.AddSeconds(20), "user@example.test")
|
||||||
|
];
|
||||||
|
|
||||||
|
AttackEvent enriched = Assert.Single(ExchangeIisAccountCorrelator.Enrich(events), attack => attack.Source == "IIS W3C");
|
||||||
|
|
||||||
|
Assert.Equal("user@example.test", enriched.Username);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KeepsIisAccountUnresolvedWhenMultipleAccountsMatch()
|
||||||
|
{
|
||||||
|
DateTimeOffset timestamp = new(2026, 8, 5, 4, 0, 0, TimeSpan.Zero);
|
||||||
|
List<AttackEvent> events =
|
||||||
|
[
|
||||||
|
IisFailure(timestamp),
|
||||||
|
SecurityFailure(timestamp.AddSeconds(20), "first@example.test"),
|
||||||
|
SecurityFailure(timestamp.AddSeconds(30), "second@example.test")
|
||||||
|
];
|
||||||
|
|
||||||
|
AttackEvent unresolved = Assert.Single(ExchangeIisAccountCorrelator.Enrich(events), attack => attack.Source == "IIS W3C");
|
||||||
|
|
||||||
|
Assert.Equal("[not logged]", unresolved.Username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttackEvent IisFailure(DateTimeOffset timestamp) => new()
|
||||||
|
{
|
||||||
|
Timestamp = timestamp,
|
||||||
|
SourceIp = "198.51.100.8",
|
||||||
|
Username = "[not logged]",
|
||||||
|
Source = "IIS W3C",
|
||||||
|
Target = "Exchange ActiveSync login"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static AttackEvent SecurityFailure(DateTimeOffset timestamp, string username) => new()
|
||||||
|
{
|
||||||
|
Timestamp = timestamp,
|
||||||
|
SourceIp = "198.51.100.8",
|
||||||
|
Username = username,
|
||||||
|
Source = "Security",
|
||||||
|
Service = "Network",
|
||||||
|
Target = "Windows login",
|
||||||
|
InstanceId = 4625
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user