Welcome to the second part of our CrowdSec Security Engine 1.5 in Action series!
In part one, we saw how to detect a successful SSH brute force attack with the new Security Engine v1.5 feature, conditional scenarios.
In this installment, I’ll demonstrate how you can leverage the same feature to detect impossible travel — aka an account connecting from two different locations, far from each other, in a short amount of time. This method, known as impossible travel detection, is crucial for identifying suspicious IP behavior and ensuring account security.
We will use the SSHD service again as an example for this article, but this feature can be used for any service logging authentication.
What is impossible travel?
Unusual or anomalous activities associated with an IP address are considered suspicious and may indicate malicious intent or unauthorized access.
Impossible travel, in particular, refers to a user or system that is observed accessing services or networks from geographically distant locations within a time frame that makes such movement physically impossible (e.g., logging in from New York and then from Tokyo an hour later). These behaviors often trigger security alerts, as they may signal account compromise, use of proxy networks, or other unauthorized actions.
Requirements
Setup the acquisition (optional)
Same as in part one, in this article, I’m running the CrowdSec Security Engine in replay mode. If you want to use service mode, you need to set up the acquisition.
#/etc/crowdsec/acquis.yaml
---
filenames:
- /var/log/auth.log
labels:
type: syslog
---
Install the SSHD collection
You can find the collection here.
sudo cscli collections install crowdsecurity/sshd
Parse successful authentication
As already seen in part one, this is the parser node to parse successful SSH authentication:
#/etc/crowdsec/parsers/s01-parse/sshd-logs.yaml
---
- grok:
pattern: 'Accepted password for %{USERNAME:sshd_valid_user} from %{IP_WORKAROUND:sshd_client_ip} port d+'
apply_on: message
statics:
- meta: log_type
value: ssh_success-auth
- meta: target_user
expression: "evt.Parsed.sshd_valid_user"
Enrich event with IP geolocation
To detect that an account connected from two different locations in a short amount of time, we need information on the geolocation of the source IP address.
We can get this info by installing the geoip-enrich parser.
sudo cscli parsers install crowdsecurity/geoip-enrich
Note: You can skip this step if you have already installed this parser.
The scenario
Now we need to check that the IPs filled in our buckets are more than 1000km away from each other.
Here is the condition that we will use for our conditional bucket:
condition: |
len(queue.Queue) >= 2
and Distance(queue.Queue[-1].Enriched.Latitude, queue.Queue[-1].Enriched.Longitude,
queue.Queue[-2].Enriched.Latitude, queue.Queue[-2].Enriched.Longitude) > 1000
This condition checks that the buckets contain at least two events and that the distance between the IPs in those events is more than 1000km away. We can achieve that using the Distance() helper, which uses the Haversine formula to determine the distance between two points with their longitude and latitude.
Here is the full scenario that detects an account connecting from more than 1000km away since its last connection in 4 hours:
#/etc/crowdsec/scenarios/impossible-travel.yaml
---
type: conditional
name: crowdsecurity/impossible-travel
description: "Detect impossible travel"
filter: "evt.Meta.log_type == 'ssh_success-auth'"
leakspeed: "4h"
distinct: evt.Meta.source_ip
capacity: -1
condition: |
len(queue.Queue) >= 2
and Distance(queue.Queue[-1].Enriched.Latitude, queue.Queue[-1].Enriched.Longitude,
queue.Queue[-2].Enriched.Latitude, queue.Queue[-2].Enriched.Longitude) > 1000
groupby: evt.Meta.target_user
blackhole: 1m
labels:
service: ssh
type: bruteforce
remediation: true
scope:
type: user
expression: evt.Meta.target_user
To make this scenario work, we must group the events by targeted user and only look at distinct IP addresses in the bucket.
Note: we set user as the scope for this scenario. If your profile configuration generates a decision only for alert with scope IP, triggering this scenario will not lead to any decision.
Bonus: Add context to the alert
With this scenario, the CrowdSec Security Engine will only log the user targeted by the attack. But it is also interesting to know from which IPs and countries the user logged in. To do that, I will use the new CrowdSec feature, called Alert Context. See more about this feature here.
To add the source IP addresses and the source countries, we need to run the following commands:
sudo cscli lapi context add --key source_ip --value evt.Meta.source_ip
sudo cscli lapi context add --key country --value evt.Enriched.IsoCode
When you inspect an alert, you will be able to see all the source IPs and countries that have been poured into the bucket.
Test with CrowdSec replay mode
We can test this scenario with the CrowdSec replay mode to ensure everything works properly.
Here are some logs that represent two successful authentications from two different IPs, in 2 different countries, and in 2 hours intervals:
#ssh_auth.log
---
Apr 11 15:53:54 myvps sshd[835241]: Accepted password for root from 1.2.3.4 port 1042 ssh2
Apr 11 17:53:54 myvps sshd[835241]: Accepted password for root from 8.8.8.8 port 1042 ssh2
We can run the CrowdSec replay mode with the following command:
#$ sudo crowdsec -dsn file://ssh_auth.log -type syslog -no-api
---
INFO[14-04-2023 18:14:24] single file mode : log_media=stdout daemonize=false
INFO[14-04-2023 18:14:24] Enabled feature flags: [none]
INFO[14-04-2023 18:14:24] Crowdsec v1.5.0-rc3-6-g169b8442-linux-169b84421227a4a2cab1c680863df958862bc615
WARN[14-04-2023 18:14:24] MaxOpenConns is 0, defaulting to 100
INFO[14-04-2023 18:14:24] Loading prometheus collectors
WARN[14-04-2023 18:14:24] Exprhelpers loaded without database client.
INFO[14-04-2023 18:14:24] Loading grok library /etc/crowdsec/patterns
INFO[14-04-2023 18:14:25] Loading enrich plugins
INFO[14-04-2023 18:14:25] Successfully registered enricher 'GeoIpCity'
INFO[14-04-2023 18:14:25] Successfully registered enricher 'GeoIpASN'
INFO[14-04-2023 18:14:25] Successfully registered enricher 'IpToRange'
INFO[14-04-2023 18:14:25] Successfully registered enricher 'reverse_dns'
INFO[14-04-2023 18:14:25] Successfully registered enricher 'ParseDate'
INFO[14-04-2023 18:14:25] Successfully registered enricher 'UnmarshalJSON'
INFO[14-04-2023 18:14:25] Loading parsers from 5 files
INFO[14-04-2023 18:14:25] Loaded 2 parser nodes file=/etc/crowdsec/parsers/s00-raw/syslog-logs.yaml stage=s00-raw
INFO[14-04-2023 18:14:25] Loaded 1 parser nodes file=/etc/crowdsec/parsers/s01-parse/sshd-logs.yaml stage=s01-parse
INFO[14-04-2023 18:14:25] Loaded 1 parser nodes file=/etc/crowdsec/parsers/s02-enrich/dateparse-enrich.yaml stage=s02-enrich
INFO[14-04-2023 18:14:25] Loaded 1 parser nodes
…
INFO[14-04-2023 18:14:26] user root performed 'crowdsecurity/impossible-travel' (2 events over 2h0m0s) at 2023-04-11 17:53:54 +0000 UTC
Tada! The impossible travel from the user root has been detected by CrowdSec!
Demo
To test this scenario in real life, we will connect to our server via SSH from France for the first time, and then, a few minutes later, we will use a VPN to connect to our server from Spain.
Note: First, don’t forget to restart CrowdSec since we have edited some parsers and scenarios and added context to alerts.
So, let’s connect for the first time on our server with the demo account:
ssh demo@[DEMO_IP]
Then we use our VPN to connect to the server in Spain:
VPN: Connected
Current network: [DEMO_NETWORK]
Gateway: Spain
And we connect again to our server with the same command.
ssh demo@[DEMO_IP]
The CrowdSec Security Engine has successfully detected this impossible travel:
time="25-04-2023 14:59:37" level=info msg="user demo performed 'crowdsecurity/impossible-travel' (2 events over 52.655606946s) at 2023-04-25 14:59:37.249468308 +0000 UTC"
time="25-04-2023 14:59:38" level=info msg="(ff7bfc7d3d8043188b90b8ca80edd29cEo2BtePh5aKyvESP) alert : crowdsecurity/impossible-travel by user demo"
With the Alert Context feature that we enabled before, we can see the two IP addresses and countries in the alert:
sudo cscli alerts inspect 15107
################################################################################################
- ID : 15107
- Date : 2023-04-25T14:59:38Z
- Machine : [machine_id]
- Simulation : false
- Reason : crowdsecurity/impossible-travel
- Events Count : 2
- Scope:Value : user:demo
- Country :
- AS :
- Begin : 2023-04-25 14:58:44.593879707 +0000 UTC
- End : 2023-04-25 14:59:37.249486653 +0000 UTC
- UUID : 1f89849e-94d5-490c-9bde-ea9ffaa171f3
- Context :
╭───────────┬────────────────╮
│ Key │ Value │
├───────────┼────────────────┤
│ country │ FR │
│ country │ ES │
│ source_ip │ [demo_ip] │
│ source_ip │ [vpn_ip] │
╰───────────┴────────────────╯
Summing up
The conditional feature in CrowdSec Security Engine 1.5 is a powerful tool!
We already saw how to use it in detecting successful brute force attacks and impossible travel.
But did you know you can also use the conditional feature to detect post-exploitation? Come back to read part three of CrowdSec Security Engine 1.5 in Action series to learn how to do it.