Based on an original 2015 article by Paul Cunningham
Using Geolocation Services to Resolve IP Addresses in Audit Logs and IP Addresses
Many Microsoft 365 and Exchange Server logs include IP addresses. The Microsoft 365 is an excellent example as every audit event includes an IP address for the source. In the context of Exchange Server, IIS logs hold information about the IP addresses used for connections like OWA, ActiveSync, and EWS.
As administrators review the events in logs, it’s natural that they might want to check IP addresses, especially if the addresses come from an unfamiliar range that isn’t used for a company location. Conditional access policies allow organizations to restrict connections to known locations. If you use PowerShell to extract and analyze log data, a variety of IP geolocation services are available to resolve IP addresses.
Web-Based Geolocation Services
IP-API is one of the major IP geolocation services. It supports free access for non-commercial use. Currently, to protect the integrity of its service, IP-API throttles access at 45 HTTP requests per minute for the endpoint that you’re likely to use with PowerShell. Other APIs, like FreeGeoIP.net have lower limits for free access while also offering paid subscriptions with much higher limits. As normal, look around to find a service that meets your needs. Some web services will block access to an IP address if it continually makes too many requests.
The Microsoft 365 Kill Chain and Attack Path Management
An effective cybersecurity strategy requires a clear and comprehensive understanding of how attacks unfold. Read this whitepaper to get the expert insight you need to defend your organization!
Using PowerShell to Resolve an IP Address
In all cases, you’re likely to use the Invoke-RestMethod cmdlet to send a query to the web service and receive structured response. In the case of IP-API, the response comes in JSON format. Here’s an example:
IPInfo = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/20.81.111.85" $IPInfo status : success country : United States countryCode : US region : VA regionName : Virginia city : Boydton zip : 23917 lat : 36.677696 lon : -78.37471 timezone : America/New_York isp : Microsoft Corporation org : Microsoft Azure Cloud (eastus) as : AS8075 Microsoft Corporation query : 20.81.111.85
Another example of using the Invoke-RestMethod cmdlet to retrieve information from a web service is using the Microsoft Translator service to translate text into a target language.
An example script that uses Invoke-RestMethod to interact with the IP-API service is available from GitHub.
Avoiding Throttling
As noted above, web services often throttle inbound HTTP requests to avoid the possibility that a rogue actor or poor code might overwhelm their service with a flood of requests. With this point in mind, any script that wishes to analyze IP information should incorporate a check to avoid throttling.
One way to avoid issues is to build a pause into the script using the Start-Sleep cmdlet. For instance, because IP-API limits requests to 45 per minute, a script could pause for 1.4 seconds after each request.
Another method is to store the IP information for resolved addresses in a hash table and check the table to see if an address is present before making a new request. This technique works well because many of the IP addresses found in logs are the same.
Here’s some code to look up an IP address against a hash table to decide if it’s necessary to issue a request to the web service. If the information already exists, it’s fetched from the hash table. If not, the script runs the request and stores the information in the hash table.
$IPInfo = $Null If (!($IPAddressHash[$AuditData.ClientIP])) { $IPInfo = Get-IPGeoLocation -IPAddress $AuditData.ClientIP $IPAddressHash.Add([string]$IPInfo.IP,$IPInfo) # Sleep to avoid any throttling issues with the web service Start-Sleep -Seconds 1 } Else { # Get the IP information from the hash table $IPInfo = $IpAddressHash[$AuditData.ClientIP] }
Obviously, it’s possible to write more bullet-proof code with good error handling but this is enough to prove the principal.
Using Conditional Access Locations
If you wanted to focus on IP addresses that aren’t used by the organization, you could extract the set of IP address ranges defined for Azure AD locations. Typically, conditional access policies use these locations to test if an incoming connection is internal or external. The Get-MgIdentityConditionalAccessNamedLocation cmdlet (from the Microsoft Graph PowerShell SDK) retrieves details of the locations. For example, this code finds the current set of locations and extracts the IP ranges defined in the locations:
[array]$CAKnownLocations = Get-MgIdentityConditionalAccessNamedLocation [array]$IPAddressRanges = $Null ForEach ($Location in $CAKnownLocations) { $IPRanges = $Null $IPRanges = $Location.AdditionalProperties['ipRanges'] If ($IPRanges) { ForEach ($Address in $IPRanges) { $IPAddressRanges += $Address['cidrAddress'] } } }
The result is a set of IPv4 and IPv6 addresses in CIDR format:
51.171.212.129/24 2001:db8::/56 2001:db8::/53
Checking an IPv4 address against a CIDR address requires expansion of the CIDR into individual addresses in the range. This script does the job. For example, here’s how to use the function in the script to populate an array using the two IPv4 CIDR discovered in the last step:
[array]$IPAddresses = Get-IPRange -Subnets "51.171.212.200/24", "51.171.212.129/24"
It’s now easy to check IPv4 addresses found in logs against the array to determine if they belong to an internal location. For example:
If ($AuditData.ClientIP -in $IPAddresses) { $InternalFlag = $True } Else { $ InternalFlag = $False }
If you don’t use IP locations for conditional access policies, you can create and populate the array in a different manner. For instance, you might have all the IP addresses available within the company defined in an Excel spreadsheet that can be read by PowerShell to create the array.
Using Geolocation Services
It’s not hard to use geolocation services to resolve IP addresses with PowerShell. The trick is to do so in an intelligent manner so that the web service doesn’t throttle or block access. This is possible by including code to detect internal IP addresses and to only request resolution from a geolocation service for unknown external addresses. Feel free to improve the PowerShell code presented here.
Hello Paul,
I tried your script from the Github looks like FreeGeoIP.net is no longer letting you use api calls without a key, could you please check and update the script to include the api key ? Thanks a lot.
You now need an API key for this to work and it’s limit is 10000 per month as of Jan 2021.
Edit ^^^^^ this is for a list of IPs. Not raw logs, but pulling IPs not to rough.
Why bother with REST? This does just as well:
(edit: removed code)
The Real Person!
Author Paul Cunningham acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
I’ve removed your code because it breaks the layout of the page.
But why bother with REST? Why not? Why bother with your method? Why bother with any “method A” if “method B” also works?
There are many ways to do any task in PowerShell. If you think your method is better for some reason, then explain why. Publish your code somewhere, write a blog post. Showing a different bit of code with no clarification is not very helpful to anyone.
Excellent comment.
Great script Paul!
Paul, do you have any expirience about searching messages/appointments using EWS API ?
I read many articles about it but still cannot finish my task (I need to find all recurring appointments).
Than I found a seems like working script, but it requires exchange API DLL’s. Well, I cannot understand where to find them.
Can you advice something?
Link for that script:
http://blogs.msdn.com/b/emeamsgdev/archive/2015/02/25/powershell-search-for-appointments.aspx
The Real Person!
Author Paul Cunningham acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
It’s the EWS Managed API you’re looking for. Can be downloaded from Microsoft.
Thanks Paul!
I should think better about it before asking )