How to hunt & defend against Business Email Compromise (BEC)
商业电子邮件泄露(BEC)是一种常见的网络攻击手段,通过钓鱼邮件获取用户凭证并访问敏感信息或发起内部钓鱼攻击。文章介绍了通过分析登录日志、地理位置等指标进行威胁狩猎的方法,并建议实施多因素认证、条件访问策略和提升用户安全意识以减少风险。 2025-3-21 07:30:0 Author: blog.nviso.eu(查看原文) 阅读量:25 收藏

Business email compromise (BEC) remains a commonly utilized tactic that serves as leverage for adversaries to gain access to user resources or company information. Depending on the end goals of the adversaries, and on the compromised user’s business role – the potential impact can vary from simply accessing sensitive information (e.g., from emails, files uploaded in SharePoint), phishing more users via internal phishing campaigns, or even result in cloud ransomware given that the user has enough permissions to manipulate cloud resources (e.g., blob storages, S3 buckets) [1][2][3]. In this article we will describe some investigation steps to hunt for this activity, and suggest preventative actions to reduce the attack surface.

Threat hunting

Based on our experience and on recent incidents managed at NVISO, the majority of cases stem from successful phishing attacks. These attacks frequently employ phishing kits. When a user enters their credentials into a phishing site, the adversary quickly uses them, often targeting the OfficeHome application. This is typically done within seconds or minutes and may involve mimicking the user’s user-agent (figure 2.). Other commonly targeted applications include: Outlook, Exchange Online, SharePoint etc.

During the initial hunting phase, establishing a baseline of expected user logon activity is essential for identifying deviations. Indicators that we are going to use, are:

  • IP ranges (especially those that are not utilized by more users);
  • Device (whether the device is managed or not);
  • Trust type;
  • User agent;
  • Country;

For instance, if a user typically logs in from United States, but logons are initiated from Korea using an unregistered device, this should raise concerns. By utilizing the following query, we can identify potential deviations from the users’ historical logon activity. The IP addresses returned from the query, allow us to investigate suspected activity on a per-user basis and determine if there are additional indicators that might suggest a potential Business Email Compromise (BEC).

Note that the query relies on historical user logon activity from trusted/registered devices, hence it may be prone to false-positives for environments that do not follow proper IT hygiene (i.e., users not having registered/trusted devices).


let ApplicationList = dynamic(["OfficeHome", "Outlook", "Exchange"]);
let LookbackTime = 90d;
let SinginLogsTable = materialize (SigninLogs
    | where CreatedDateTime > ago(LookbackTime)
    | extend
        Information = parse_json(LocationDetails),
        DeviceDetail = parse_json(DeviceDetail),
        AuthenticationDetails = parse_json(AuthenticationDetails)
    | mv-expand AuthenticationDetails
    | extend
        State = tostring(Information.state),
        Country = tostring(Information.countryOrRegion),
        OS = tostring(DeviceDetail.operatingSystem),
        Browser = tostring(DeviceDetail.browser),
        Trusttype = tostring(DeviceDetail.trustType),
        Device = tostring(DeviceDetail.displayName),
        AuthenticationStepResultDetail = tostring(parse_json(AuthenticationDetails.authenticationStepResultDetail)),
        AuthenticationMethod = tostring(parse_json(AuthenticationDetails.authenticationMethod))
    | project
        Type,
        CreatedDateTime,
        UserPrincipalName,
        IPAddress,
        Country,
        State,
        OS,
        Device,
        Trusttype,
        UserAgent,
        Browser,
        ResultType,
        AuthenticationRequirement,
        AppDisplayName,
        AuthenticationStepResultDetail,
        AuthenticationMethod,
        UserId,
        AuthenticationDetails
    );
//Set a baseline of expected IP addresses to be filtered out later
let ExpectedIPAddresses = SinginLogsTable
    | project CreatedDateTime, IPAddress, UserPrincipalName, Device, Trusttype
    | where isnotempty(Device) or isnotempty(Trusttype)
    | distinct IPAddress;
let ExpectedNonInteractiveIPAddresses = AADNonInteractiveUserSignInLogs
    | where CreatedDateTime > ago(30d)
    | extend
        Information = parse_json(LocationDetails),
        DeviceDetail = parse_json(DeviceDetail)
    | extend
        Country = tostring(Information.countryOrRegion),
        Trusttype = tostring(DeviceDetail.trustType),
        Device = tostring(DeviceDetail.displayName)
    | project UserPrincipalName, IPAddress, Trusttype, Device
    | where isnotempty(Device) or isnotempty(Trusttype)
    | distinct IPAddress;
SinginLogsTable
| where CreatedDateTime > ago(7d)
| join kind=inner (SinginLogsTable
    | summarize CountryList = make_set(Country) by UserPrincipalName
    )
    on UserPrincipalName
//Remove entries that a user has logons over the specified threshold (EventCount)
| join kind=leftanti (SinginLogsTable
    | summarize EventCount = count() by UserPrincipalName, Country
    //Adjust counter when needed, the higher the number the more sensitive for FP
    | where EventCount > 70
    )
    on UserPrincipalName and Country
//Remove entries when a user is only logging from one country (e.g., only BE)
| join kind=leftanti (SinginLogsTable
    | summarize CountryList = make_set(Country) by UserPrincipalName
    | where array_length(CountryList) == 1
    )
    on UserPrincipalName
//Remove entries where a user was observed logging from a registered/trusted device
| where IPAddress !in (ExpectedIPAddresses)
| where IPAddress !in (ExpectedNonInteractiveIPAddresses)
//Comment out the next line to exclude IPv6 IP addresses
// | where IPAddress !has ":"
| where isempty(Device) and isempty(Trusttype)
//Filter for successful logins or failed logon attempts that had correct password and failed (could indicate compromised account)
| where ResultType == 0 or (ResultType != 0 and AuthenticationRequirement == "multiFactorAuthentication" and AuthenticationStepResultDetail == "Correct password")
//Specify the applications that a suspected user would try to access. For a more narrowed down query start with 'OfficeHome' only
| where AppDisplayName has_any (ApplicationList)
| summarize max(CreatedDateTime), min(CreatedDateTime), CountryList = make_set(CountryList), UnexpectedCountries = make_set(Country), IPAddresses = make_set(IPAddress), AccessedApplications = make_set(AppDisplayName), AuthenticationResults = make_set(AuthenticationStepResultDetail), EventCount = count() by UserPrincipalName
| extend ExpectedCountries = set_difference(CountryList, UnexpectedCountries)
| project min_CreatedDateTime, max_CreatedDateTime, UserPrincipalName, ExpectedCountries , UnexpectedCountries, IPAddresses, AccessedApplications, AuthenticationResults, EventCount
| sort by EventCount desc

C#


Figure 1: Query results - suspected-as-compromised users
Figure 1: Query results – suspected-as-compromised users

The marked users in the figure above (fig. 1), were indeed compromised based on their logon activity. Upon further inspection of a user’s logon activity, we identified that although the user typically logs in from Germany using a registered device, there was an attempt to log in from United States to OfficeHome using a correct password, but from an unknown device. In this case the adversary managed to mirror the compromised user’s browser & user agent as well (fig. 2).

Suspected user's logon activity
Figure 2: Suspected user’s logon activity

Consulting public threat intelligence databases such as AbuseIPDB regarding the reputation of the source IP address can provide additional insights, particularly if an IP address has ever been associated with known malicious activity.

AbuseIPdb intel on IP address
Figure 3: AbuseIPdb intel on IP address

Upon completing the scoping of the suspected IP addresses, it is important to investigate whether additional users might be impacted. Hence we should investigate for the organization’s user logon activity (both interactive and non-interactive) that originate from these IP addresses, which are deemed suspicious.

It is worth mentioning that if you encounter a VPN IP address (e.g., SurfShark), you should extend your search to include at least the /24 subnet of that IP address. For example if the IP address is 146[.]70[.]183[.]190, the search should be expanded to cover the range 146[.]70[.]183[.]0/24.

When a user account is deemed to be compromised, it is prudent to assume that post-compromise activities may take place. Such activities include, but are not limited to:

  • Accessing emails;
  • Registering new devices and/or MFA methods;
  • Creating inbox redirection rules;
  • Conducting an internal phishing campaign;
  • Accessing files in cloud resources (e.g., SharePoint, DropBox);

Some sample queries that can assist with the investigation of post-compromise activities, can be seen bellow.


KQL Suspicious Operations Query:

let TargetUserPrincipalNameList = dynamic(["[email protected]", "[email protected]"]);
let OperationNames = dynamic(["User registered security info", "User started security info registration", "User started password reset", "User started security info registration", "User started password change", "User deleted security info", "User updated security info", "User changed default security info"]);
let timeframe = 30d;
AuditLogs
| where TimeGenerated > ago(timeframe)
| mv-expand InitiatedBy, TargetResources, AdditionalDetails
| extend
    InitiatedUserPrincipalName = parse_json(InitiatedBy)['user']['userPrincipalName'],
    InitiatedIpAddress = parse_json(InitiatedBy)['user']['ipAddress'],
    TargetUserPrincipalName = parse_json(TargetResources)['userPrincipalName'],
    TargetResourceDisplayName = parse_json(TargetResources)['displayName'],
    TargetResourceID = parse_json(TargetResources)['id'],
    TargetResourceType = parse_json(TargetResources)['type'],
    UserAgent = parse_json(AdditionalDetails)['value']
| project-reorder
    TimeGenerated,
    LoggedByService,
    OperationName,
    Result,
    ResultDescription,
    InitiatedIpAddress,
    TargetUserPrincipalName,
    TargetResourceID,
    TargetResourceType,
    InitiatedUserPrincipalName,
    Identity,
    UserAgent,
    AdditionalDetails
//Remove comment bellow to add the suspicious IP addresses, and comment out the TargetUserPrincipalName checks
// | where InitiatedIpAddress in ()
| where TargetUserPrincipalName has_any (TargetUserPrincipalNameList)
    or InitiatedUserPrincipalName has_any (TargetUserPrincipalNameList)
    or TargetResources has_any (TargetUserPrincipalNameList)
| where (AADOperationType == "Add" and LoggedByService in ("Authentication Methods", "Device Registration Service")) or OperationName in (OperationNames)

C#

Suspicious operations query results
Figure 4: Suspicious operations query results

KQL Inbox Redirection Rule Query:

let Timeframe = 30d;
//Fill either or both lists
let IPAddress = dynamic(["<IP_ADDR>"]);
let Username = dynamic(["[email protected]"]);
OfficeActivity
| where TimeGenerated > ago(Timeframe)
| where OfficeWorkload == "Exchange"
| where ClientIP has_any (IPAddress) or Client_IPAddress has_any(IPAddress)
//| where UserId in (Username)
| where Operation in ("UpdateInboxRules","Set-InboxRule","New-InboxRule","Remove-InboxRule","Enable-InboxRule","Disable-InboxRule")
| parse Parameters with * "{\"Name\":\"MoveToFolder\",\"Value\":\"" MoveToFolder "\"}" *
| parse Parameters with * "{\"Name\":\"Name\",\"Value\":\"" RuleName "\"}" *
| parse Parameters with * "{\"Name\":\"SubjectContainsWords\",\"Value\":\"" SubjectOrBodyContainsWords "\"}" *
| parse Parameters with * "{\"Name\":\"MarkAsRead\",\"Value\":\"" MarkAsRead "\"}" *
| parse Parameters with * "{\"Name\":\"ForwardTo\",\"Value\":\"" ForwardTo "\"}" *
| project-reorder TimeGenerated, RecordType, UserId, Operation, ResultStatus, MoveToFolder, RuleName, SubjectOrBodyContainsWords, ForwardTo, MarkAsRead

C#

Inbox redirection rule query results
Figure 5: Inbox redirection rule query results

Mitigation & Prevention

In order to reduce the impact of a BEC, we have a couple of weapons in our arsenal. Generally, such mitigation & prevention measures include:

  • Organization’s users should have MFA enrolled
  • Proper Conditional Access Policies (CAPs) in place
  • Phishing simulation campaigns

While it may seem obvious, user accounts that do not utilize any multi-factor authentication (MFA) significantly increase the chances of a successful compromise. In such cases, an adversary only needs to acquire or guess the user’s password to gain access to their account. Implementing MFA is a critical step in enhancing account security and reducing the risk of unauthorized access.

Conditional Access Policies (CAPs) are an effective tool for blocking access to resources based on specific criteria. For instance, if a user that attempts to login from an anonymous IP address (e.g., VPN, Tor) or if the logon attempt originates from an non trusted device, the login should be declined [4].

Perhaps the most challenging task is enhancing user awareness. The majority of phishing attacks rely on the human factor, which is often the weakest link of the chain. Therefore it is crucial to educate the users properly, in order to spot phishing attacks, and understand the consequences of their actions.

Sources

[1] https://www.sentinelone.com/blog/the-state-of-cloud-ransomware-in-2024/

[2] https://www.mitiga.io/blog/ransomware-strikes-azure-storage-are-you-ready

[3] https://www.sans.org/blog/ransomware-in-the-cloud/

[4] https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-policies

Profile picture - Thomas Papaloukas

Thomas Papaloukas

Thomas works as a Senior Intrusion Analyst at NVISO, focusing on incident response & operations, and enjoys fiddling with detection and reverse engineering. He likes the challenges of identifying and responding to security threats and an effective way to detect & prevent them.


文章来源: https://blog.nviso.eu/2025/03/21/how-to-hunt-defend-against-business-email-compromise-bec/
如有侵权请联系:admin#unsafe.sh