Hunting for badguys|flvbox.org
1. Hunting for badguys
1.1. What is a badguy
The badguy is a host from which SSH authentication requests are directed to my server flvbox.org.
These hosts are often consumer router boxes running malicious software injected without the owner's knowledge. Other times, they are outdated Linux hosts exposing various services from NFS to MySQL, which I also presume are running malicious software without the owner's knowledge.
I discovered this activity by monitoring the /var/log/authlog
file and observing a high
volume of activity such as:
Oct 1 14:42:51 flvbox sshd[11294]: Invalid user lenovo from 178.128.38.217 port 56132 Oct 1 14:42:51 flvbox sshd[11294]: Connection closed by invalid user lenovo 178.128.38.217 port 56132 [preauth]
Figure 1: /var/log/authlog
1.2. Blocking badguys
In order to deny all form of communication to badguys I've created a simple shell
script that collect badguys from /var/log/authlog
and add these IPs to /etc/badguys
file every 2 minutes (holy cron).
Figure 2: Badguys API detail
I found these three main cases
cp /etc/badguys /tmp/badguys.bak echo "1. case -Invalid user-" cat /var/log/authlog | grep "Invalid user" | awk '{print $(NF-2)}' | sort | uniq > /tmp/bad echo "2. case -Disconnected from authenticating user root-" cat /var/log/authlog | grep "Disconnected from authenticating user root" | awk '{print $(NF-3)}' | sort | uniq >> /tmp/bad echo "3. case -kex_exchange_identification-" grep -A 1 "error: kex_exchange_identification:" /var/log/authlog | grep "reset by" | grep "port" | awk '{print $(NF-2)}' | sort | uniq >> /tmp/bad cat /tmp/bad /tmp/badguys.bak | sort | uniq > /etc/badguys && rm /tmp/bad /tmp/badguys.bak
Ok, let's the firewall handle it
In my case server OS is OpenBSD and the firewall adopted is pf from OpenBSD base system.
In order to prevent network activity with badguys I've added these lines to /etc/pf.conf
# Regole per badguys table <badhosts> persist file "/etc/badguys" block on $if from <badhosts> to any
1.3. Alerting
Using the flv_telegram.py
Python class, I will receive a Telegram message with all the information from the badguy table in the database, for each bad guy.
Figure 3: Telegram notifications
1.4. Analysis
Often these automated attacks try to test a series of vulnerabilities and repeatedly attempt to gain access (especially via SSH) to acquire computing power for mining cryptocurrencies like Monero. So there's someone in the world who doesn't have a life and creates botnets to mine cryptocurrencies using computing power fraudulently acquired from every corner of the planet. The world was so happy before this that we really needed it.
This is how I tried to see where these badguys come from
First a Python script that merge IPs taken from /etc/badguys
file with geo
data (API call to http://ip-api.com/json/{ip}
) into a sqlite database.
# This function insert a new record IP (PK),timestamp, latitude, longitude, country_code, provider, regionName, country # into sqlite database, call after get_geolocation(ip) that's retrieve values def insert_ip_data( conn, ip, timestamp, latitude, longitude, country_code, provider, regionName, country, ): cursor = conn.cursor() cursor.execute( """ INSERT OR IGNORE INTO ip_data (IP_ADDRESS, timestamp, latitude, longitude, country_code, provider, regionName, country) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( ip, timestamp, latitude, longitude, country_code, provider, regionName, country, ), ) conn.commit() # API call to ip-api.com return geo data for each ip, call after ip_exists_in_db(conn, ip) in order # to reduce useless API call # NB use time.sleep(1.5) in order to respect API requests limit (45 call/minute) def get_geolocation(ip): url = f"http://ip-api.com/json/{ip}" response = requests.get(url) data = response.json() if data["status"] == "success": return ( data["lat"], data["lon"], data["countryCode"], data.get("as", "Unknown"), data["regionName"], data["country"], ) return None, None, None, None, None, None # Simple method in order to verify the existance of an IP on sqlite database def ip_exists_in_db(conn, ip): cursor = conn.cursor() cursor.execute("SELECT 1 FROM ip_data WHERE IP_ADDRESS = ?", (ip,)) return cursor.fetchone() is not None
And a mandatory streamlit dashboard in order to visualize counts and geographic data
Figure 4: Badguys dashboard
Figure 5: Badguys count over time
Figure 6: Badguys count per nation (pie)
Figure 7: Badguys count per ISP
Figure 8: Badguys map detailed view