Perspektiver og nyheder om

cyberforsvar der virker

2018-05-07 11:30 | Nichlas Jørgensen

CEO fraud log analysis

At Langkjaer Cyber Defence we help our customers respond to cyber attacks. Here on our blog we share as many of our learnings as possible to help others deal with similar situations.

Note: We mostly blog in Danish, but this post is written in English to reach a wider technical audience.

During a recent investigation of a CEO fraud incident, I wanted to try out a new idea that involved geolocation for successful authentication attempts vs. time of authentication.

There are many ways to "hunt" for evil in log files. The method described below is just one of many ways trying to get from nothing to having an investigation lead to something. NB! For this analysis method to work, you will need two successful authentication attempts in different countries within a given timeframe.

Office365 log files from the account

We were handed the raw log files extracted from the Office365 account of the CEO, and I started thinking about how to analyse them:

  • Should I index them into Elasticsearch ?
  • Easy wins with some bash oneliners ?
  • Custom analysis of the logfiles in Python ?
  • Create some more human friendly .csv's and go through it manually ?

My normal habit when starting up a new analysis of something is jumping directly into coding a parser in Python. Let's just say I have written a LOT of parsers which pretty much did the same thing, just for different formats...

Stop "hack and slash" coding, and let's work smarter...

Here at Langkjaer Cyber Defence we are always trying to think "out of the box" and come up with new ideas that can be used as more generic ways of detecting malicious activity.

Think about a log file for any given service that has some kind of authentication via the Internet. What are the technical things you have, and how can you enrich them to ease your investigation:


There are usually a timestamp of some kind for every log entry. If not, analysis will be hard :-). Besides parsing these into something that can be sorted by timestamps, not much to enrich here.

"Successful authentication" log entry

The standard is to have some string / identifier that defines a successful authentication attempt.

Public IP-address of the IP that authenticated successfully

There are tons of ways to enrich these. However, a favorite of mine are the Max Mind Databases. They provide offline enrichment of IP's, and can be used to get GPS coordinates from an IP.

Where and when has someone successfully authenticated to the service?

Speaking of location (or GPS coordinates), I would like to underline the fact that humans only can travel at a certain speed between countries. So, for instance: It should be impossible for a user account to logon in Denmark and then logon 30 minutes later from Mexico. And if this occurs it is definitely something to manually investigate further.

Enough with the theory, lets get down to business

The first hurdle was to build some regexes for the various values I needed to extract from the log format (Office365), and also finding the identifier for successful authentication attempts. The idea was to try to make the rest of the code generic, so you would only have to define the values for another log format:

#Define path to logfiles
self.path_to_logfiles = '/logs'

#Define a string in the log entry, which will define a successful logon
self.successful_auth_string = ',"UserLoggedIn",'

# for easy python regex building :-)
#Define a ipv4 regex for extracting the public IP, of the successful auth
self.ipv4_ip_regex = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')

#Define a timestamp regex for when the successfuly auth was created
self.timestamp_regex = re.compile('\d{0,4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}.\d{1,3}')

#Defing an account regex, in our example logs(Office365 logs) the username was a email address
self.user_account_regex = re.compile('[\w\.-]+@[\w\.-]+')

#Define the Python lib datetime format for self.timestamp_regex
self.datetime_format = '%Y-%m-%dT%H:%M:%S.%f'

Next up was simply to loop over the log files and extract information grouped by each user ID:

for line in logfile:
    if self.successful_auth_string in line:
        ip =,line).group(), line).group()
        d = datetime.strptime(timestamp_string, self.datetime_format)
        user_account =, line).group()

        if user_account not in self.auth_dict:
            self.auth_dict[user_account] = []

Now I have the stuff extracted from the logs, time to find some evil bits...

First thing was to loop over the self.auth_dict which now has successful authentications grouped per user with a datetimeobject timestamp (for easy sorting).

for user in self.auth_dict:
    #Sort auths by timestamps asc
    for login in sorted(self.auth_dict[user]):
        ip = login.values()[0]
        #Use cache or do lookup in maxmind file
        if ip not in self.mm_cache:
            geo_information =
            self.mm_cache[ip] = geo_information
            geo_information = self.mm_cache[ip]									

Next step was to compare current authenticated GPS coordinates with last authenticated GPS coordinates for successful authentications

First problem: Figure out how to calculate distance between two GPS coordinates in Python.

After asking Google, I stumbled upon this library: Geopy - "Calculate the geodesic distance between two points." Exactly what I needed.

Now I needed some kind of maximum travel speed between locations, so I just went for the average speed (km/h) of a plane:

self.max_travel_speed = 1000

According to: Average speed of planes.

Next, I wrote a small function which takes two coordinates with a corresponding timestamp, and returns the distance between the two coordinates combined with the travel speed between these.

km_between_ips = geopy.distance.geodesic(current_cord, last_cord).km
seconds_between_logins = (current_timestamp - last_timestamp).seconds
if seconds_between_logins > 0:

    #3600 seconds per hour
    time_in_hours = seconds_between_logins / float(3600)
    km_per_hour = km_between_ips / float(time_in_hours)
    return {'km_per_hour' : km_per_hour, 'distance_in_km': km_between_ips}

First Run... Great FAILURE

As always when trying something new, there are some bumps on the road and I had definitely hit one here...

It turned out that the GPS coordinates are estimated within a radius of x number of kilometers, and I was getting too many false alerts for Danish IP's.

So back to the drawing board.

"If it is important to you, you will find a way. If not you'll find an excuse"

I figured, "OK, lets only check the ones where a country change has happened since last successful authentication":

#Handle only when countries are changing to big inaccuracy in the same country
if str(['en']) != str(last_entry['geo'].country.names['en']):
    current_cord = [geo_information.location.longitude, geo_information.location.latitude]
    speed_dict = self._do_gps_analysis(current_timestamp, current_cord, last_entry['timestamp'], last_entry['coords'])
    if (speed_dict is not False) and (speed_dict['km_per_hour'] >= self.max_travel_speed):
            print 'Potential evil activity found for user account: ' + user
        print 'Current IP: ' + ip + ' -> ' + str(['en'])
        print 'Last IP: ' + last_entry['ip'] + ' -> ' + str(last_entry['geo'].country.names['en'])
        print 'Speed: ' + str(speed_dict['km_per_hour']) + ' km/h'
        print 'Distance: ' + str(speed_dict['distance_in_km']) + ' km'
        print '******************************'

From Nigeria with love ...

After modifying the code a little bit to only include the above mentioned country changes, results where looking much more interesting now:


Hello evil bits...

Apparently, somebody had successfully authenticated several times in Nigeria and in Denmark in a very short timespan. If this was a physical move, someone would have traveled at no less than 175744 km/h between Denmark and Nigeria. This seemed highly unlikely.

Now after investigating the initial malicious login from, I noticed an interesting pattern before the successful login. Multiple failed authentication attempts coming from all over the world, leading up to the successful authentication attempt from Nigeria for the CEO's account:

2017-11-23 03:46:04 UserLoginFailed Indonesia
2017-11-23 18:47:59 UserLoginFailed China
2017-11-24 13:29:59 UserLoginFailed China
2017-11-25 03:42:32 UserLoginFailed China
2017-11-25 18:31:32 UserLoginFailed Mongolia
2017-11-27 05:56:47 UserLoginFailed India
2017-11-27 18:16:16 UserLoginFailed China
2017-11-28 21:41:58 UserLoginFailed China
2017-11-30 10:34:56 UserLoginFailed China
2017-12-02 08:10:21 UserLoginFailed Republic of Korea
2017-12-03 08:26:14 UserLoginFailed China
2017-12-04 12:09:02 UserLoginFailed United Arab Emirates
2017-12-05 12:08:02 UserLoginFailed China
2017-12-06 04:41:21 UserLoginFailed China
2017-12-06 18:11:29 UserLoginFailed China
2017-12-07 07:08:04 UserLoginFailed China
2017-12-07 18:23:44 UserLoginFailed China
2017-12-10 01:44:52 UserLoginFailed China

Interestingly, there were never more than two authentication attempts performed per day, and someone made sure to spread the attempts over multiple IP-addresses, probably to avoid triggering any alerts for the given account.

Let me handle some mails for you...

After gaining access to the CEO's account, the perpetrators tried to get the CEO's bank to transfer money to an account. A clever trick they did before contacting the bank was to setup Office365 mail redirect rules for the correspondence with the bank, so this communication would remain hidden from the CEO.

Let's have a look at these evil IPs...

Investigating the IPs used for the unsuccessful authentication attempts quickly revealed an interesting pattern. On Pastebin, 13 of the 18 IPs used was mentioned in a posting in July 2017 (Titled: botnetas)


This list was "only" 2068 IP's long, so I figured that 13 IP's being present in such a small list highly likely indicates that they are related to each other in some way. Probably this was a botnet / chained infrastructure in some way that the bad guys were using for executing the authentication attempts.

After having a closer look at the IP's involved in the successful authentication from Nigeria, I stumbled on this site: where apparently the post appears to have been made from IP: which was the initial IP-address that had successfully been used to gain access to the CEO's account.


The post was apparently made from a company named: Gosky Resources Nigeria limited

If this company is involved in the malicious activity, or just a victim of someone else using their infrastructure is impossible to tell. However, since the post appears being dated to 16 February 2018 and the successful authentication was on 21 January 2018, I find it highly likely that the infrastructure was tied to the company at that point in time.

Digging further through the interwebz, I found another site where this guy's IP also was logged. Not the most stealthy scammer...


Apparently a German theater festival in Frankfurt had a guestbook which was filled with Internet scammers trying to scam / advertise. Why this IP appeared here with a empty post on this site no one knows. However, it is an interesting place to find the IP together with ~6000 scam guestbook postings.


A lot of these postings were about great cheap loans or credit where applying is done through "highly trustworthy" Gmail accounts like "" or "". Also a lot of these guys made sure to warn about Internet scams which I found kinda ironic...

Other interesting stuff...

Based on the other IP from the incident (, I found this interesting post on a forum related to a multiplayer strategy game called "Hackers"




Sticking to the facts, what do we now know about the incident:

  • The two malicious IP's that successfully authenticated to the CEO's account were both from Nigeria.
  • We saw multiple authentication requests targeting the account up until the successful authentication. Many of them seem to have a relationship with each other.
  • Office365 mail redirect rules was created to hide the correspondence with the bank.
  • Thankfully, the incident was resolved by the bank contacting the CEO, so the perpetrators didn't receive any money. The issues were resolved after a password reset for the CEO's account, in combination with the above investigation.

    This is a prime example of why two factor authentication is important: If this had been implemented the above attack method would not have been successful.

    Implementing two factor authentication of course introduces extra work for the users. There is no way of getting around this. However, today you can use an app on your phone or acquire a physical device token. It is a fairly simple way of raising the security bar, and one that we will strongly recommend.

    Another thing that could had prevented the incident was the use of a stronger password, which would make bruteforcing attempts significantly harder.

    IOC's and Python script for "hunting" geolocation logons vs timestamps

    The complete Python script for doing geolocation analysis of logons can be found here:

    IOC IP's:

    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Unsuccessful auth attempt, mentioned here:
    • / Inbox redirect rules
    • / Successful auth
    • / Successful auth