Most if not every environment has high-value accounts. These could be accounts with high privileges needed to perform administrative functions or accounts with access to sensitive data – for example high-level employees such as CEO or CIO. These high-value accounts are often the focus of attacks due to the value of the information or assets that they can access. Increased security monitoring for these accounts is therefore desirable and security teams may be more tolerant of greater noise given the high impact of compromise.
This post illustrates putting privileged account monitoring in place using Azure Sentinel. This example will focus on monitoring rare activity from high-value accounts and providing a quick view into related activity by these accounts across network and cloud logs. This provides a security team with the ability to get some sense of context surrounding the unusual activity and so rapidly understand the who, what, where and when of the activity, allowing quick determination of whether further investigation is needed.
A security team can provide their own custom list of privileged accounts. However knowing which accounts are privileged is not always straightforward: so we also show an example of augmenting that custom list with additional privileged accounts inferred from the log data itself. For example, accounts recently added to well-known privileged groups can be inferred as high-value accounts that should be added to the custom list. The specific query discussed here is posted on the Azure Sentinel Github. However, the same concepts are applicable to monitoring other entities (e.g. high-value hosts or sensitive files rather than accounts) assuming that you have corresponding logs and that these are connected to Azure Sentinel.
High-value account monitoring details
The Tracking Privileged Account query discussed in this section makes use of the following logs that include Account entity types: AWSCloudTrail, SigninLogs, SecurityEvent, OfficeActivity, W3CIISLog, SecurityAlert. It is provided as a starting point and modifying it for your environment is encouraged! Check the Azure Sentinel Github for the latest version.
Note: In order for data to be present in these data tables, the corresponding connector must be enabled.
Building the privileged account list
The datatable construct allows building your own custom list of privileged accounts, made up in this case of Account and Domain values:
let List = datatable(Account:string, Domain:string) ["john", "johnsdomain.com", "greg", "gregsdomain.net", "larry", "Domain"];
As discussed above, we can infer additional high-value user accounts from membership addition to well-known privileged groups. The query snippet below shows identifying such accounts form the SecurityEvents table using a regex pattern match for well known SIDs for Active Directory.
let WellKnownLocalSID = "S-1-5-32-5[0-9][0-9]"; let WellKnownGroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103"; let InferredPrivilegedAccounts = SecurityEvent | where TimeGenerated > ago(8d) | where EventID in ("4728", "4732", "4756") | where AccountType == "User" and MemberName == "-" // Exclude Remote Desktop Users group: S-1-5-32-555 | where TargetSid !in ("S-1-5-32-555") // 4728 - A member was added to a security-enabled global group // 4732 - A member was added to a security-enabled local group // 4756 - A member was added to a security-enabled universal group | where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID | project DomainSlashAccount = tolower(SubjectAccount), AccountAtDomain = tolower(strcat(SubjectUserName,"@",SubjectDomainName)), AccountNameOnly = tolower(SubjectUserName);
The account name can be recorded in varying forms in different logs, for example ‘email@example.com’ or ‘domain\account’. So now that we have our list of user accounts defined and stored in the list, we format them multiple ways – here 3 formatted versions for each of our individual Account and Domain properties: AccountAtDomain, (i.e. firstname.lastname@example.org); AccountNameOnly, (i.e. testaccount); AccountSlashDomain, (i.e. contoso\testaccount).
let AccountFormatCustomList = CustomAccountList | extend AccountAtDomain = tolower(strcat(Account,"@",Domain)), AccountNameOnly = tolower(Account), DomainSlashAccount = tolower(strcat(NtDomain,"\\",Account));
Finding unusual privileged account activity
If an account in our list performs a new logon attempt to a system or performs a new type of service activity not seen in the prior week, the first part of our hunting query returns a summary of the new activity and the timeframe it occurred in.
First we normalize each dataset to common column names such as ‘AccountName’ in the snippet below:
<snip> (OfficeActivity | where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime | extend DataType = "OfficeActivity", AccountName = tolower(UserId) | project-rename EventType = Operation, ServiceOrSystem = OfficeWorkload | join kind=inner (AccountFormat | project AccountNameOnly, AccountName = AccountAtDomain, DomainSlashAccount) on AccountName), (W3CIISLog | where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime | extend DataType = "W3CIISLog", AccountName = tolower(csUserName) | where csUserName != "-" and isnotempty(csUserName) | project-rename EventType = csMethod, ServiceOrSystem = sSiteName, ClientIP = cIP | join kind=inner (AccountFormat | project AccountNameOnly, AccountName = AccountAtDomain, DomainSlashAccount) on AccountName), <snip>
Having collected this information in a standardized way across multiple datasets we can look for just new types of activity today that hadn’t occurred in the prior week:
<snip> // Find new activity today versus prior week let ActivityLastDay = activity(LastDay, now()) | summarize RareActivityStartTimeUtc = min(TimeGenerated), RareActivityEndTimeUtc = max(TimeGenerated), RareActivityCount = count() by DataType, AccountName, EventType, ServiceOrSystem, WinSecEventDomain; let Activity7day = activity(Prev7Day, PrevDay) | summarize HistoricalActivityCount = count() by DataType, AccountName, EventType, ServiceOrSystem; let NewActivityToday = ActivityLastDay | join kind=leftanti (Activity7day) on DataType, AccountName, ServiceOrSystem <snip>
Looking for such basic anomalies for all accounts would typically be too noisy – by restricting this to high-value accounts we limit the set of results just to those accounts where we are most concerned about the potential impact of a compromise.
Results from a sample network environment are shown below:
This shows a high-value account logged on to 3 hosts for the first time and also performed Sharepoint searches – activities that were atypical for this account based on its prior week history.
Retrieving related context for unusual privileged account activity
To aid in triage we want to know what related activity surrounded this unusual event so we have better context and can investigate in more detail if needed.
<snip> let RelatedActivity = (union isfuzzy=true (NewActivityToday | join kind=inner ( OfficeActivity | where TimeGenerated > LastDay | summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(OfficeWorkload), RelatedActivityServiceOrSystemSet = makeset(OfficeWorkload), RelatedActivityClientIPSet = makeset(ClientIP), RelatedActivityCount = count() by AccountName = tolower(UserId), RelatedActivityEventType = Operation ) on AccountName), <snip>
For the example above, the related activity (slightly redacted) is shown below. The account successfully logged onto 9 and then 13 different systems during this timeframe. Additionally, you can see the failed logon attempts to SharePoint as part of Office365, where the account is later able to login and then add a member to the role. Review of the source IP addresses can be a quick pointer to confirm that the activity is malicious.
Once such a query has been adapted with a suitable custom list it can be added to Azure Sentinel’s Hunting Queries:
Queries whose results are high-confidence enough that they should trigger alerts and investigations can be added via the Azure Sentinel alert rule creation experience.
In this post we have shown Azure Sentinel users how to search for unusual activity across a variety of logs to identify suspect activity in your environment – this helps reduce mean time to detection, reduce mean time to remediation and quickly understand the breadth of impact an attack may have had. This is just one such fusion of data that will help in this endeavor – you can contribute and share security knowledge and experience through valuable queries in the Azure Sentinel GitHub community.
Appendix – miscellaneous notes about the query language
Logs often have similar Entity types, but the column/property name may be different. How to normalize (for example lower/upper case as above) is an important consideration.
When union-ing multiple logs – some of which may not exist – an important argument to the “ union operation is the isfuzzy=true argument. This will keep the query from failing when the engine does the initial check for existence of each table in the union. See KQL documentation and best practices for further advice on queries.
When performing joins and one table is known to be smaller, put it on the left hand side to improve performance – in the case above the set of high-value counts is likely relatively smaller so we put it on the left hand side of the join operations.
KQL is the query language used and more details are available here.
Additionally, Log Analytics has some minor limitations from the full KQL, which is detailed here.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.