Hunting for Teams Phishing with Microsoft Sentinel, Defender, Microsoft Graph and MSTICPy
Published Aug 17 2022 08:00 AM 15.5K Views

As a growing amount of our communications are moving from traditional formats such as in-person meetings and email, to instant messaging and hybrid collaboration via Microsoft Teams, the risk of threat actors abusing Teams to delivery malicious content has also grown.


In our previous blog we discussed detecting some common threats in Teams using the Teams audit log such as anomalous users joining meetings or malicious administrative changes.


In this blog we will look at how to detect another threat actor angle in Microsoft Teams, the sharing of malicious links. Microsoft Teams provides protection against the sharing of malicious links using our Safe Links feature. However, as a defender it is still important to investigate blocked malicious events to determine their root cause and identify any broader compromise that may have led to the event.

Alongside this blog we have released a Jupyter Notebook, written using MSTICPy, our open-source cybersecurity toolkit to help you conduct your own investigations. The notebook can be found via the Microsoft Sentinel Notebooks Templates feature or via GitHub.



This investigation will use several data sources to piece together a threat actors’ activities. Collection and access to this data is required. The required data includes:

  • Team audit log events (ideally captured in Microsoft Sentinel)
  • SmartScreen events generated by M365 Defender (ideally captured in Microsoft Sentinel)
  • Teams message events via the Microsoft graph – note that this requires the creation of a Azure AD app with delegated permissions, more details on this can be found in this documentation.


Scoping the Investigation

In any environment with a significant user base there are going to be many URLs shared in messages daily, searching and investigating each one of these is not a feasible or useful task for a security analyst. Therefore, we need to start by scoping our investigation. As mentioned previously Safe Links provides protection against malicious links shared in Teams, therefore we can search for events where URLs originating from Teams were blocked by Safe Links and work back from there to identify who shared those links and what else they have done.

Safe Links data is present in two Microsoft 365 Defender datasets, either one we can use. The first is in M365 Defender DeviceEvents. To find the relevant events here we need to filter for SmartScreenUrlWarning events, and then look for instances where the URL was opened from teams.exe



| where TimeGenerated > ago(7d)
| where ActionType == "SmartScreenUrlWarning"
| join (DeviceEvents | where ActionType == "BrowserLaunchedToOpenUrl" | extend OpeningProcess = InitiatingProcessFileName) on DeviceId, RemoteUrl
| extend TeamsUser = InitiatingProcessAccountUpn1
| where OpeningProcess =~ "teams.exe"
| project-reorder DeviceName, RemoteUrl, OpeningProcess, TeamsUser



The other dataset is the new UrlClickEvents table, here you simply need to filter on the Workload field for Team based events.

Once we have found some suspicious events in either of these datasets we can start to look at who sent them and what else they have been doing.


Identifying user activity

Once we have a user we are interested in we want to find out what else they have been up to, and what other URLs they may have shared in Teams. To do that we need to look at what Teams and chats the user is in. The most effective way to do this is via the Microsoft Graph and the /joinedTeams endpoint.  In the companion notebook we do this by using MSTICPy’s Microsoft Graph data provider and calling

Screenshot showing Graph API calls with MSTICPyScreenshot showing Graph API calls with MSTICPy


Once we have a list of the Teams our target user is in we can search Microsoft Teams logs in Microsoft Sentinel for messages in those teams that contain URLs.



let teams = dynamic({list(teams['ID'].unique())});
    | where TimeGenerated > ago(30d)
    | where OfficeWorkload =~ "MicrosoftTeams"
    | where Operation in ("MessageCreatedHasLink", "MessageUpdatedHasLink")
    | where AADGroupId in (teams)
    | where UserId =~ USER
    | project MessageId, AADGroupId, ChannelGuid



For privacy reasons the Microsoft Teams logs do not contain the message content, therefore we need to again use the Microsoft Graph and call in order to get the message contents and the included URLs. Again, we can do this using MSTICPy:

Screenshot of Graph API calls with MSTICPyScreenshot of Graph API calls with MSTICPy


Within Microsoft Teams channels are not the only place to share URLs, they can also be shared as part of chats. So, in order to get a complete picture for a user we also need to enumerate chats that the user is part of and get messages from there. Again we need to use the Microsoft Graph to get this information, calling to get a list of chats a user is in, and then to get the message contents.

Screenshot of Graph API calls using MSTICPyScreenshot of Graph API calls using MSTICPy


We now have a list of messages containing URLs for our target user; however this may still be a large list of messages, we need to filter this list to see if there are any URLs that are of particular interest. To do this we first have to extract the URLs from the messages they are in. We can use MSTICPy’s IoC extract feature that identify URL patterns and extract them out. Once extracted we can look to identify URLs of interest by looking them up against Threat Intelligence sources using MSTICPy’s threat intelligence enrichment features:

Screenshot showing TI lookups using MSTICPyScreenshot showing TI lookups using MSTICPy


This output can then show us what other URLs the targeted user may have shared that are suspicious, as well as an overview of that URLs were shared and where. Using this information it is possible to better understand whether a URL blocked by Safe Links what legitimate activity, or was part of a pattern of malicious activity by the user, such as sharing multiple malicious links, or sharing malicious links across multiple channels and chats – something that likely indicates that the user account involved has been compromised.


Further Investigations

Once we have identified malicious users and URLs we can widen our search to look for other instances of the malicious URLs shared in the environment. One good method for this is the use network data in Microsoft Sentinel to look for instances of these URLs at the network level, this may help to identify other compromises where the malicious URL was shared from a source outside the monitoring scope, for example via personal email. We can do this by querying various data sources in Microsoft Sentinel:



let urls = dynamic({all_iocs_df["Observable"].unique().tolist()});
    | where TimeGenerated > ago(7d)
    | where RequestURL in (urls)
    | extend timekey = bin(TimeGenerated, 1h)
    | join kind=inner (DeviceNetworkInfo
    | where TimeGenerated > ago(7d)
    | mv-expand IPAddresses
    | extend device_ip = tostring(IPAddresses.IPAddress)
    | extend timekey = bin(TimeGenerated, 1h)) on $left.SourceIP == $right.device_ip, timekey
    | project-reorder DeviceName1, timekey
    | summarize max(timekey) by DeviceName1



We can also look for other alerts related to these URLs that may have been raised by other security products. Again, Microsoft Sentinel proves ideal for this, collating data and alerts from multiple sources in one place:



let urls = dynamic([{all_iocs_df["Observable"].unique().tolist()}]);
    | mv-expand todynamic(Entities)
    | where tostring(Entities.Type) =~ "url"
    | evaluate bag_unpack(Entities, "Entities_")
    | where Entities_Url in (urls)




As more and more enterprise communications move to Teams it’s more important than ever to monitor activity there for security risk. Whilst Teams has robust protections against the sharing of malicious URLs its still important to investigate blocked events to identify the source of the issue, and any wider compromise it might be indicative of.  

When conducting these investigations using complex data sources such as Microsoft Teams it is often necessary to leverage multiple data sources at once to enrich and expand core data sources. Jupyter Notebooks in Microsoft Sentinel, using MSTICPy’s wide range of features, provides an ideal environment where these investigations can be conducted with data from a range of internal and external sources all accessed from a single location.

To learn more about Microsoft Sentinel Notebooks and MSTICPy please review our documentation and training resources:


Version history
Last update:
‎Aug 16 2022 05:15 PM
Updated by: