Pchełki Python: lokalizujemy piratów

https://xpil.eu/e0m

Kilka dni temu włączyłem na swoim publicznym adresie IP usługę SSH na porcie 22 - rzecz jasna prawie natychmiast zaczęły mi się tam dobijać tłumy losowego chakierstwa i innego piractwa. Ponieważ port ów mam otwarty na swoim NAS-ie (przez ruter dostawcy, na którym zrobiłem mapowanie portu 22 z adresu publicznego na lokalny), za każdym razem kiedy ktoś próbuje się zalogować NAS wysyła mi powiadomienie na e-mail.

Tak naprawdę powiadomienie przychodzi tylko wtedy, gdy ktoś spróbuje się zalogować z danego adresu IP więcej niż raz i zostaje zablokowany.

Póki co liczba tych e-maili nie przytłacza (średnio 3-4 maile na godzinę, pół biedy). Ponadto nie martwi mnie też zbytnio sam fakt otwartego portu, bo użytkownika domyślnego wyłączyłem już dawno, zamiast tego utworzyłem innego z dość losową i trudną do zgadnięcia nazwą więc mogę spać spokojnie.

Zachciało mi się sprawdzić skąd się owi chakierzy próbują do mnie dobijać.

Dla pojedynczego adresu IP sprawa jest prosta: kopiuję, wklejam na whatismyipaddress.com i dostaję pierdylion informacji.

Ale żeby to zrobić en-masse, trzeba już pokombinować...

Skoro za każdym razem dostaję powiadomienie na e-mail (którego nie kasuję tylko wrzucam do archiwum Gmaila), to powinno się dać wyciągnąć te informacje automatycznie, ze skrzynki pocztowej, nieprawdaż? Tym bardziej, że zablokowany adres IP pojawia się w tytule wiadomości co powinno ułatwić sprawę.

W UI Gmaila wygląda to mniej więcej tak:

Jak widać każde powiadomienie przychodzi dwukrotnie (chyba dlatego, że na tym NAS-ie mam dodatkowo software do obsługi kamer, który wykrywa próby włamu do ssh i powiadamia e-mailem niezależnie od samego OS-a). Gdyby tak odfiltrować te wiadomości i wyłuskać z nich same adresy IP... Hmmm.

Instalujemy niezbędne biblioteki...

pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

... i lecimy z koksem:

import os
import pickle
import re

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

SCOPES = ['https://mail.google.com/']

def gmail_authenticate():
    creds = None
    if os.path.exists("pygmsearch/token.pickle"):
        with open("pygmsearch/token.pickle", "rb") as token:
            creds = pickle.load(token)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'pygmsearch/credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open("pygmsearch/token.pickle", "wb") as token:
            pickle.dump(creds, token)
    return build('gmail', 'v1', credentials=creds)


def search_messages(service, query):
    result = service.users().messages().list(userId='me', q=query).execute()
    messages = []
    if 'messages' in result:
        messages.extend(result['messages'])
    while 'nextPageToken' in result:
        page_token = result['nextPageToken']
        result = service.users().messages().list(
            userId='me', q=query, pageToken=page_token).execute()
        if 'messages' in result:
            messages.extend(result['messages'])
    return messages


service = gmail_authenticate()
messages = search_messages(service, "subject: zablokowany")

result = []

for message in messages:
    msg = service.users().messages().get(userId='me', id=message['id'], format='full').execute()
    headers = msg['payload']['headers']
    for h in headers:
        if(h['name'] == 'Subject'):
            if(h['value']).startswith('Adres IP'):
                v = h['value']
                ips = re.findall(r'\[(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]', v)
                if(ips):
                    for ip in ips:
                        result.append(ip.translate(str.maketrans('','','[]')))

result = list(dict.fromkeys(result))
print(result)

Żeby powyższy kod miał szansę w ogóle wystartować, msimy najpierw włączyć API Gmail na naszym koncie (jak to zrobić? tu jest dość dobrze opisane: https://support.google.com/googleapi/answer/6158841?hl=en) a potem jeszcze jednorazowo (czyli przy pierwszym uruchomieniu skryptu) potwierdzić w przeglądarce, że faktycznie to my próbujemy się zalogować do naszej skrzynki pocztowej. Po pierwszym zalogowaniu się skrypt zapisze token w lokalnym pliku token.pickle i następnym razem połączy się już automatycznie.

Uruchamiamy skrypt, czekamy około 3-4 minut (komunikacja z serwerami Google jest szybka, ale bez przesady...) i dostajemy na wyjściu listę adresów IP, które zostały zablokowane na naszym SSH. To jeszcze nie jest ostateczny wynik (nadal nie wiemy *skąd* dranie się do nas próbują włamać), ale od czegoś trzeba zacząć.

Zanim przejdę do kolejnego etapu, omówię teraz co ciekawsze kawałki powyższego kodu:

Funkcje gmail_authenticate oraz search_messages zostały żywcem zerżnięte z tej strony. Jedyne, co możesz chcieć tu zmienić to ścieżka dostępu do pliku token.pickle - w oryginale nie było tam żadnego przedrostka, ja dodałem bo mi tak pasuje.

service = gmail_authenticate() - tutaj logujemy się do Gmaila (za pierwszym razem otworzy się tu domyślna przeglądarka www i zapyta o potwierdzenie)

messages = search_messages(service, "subject: zablokowany") - tu z kolei wyszukujemy wśród e-maili wszystkie, których tytuł zawiera słowo "zablokowany". Można zamiast tego wyszukać na przykład "został zablokowany przez" - w sumie wsioryba, byle wybrać taki tekst, który jednoznacznie odfiltruje szukane przez nas wiadomości.

search_messages zwraca tak naprawdę wyłącznie listę *identyfikatorów* wiadomości pasujących do szukanego tekstu, więc potem trzeba w pętli przelecieć się po tej liście i pościągać te wiadomości po czym powydłubywać z ich tytułów adresy ip. Tu z pomocą przychodzą wyrażenia regularne - adres IP znajdujemy za pomocą '\[(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]' czyli najpierw otwarty nawias kwadratowy, potem trzy grupy liczb jedno-, dwu- lub trzycyfrowych z kropką, i na koniec jeszcze jedna taka grupa ale bez kropki zakończona zamykającym nawiasem kwadratowym.

ip.translate(str.maketrans('','','[]')) usuwa z wyników nawiasy kwadratowe. W Pythonie 2.x było to dużo prostsze, wystarczyło zrobić ip.translate(None, '[]') ale w trójce namieszali i trzeba dookoła - ale dzięki temu jest ponoć szybciej i bardziej uniwersalnie.

Na koniec usuwamy duplikaty i wyświetlamy wyniki, czyli zwykłą listę adresów IP.

Ale, ale. Co nam po adresach? Przecież postawione zadanie brzmiało: skąd się te ciule łyse do nas dobijają?

W tym celu skorzystamy z biblioteki geoip-lite czyli darmowej wersji bazy danych geoip, która potrafi przetłumaczyć adres IP na jego lokalizację.

Kiedyś był to osobny, niezależny projekt, ale potem kupiła ich firma Maxmind i teraz można wykupić dostęp do bazy adresów IP - na szczęście ciągle istnieje wersja darmowa której jedynymi wadami są mniejsza częstotliwość aktualizacji danych (około raz na dwa tygodnie) oraz dokładność samej lokalizacji. Dla naszych potrzeb - w zupełności wystarczy.

Zaczynamy od zainstalowania odpowiedniej biblioteki:

pip install maxminddb-geolite2

A potem już samo gęste:

from geolite2 import geolite2

adresy_ip = ['31.184.198.71', '61.177.173.42', '61.177.173.37', '61.177.172.19', '61.177.172.114', '61.177.172.108', '61.177.172.104', '61.177.173.43', '61.177.173.56', '183.100.29.185', '125.17.153.207', '61.177.172.61', '61.177.172.90', '61.177.172.76', '61.177.173.54', '61.177.172.98', '61.177.173.52', '61.177.173.46', '61.177.172.160', '36.110.228.254', '61.177.173.55', '61.177.172.124', '218.92.0.221', '61.177.172.87', '61.177.173.61', '67.253.48.250', '171.25.193.235', '162.247.74.74', '194.180.48.55', '18.116.202.246', '20.239.196.60', '81.70.142.138', '188.164.167.192', '49.51.19.119', '36.112.171.51', '171.225.184.81', '141.98.11.57', '172.247.194.147', '180.75.5.30', '179.43.140.246', '144.126.217.217', '221.234.230.12', '219.128.12.190', '45.133.38.41', '203.154.158.157', '138.19.142.37', '61.177.173.53', '61.177.173.39', '185.238.36.24', '45.94.142.14', '165.22.67.75', '112.85.42.88', '164.92.207.214', '64.225.103.120', '112.85.42.89', '159.223.23.82', '112.85.42.71', '69.49.229.187', '222.186.30.112', '112.85.42.87', '211.36.141.195', '112.85.42.124', '61.177.172.89', '112.85.42.227', '112.85.42.73', '122.194.229.38', '112.85.42.74', '122.194.229.37', '112.85.42.15', '61.177.172.154', '157.230.120.129', '172.104.144.235', '122.194.229.54', '112.85.42.128', '222.186.42.13', '222.186.30.76', '221.131.165.50', '185.246.130.20', '221.131.165.65', '222.186.42.7', '221.181.185.151', '221.131.165.33', '96.9.67.48', '221.131.165.75', '58.222.83.94', '221.131.165.56', '221.181.185.159', '222.186.180.130', '222.187.232.39', '221.181.185.94', '222.187.254.41', '216.235.12.116', '114.240.224.140', '222.187.238.58', '221.181.185.111', '134.209.83.158', '222.186.42.137', '188.166.60.8', '24.221.224.89', '188.166.24.204', '74.81.30.208', '83.252.70.228', '221.131.165.62', '222.179.101.18', '89.106.23.3', '77.237.73.26', '24.54.148.227', '204.191.196.151', '179.213.34.231', '208.89.176.104', '136.144.41.139', '141.98.10.246', '52.188.213.193', '209.141.42.136', '2.183.55.15', '164.68.105.148', '45.141.84.126', '119.45.167.35', '205.185.122.230', '209.141.41.46', '205.185.125.72', '101.42.107.102', '156.205.108.157', '167.71.10.210', '121.169.34.119', '104.167.223.21', '20.206.109.196', '198.98.59.119', '209.141.44.236', '167.71.2.44', '167.71.12.34', '117.248.249.70', '222.187.237.11', '45.61.186.123', '20.102.80.72', '2.57.122.192', '95.217.33.238', '198.98.50.237', '164.90.203.66', '101.34.244.235', '60.170.247.162', '20.104.53.82', '157.90.104.189', '219.147.221.123', '209.141.51.224', '67.83.0.255', '20.48.236.70', '209.141.37.136', '123.57.37.19']

reader = geolite2.reader()
for ip in adresy_ip:
    loc = reader.get(ip)
    if(loc):
        print(loc['country']['names']['en'])
    else:
        print("!", ip, "!")

Żeby niepotrzebnie nie obciążać serwerów Ggmaila (a także nie czekać za każdym razem po 3-4 minuty na wyniki), listę adresów IP skopiowałem z wyników poprzedniego skryptu. Dodatkowy warunek if(loc) jest niezbędny ponieważ nie wszystkie adresy IP znajdują się w bazie geoip i bez tego skrypt beknie.

Na wyjściu dostajemy:

Russia
China
China
China
China
China
China
China
China
Republic of Korea
India
China
China
China
China
China
China
China
China
China
China
China
China
China
China
United States
Sweden
United States
Germany
United States
United States
Netherlands
Russia
Canada
China
Vietnam
! 141.98.11.57 !
United States
Malaysia
Switzerland
United States
China
China
! 45.133.38.41 !
Thailand
Hong Kong
China
China
France
! 45.94.142.14 !
United States
China
United States
United States
China
United States
China
United States
China
China
Republic of Korea
China
China
China
China
China
China
China
China
China
United States
Germany
China
China
China
China
China
Sweden
China
China
China
China
Cambodia
China
China
China
China
China
China
China
China
Canada
China
China
China
United States
China
Netherlands
United States
Netherlands
United States
Sweden
China
China
Turkey
Iran
United States
Canada
Brazil
United States
United States
! 141.98.10.246 !
United States
United States
Iran
United States
! 45.141.84.126 !
China
United States
United States
United States
China
Egypt
United States
Republic of Korea
Mali
United States
United States
United States
United States
United States
India
China
United States
United States
! 2.57.122.192 !
Finland
United States
United States
China
China
United States
United States
China
United States
United States
United States
United States
China

Jak widać przeważają chakieży z Chin i (o dziwo) Ameryki. Sprawdźmy konkretne liczby:

from geolite2 import geolite2

from collections import Counter
from pprint import pprint

adresy_ip = ['31.184.198.71', '61.177.173.42', '61.177.173.37', '61.177.172.19', '61.177.172.114', '61.177.172.108', '61.177.172.104', '61.177.173.43', '61.177.173.56', '183.100.29.185', '125.17.153.207', '61.177.172.61', '61.177.172.90', '61.177.172.76', '61.177.173.54', '61.177.172.98', '61.177.173.52', '61.177.173.46', '61.177.172.160', '36.110.228.254', '61.177.173.55', '61.177.172.124', '218.92.0.221', '61.177.172.87', '61.177.173.61', '67.253.48.250', '171.25.193.235', '162.247.74.74', '194.180.48.55', '18.116.202.246', '20.239.196.60', '81.70.142.138', '188.164.167.192', '49.51.19.119', '36.112.171.51', '171.225.184.81', '141.98.11.57', '172.247.194.147', '180.75.5.30', '179.43.140.246', '144.126.217.217', '221.234.230.12', '219.128.12.190', '45.133.38.41', '203.154.158.157', '138.19.142.37', '61.177.173.53', '61.177.173.39', '185.238.36.24', '45.94.142.14', '165.22.67.75', '112.85.42.88', '164.92.207.214', '64.225.103.120', '112.85.42.89', '159.223.23.82', '112.85.42.71', '69.49.229.187', '222.186.30.112', '112.85.42.87', '211.36.141.195', '112.85.42.124', '61.177.172.89', '112.85.42.227', '112.85.42.73', '122.194.229.38', '112.85.42.74', '122.194.229.37', '112.85.42.15', '61.177.172.154', '157.230.120.129', '172.104.144.235', '122.194.229.54', '112.85.42.128', '222.186.42.13', '222.186.30.76', '221.131.165.50', '185.246.130.20', '221.131.165.65', '222.186.42.7', '221.181.185.151', '221.131.165.33', '96.9.67.48', '221.131.165.75', '58.222.83.94', '221.131.165.56', '221.181.185.159', '222.186.180.130', '222.187.232.39', '221.181.185.94', '222.187.254.41', '216.235.12.116', '114.240.224.140', '222.187.238.58', '221.181.185.111', '134.209.83.158', '222.186.42.137', '188.166.60.8', '24.221.224.89', '188.166.24.204', '74.81.30.208', '83.252.70.228', '221.131.165.62', '222.179.101.18', '89.106.23.3', '77.237.73.26', '24.54.148.227', '204.191.196.151', '179.213.34.231', '208.89.176.104', '136.144.41.139', '141.98.10.246', '52.188.213.193', '209.141.42.136', '2.183.55.15', '164.68.105.148', '45.141.84.126', '119.45.167.35', '205.185.122.230', '209.141.41.46', '205.185.125.72', '101.42.107.102', '156.205.108.157', '167.71.10.210', '121.169.34.119', '104.167.223.21', '20.206.109.196', '198.98.59.119', '209.141.44.236', '167.71.2.44', '167.71.12.34', '117.248.249.70', '222.187.237.11', '45.61.186.123', '20.102.80.72', '2.57.122.192', '95.217.33.238', '198.98.50.237', '164.90.203.66', '101.34.244.235', '60.170.247.162', '20.104.53.82', '157.90.104.189', '219.147.221.123', '209.141.51.224', '67.83.0.255', '20.48.236.70', '209.141.37.136', '123.57.37.19']

kraje = []
reader = geolite2.reader()
for ip in adresy_ip:
    loc = reader.get(ip)
    if(loc):
        kraje.append(loc['country']['names']['en'])
policzone = Counter(kraje)
pprint(policzone)

Wynik:

'China': 71,
'United States': 40,
'Republic of Korea': 3,
'Sweden': 3,
'Netherlands': 3,
'Canada': 3,
'Russia': 2,
'India': 2,
'Germany': 2,
'Iran': 2,
'Vietnam': 1,
'Malaysia': 1,
'Switzerland': 1,
'Thailand': 1,
'Hong Kong': 1,
'France': 1,
'Cambodia': 1,
'Turkey': 1,
'Brazil': 1,
'Egypt': 1,
'Mali': 1,
'Finland': 1

Ponieważ nie wybieram się w najbliższym czasie ani do Chin, ani do USA, rozważam całkowite zablokowanie dostępu z tych dwóch krajów.

https://xpil.eu/e0m

1 Comment

  1. Obok głównego tematu wpisu, ale jednak nieco związane. Polecam przeniesienie SSH (w zasadzie w Twoim przypadku zmianę przekierowywanego portu na routerze) z portu 22 na powiedzmy 40. Zabezpieczenie to oczywiście żadne, ale powinno elegancko odsiać skany SSH z losowych automatów.

Skomentuj rozie Anuluj pisanie odpowiedzi

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.