Monitor External Mailbox Forwards in all Office 365 Customer tenants

Microsoft Flow With External Forwarding Address Information

It’s common for hackers and bad actors to set up an external email forwarder on an Office 365 account they have gained access to.

We’ve come across this action on almost every account breach we’ve investigated, and have implemented this monitoring solution to help identify breaches when they occur.

It works by monitoring the ForwardingSMTPAddress property on Office 365 Exchange Online mailboxes. It compares the domain for this address against the accepted domains for your organisation. If this mailbox is forwarding externally, an action will be performed.

It’s important to note that in almost all cases we’ve looked at, the breaches were due to phishing attacks, with the users willingly providing their credentials to non-Microsoft login screens. No breaches were attributed to vulnerabilities in the Office 365 service and all could have been prevented by implementing multi factor authentication.

This solution involves the following components

A Timer Triggered PowerShell Azure Function

This script will check the mailboxes for all customer Office 365 tenants for external forwards. If a forward is detected, it will then update our table storage and trigger a Microsoft Flow

Azure table storage

Table storage stores details about the external forwards, and whether or not they’re approved.

Microsoft Flow

Microsoft Flow is used for notifications, and to allow us to quickly approve or remove detected forwards

A HTTP Triggered PowerShell Azure Function

This Azure function will either remove a forward from a mailbox, or update the table storage when the forward is approved.

Standalone script to check all Office 365 customer tenants for external email forwards

Before you implement this solution, I recommend you kick off this script. It’ll scan all customer tenants for external forwards, and export the details to a CSV. This script will let you know whether your customers are currently being affected by an unauthorised external forward.

How to monitor Office 365 customer tenants for external email forwards

The first step is to set up your two Azure Functions.

Use the steps in this guide to create an Azure Function App and two PowerShell Azure functions that can connect to Office 365. You’ll need to change it a little as follows:

  1. Create an Azure Function App
  2. Encrypt your Office 365 delegated admin credentials and add the details to the Function App’s Application Settings. You’ll need to use the credentials of a user with delegated admin permissions on your customer tenants.
  3. Create a Timer Triggered PowerShell Azure Function called TT-CheckForwardingAddresses and set it to run on a daily schedule: eg: 0 0 19 * * *
  4. Create a HTTP Triggered PowerShell Azure Function called HT-HandleForwardingAddresses
  5. Make sure you’ve connected to the function app via FTP and uploaded the key and MSOnline module into a bin folder within both function folders.
  6. Copy and paste the below scripts into the functions and click save.
  7. Copy the URL from the HTTP Triggered Azure Function by clicking Get function URL on the top right of the function, and save it somewhere for later.Copy URL From HTTP Trigger PowerShell Azure Function

Script for Azure Function to monitor external forwarding addresses (TT-CheckForwardingAddresses)

$FunctionName = 'TT-CheckForwardingAddresses'
$ModuleName = 'MSOnline'
$ModuleVersion = '1.1.166.0'
$username = $Env:user
$pw = $Env:password
#import PS module
$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1"
# 
$flowUri = "URLFromMicrosoftFlowGoesHere"
                            
$storageAccount = "StorageAccountNameGoesHere"
$accesskey = "StorageAccountPrimaryKeyGoesHere"
$TableName = "externalforwardingaddress"

Import-module $PSModulePath
 
# Build Credentials
$keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key"
$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath)
$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword)
 
function GetTableEntityAll($TableName) {
    $version = "2017-04-17"
    $resource = "$tableName"
    $table_url = "https://$storageAccount.table.core.windows.net/$resource"
    $GMTTime = (Get-Date).ToUniversalTime().toString('R')
    $stringToSign = "$GMTTime`n/$storageAccount/$resource"
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
    $hmacsha.key = [Convert]::FromBase64String($accesskey)
    $signature = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
    $signature = [Convert]::ToBase64String($signature)
    $headers = @{
        'x-ms-date'    = $GMTTime
        Authorization  = "SharedKeyLite " + $storageAccount + ":" + $signature
        "x-ms-version" = $version
        Accept         = "application/json;odata=fullmetadata"
    }
    $item = Invoke-RestMethod -Method GET -Uri $table_url -Headers $headers -ContentType application/json
    return $item.value
}
  
function InsertReplaceTableEntity($TableName, $PartitionKey, $RowKey, $entity) {
    $version = "2017-04-17"
    $resource = "$tableName(PartitionKey='$PartitionKey',RowKey='$Rowkey')"
    $table_url = "https://$storageAccount.table.core.windows.net/$resource"
    $GMTTime = (Get-Date).ToUniversalTime().toString('R')
    $stringToSign = "$GMTTime`n/$storageAccount/$resource"
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
    $hmacsha.key = [Convert]::FromBase64String($accesskey)
    $signature = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
    $signature = [Convert]::ToBase64String($signature)
    $headers = @{
        'x-ms-date'    = $GMTTime
        Authorization  = "SharedKeyLite " + $storageAccount + ":" + $signature
        "x-ms-version" = $version
        Accept         = "application/json;odata=fullmetadata"
    }
    $body = $entity | ConvertTo-Json
    $item = Invoke-RestMethod -Method PUT -Uri $table_url -Headers $headers -Body $body -ContentType application/json
}


# Connect to MSOnline

Connect-MsolService -Credential $credential -erroraction SilentlyContinue

# Start Script

$forwards = GetTableEntityAll -TableName $TableName

$customers = Get-msolpartnercontract -All -erroraction SilentlyContinue
foreach ($customer in $customers) {

    Write-Output "Connecting to $($customer.name)"
    $Msoldomains = Get-MsolDomain -TenantId $customer.tenantid | Sort-Object Name
    $InitialDomain = ($Msoldomains | Where-Object {$_.IsInitial})

    try {
        $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitialDomain.Name
        $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection -erroraction SilentlyContinue
        Import-PSSession $s -CommandName Get-Mailbox, Get-AcceptedDomain -AllowClobber -ErrorAction Stop
        $mailboxes = $null
        $mailboxes = Get-Mailbox -ResultSize Unlimited
        $domains = $null
        $domains = Get-AcceptedDomain | Sort-Object name

        $domainMatch = $false
        if ($Msoldomains[0].name -contains $domains[0].name) {
            $domainMatch = $true
        }

        if ($domainmatch) {
            foreach ($mailbox in $mailboxes) {

                $forwardingSMTPAddress = $null
                $forwardingSMTPAddress = $mailbox.forwardingsmtpaddress
                $externalRecipient = $null
                if ($forwardingSMTPAddress) {
                    $email = ($forwardingSMTPAddress -split "SMTP:")[1]
                    $domain = ($email -split "@")[1]
                    if ($domains.DomainName -notcontains $domain) {
                        $externalRecipient = $email
                    }
    
                    if ($externalRecipient) {
                        Write-Output "$($mailbox.displayname) - $($mailbox.primarysmtpaddress) forwards to $externalRecipient"
    
                        $forwardHash = $null
                        $forwardHash = [ordered]@{
                            Customer           = $customer.Name
                            TenantId           = $customer.TenantId
                            PrimarySmtpAddress = $mailbox.PrimarySmtpAddress
                            DisplayName        = $mailbox.DisplayName
                            ExternalRecipient  = $externalRecipient
                        }
    
                        $filter = $null
                        $filter = $forwards | Where-Object {$_.primarysmtpaddress -contains $mailbox.PrimarySmtpAddress}
                    
                        if (!$filter -or $filter.externalrecipient -notcontains $externalRecipient) {
                            InsertReplaceTableEntity -TableName $TableName -PartitionKey $customer.TenantId -RowKey $mailbox.PrimarySmtpAddress -entity $forwardHash
                        }
                        if ($filter.allowedaddress -notcontains $externalRecipient) {
                            $json = $forwardHash | ConvertTo-Json
                            Invoke-RestMethod -method POST -uri $flowUri -body $json -ContentType application/json
                        }
                    }
                }
            }
   
        }
        Remove-PSSession $s -erroraction SilentlyContinue
    }
    catch {
        Write-Output "Couldn't connect to $($customer.name)"
    }
}

Script for Azure Function to handle external forwarding addresses (HT-HandleForwardingAddresses)

# GET method: each querystring parameter is its own variable
if ($req_query_tenantid) {
    $tenantid = $req_query_tenantid 
}

if ($req_query_response) {
    $response = $req_query_response
}

if ($req_query_user) {
    $user = $req_query_user
}

if ($req_query_ext) {
    $ext = $req_query_ext
}

$FunctionName = 'HT-HandleForwardingAddresses'
$ModuleName = 'MSOnline'
$ModuleVersion = '1.1.166.0'
$username = $Env:user
$pw = $Env:password
#import PS module
$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1"

$storageAccount = "StorageAccountNameGoesHere"
$accesskey = "StorageAccountPrimaryKeyGoesHere"
$TableName = "externalforwardingaddress"


function MergeTableEntity($TableName, $PartitionKey, $RowKey, $entity) {
    $version = "2017-04-17"
    $resource = "$tableName(PartitionKey='$PartitionKey',RowKey='$Rowkey')"
    $table_url = "https://$storageAccount.table.core.windows.net/$resource"
    $GMTTime = (Get-Date).ToUniversalTime().toString('R')
    $stringToSign = "$GMTTime`n/$storageAccount/$resource"
    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
    $hmacsha.key = [Convert]::FromBase64String($accesskey)
    $signature = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
    $signature = [Convert]::ToBase64String($signature)
    $body = $entity | ConvertTo-Json
    $headers = @{
        'x-ms-date'      = $GMTTime
        Authorization    = "SharedKeyLite " + $storageAccount + ":" + $signature
        "x-ms-version"   = $version
        Accept           = "application/json;odata=minimalmetadata"
        'If-Match'       = "*"
        'Content-Length' = $body.length
    }
    $item = Invoke-RestMethod -Method MERGE -Uri $table_url -Headers $headers -ContentType application/json -Body $body
 
}

Import-module $PSModulePath
 
# Build Credentials
$keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key"
$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath)
$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword)


if ($response -contains "remove") {
    # Connect to MSOnline
 
    Connect-MsolService -Credential $credential
 
    # Start Script
 
    $Customers = Get-MsolPartnerContract -All | where-object {$_.tenantid -contains $tenantid}
    foreach ($customer in $customers) {

        $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.IsInitial -eq $true}
    
        Write-Output "Removing forward from $user"
        $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitialDomain.Name
        $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
        Import-PSSession $s -CommandName Set-Mailbox -AllowClobber

        set-mailbox $user -forwardingsmtpaddress $null

        Remove-PSSession $s
    }
}
if ($response -contains "approve") {
    $hash = @{
        AllowedAddress = $ext
    }
    MergeTableEntity -TableName $tablename -PartitionKey $tenantid -RowKey $user -entity $hash
}

Out-File -Encoding Ascii -FilePath $res -inputObject "Response processed"

Prepare your Azure Table Storage

  1. Download and install Azure Storage Explorer from www.storageexplorer.com
  2. Sign into your Azure account to access your storage accounts.
  3. Select a Storage Account (You can use the same one that is used by your Azure Function App if you like)
  4. Right click on tables and click Create Table, call it externalforwardingaddressCreate Azure Storage Table
  5. Right click on the Storage account to retrieve its Primary Key.Copy Azure Storage Primary key
  6. Switch back to your Azure Function App and update the $StorageAccount and $accessKey variables with the storage account name and Primary Key. Do this for both Azure Function Apps.Update Azure Function With Azure Storage Details

Create your Microsoft Flow

  1. Visit flow.microsoft.com and sign in with your Office 365 account.
  2. Click My Flows
  3. Click Create from blank.
  4. Click Search from hundreds of connectors or triggersCreate Microsoft Flow From Blank
  5. Search for Request and click Request – When a HTTP Request is received.Add HTTP Request Trigger To Microsoft Flow
  6. Copy and paste the following schema into the Request Body JSON Schema field:
    {
        "type": "object",
        "properties": {
            "Customer": {
                "type": "string"
            },
            "TenantId": {
                "type": "string"
            },
            "PrimarySmtpAddress": {
                "type": "string"
            },
            "DisplayName": {
                "type": "string"
            },
            "ExternalRecipient": {
                "type": "string"
            }
        }
    }
  7. Click + New Step.
  8. Add a sample action, eg. a mobile notification. It doesn’t matter what you add here, we just need to add an action so that we can save the flow and retrieve the Request URL.
  9. Name your flow something descriptive like ‘Request Forwarding Address Approval‘ and save it.Save Microsoft Flow As Request Forwarding Address Approval
  10. Save the flow and copy the URL from the HTTP Request Step, then save it somewhere for later.Copy HTTP Post URL From Microsoft Flow
  11. Edit the flow again and remove the action you just added.
  12. Add an Outlook – Send email with Options Step.
  13. Enter your own email, or the email address for your support desk as the recipient.Send Email With Options To User
  14. Define the following subject and body, dragging in the relevant output values from the Dynamic content menu.Create Send Email With Options Step
  15. Add Approve and Remove as options.
  16. Add a Condition to your flow. You can rename it to Check response.
  17. Add SelectedOption from the Dynamic content menu into the first field. Choose is equal to and type ApproveCheck Response In Microsoft Flow
  18. In the Yes section, add a HTTP – HTTP action. Rename it to Allow forwarding addressCreate Yes Condition Step In Microsoft Flow
  19. Choose GET as the method and copy and paste the URI of the HTTP Triggered Azure Function that we created earlier.
  20. At the end of the URI, append &tenantid=, then drag in the tenantId from the initial request output. Append &user= and drag in the PrimarySmtpAddress value. Append &ext= and drag in the ExternalRecipient value.
  21. Finally, add &response=approve.
  22. In the No section, add a HTTP-HTTP action. Rename it to Remove forwarding address.Create No Condition Step In Microsoft Flow
  23. Paste in the HTTP Triggered Azure function URL. Append &tenantid= and drag in the TenantId value. Append &user= and drag in the PrimarySmtpAddress value.
  24. Finally append &response=remove
  25. Your completed flow should look like this.Microsoft Flow Completed Example
  26. Click Update flow.

Update your Azure Function with the Flow details

  1. Return to your Timer Triggered Azure Function (TT-CheckForwardingAddresses),  update the $flowUri variable with the URL that we copied from the Microsoft Flow.Update Flow URI Variable In Azure Function
  2. Click Save and Run

Your Azure Function should now connect to all customer tenants. Any external forwarders on mailboxes will trigger a Microsoft Flow.

The Microsoft Flow will send you an email asking whether this forward is to be approved or removed.Microsoft Flow With External Forwarding Address Information

If you choose Approve, the Azure storage table will be updated with the approved external address. If you choose remove, the forwarder will be removed from the user’s mailbox. You can then contact the user and perform the appropriate actions to secure the account.Allowed External Forwarding Address In Azure Table Storage

Was this article helpful?

Related Articles