furas.pl
# prywatne notatki - Python, Linux, Machine Learning, etc.

Wyciek Collection #1 - przegląd domen z użyciem Pythona

Wstęp

Około 16 stycznia 2019 (czyli z miesiąc przed tym wpisem) pojawiły się informacje o wielkim wycieku haseł zwanym Collection #1. Nazwano go tak od nazwy katalog na portalu MEGA, który zawierał około 3000 plików z hasłami do różnych portali.

Całość chyba dość szybko znikła z MEGA - choć na pewnych portalach pojawiła się informacja o ponownym dostępie do Collection #1-5

Pierwsze informacje o wycieku podał Troy Hunt we wpisie The 773 Million Record "Collection #1" Data Breach. Autor jest twórcą portalu Have I Been Pwned, który zbiera dane z różnych wycieków i pozwala sprawdzić czy nasz email pojawił się w jakimś z takich wycieków. Pozwala też zapisać się aby otrzymywać powiadomienia gdy nasz email pojawi się w przyszłych wyciekach.

Tak więc dostęp do wszystkich danych jest trudny ale za to na PasteBin jest dostępna lista plików w Collection #1 i do tego można ją ściągnąć w postaci czystego tekstu (RAW) więc za pomocą Pythona można ją łatwo pobrać i przejrzeć nazwy domen występujące w nazwach plików.

W ten sposób można sprawdzić jakie polskie domeny zostały "skompromitowane" lub z jakich krajów najwięcej domen pojawia się na liście.

Przegląd listy z użyciem Pythona

Krótki skrypt z użyciem standardowego modułu urllib.request pozwala pobrać i wyświetlić całą listę. Nie trzeba do tego instalować ani requests ani beautifulsoup lub lxml do

import urllib.request

url = 'https://pastebin.com/raw/UsxU4gXA'

text = urllib.request.urlopen(url).read().decode()

print(text)
-rwxrwxrwx   33405108 Jan 22  2016 000webhost.txt
-rwxrwxrwx     165025 Jul 29  2017 01nii.ru {1.931} [HASH].txt
-rwxrwxrwx    1157635 Jun 17  2018 048235631.com {20.677} [HASH+NOHASH].txt
-rwxrwxrwx      37117 Jun 24  2018 0933779000.com {1.156} [HASH] [NOHASH].txt
-rwxrwxrwx     507968 Okt  1  2017 10th.holdgold.ru {9.283} [HASH].txt
-rwxrwxrwx     300417 Mär 25  2018 116.125.120.57 {5.612} [HASH].txt
-rwxrwxrwx    1396742 Jan 27  2018 124sold.gr {26.361} [HASH].txt
-rwxrwxrwx      86998 Apr 15  2018 1452arte.com {2.826} [NOHASH].txt
-rwxrwxrwx    1461800 Dez  4  2017 14.63.169.59 {39.634} [HASH].txt
-rwxrwxrwx     216683 Dez  2  2017 163.24.21.143 {3.645} [HASH].txt
-rwxrwxrwx     193992 Feb  3  2018 173.221.254.15 {4.923} [HASH].txt
-rwxrwxrwx     506613 Nov  9  2017 1924.holdgold.ru {9.256} [HASH].txt
-rwxrwxrwx     785272 Jul 26  2017 1.html
-rwxrwxrwx     139565 Nov  4  2017 1shopram.ga {2.456} [HASH].txt

Tekst dzielę na wiersze i w każdym wierszu wyrzucam początkowe 35 znaków, które uprawnienia, rozmiar i datę. Pozbywam się też roszerzenia .txt choć później doszedłem, że nie ma takiej potrzeby lub można by zrobić przegląd plików według rozszerzeń bo występują też inne rozszerzenia niż .txt.

# podział na wiersze
rows = text.split('\n')

# pozbycie się początku i rozszerzenia `.txt`
filenames = [item[35:].replace('.txt', '') for item in rows]

print('\n'.join(filenames))
000webhost
01nii.ru {1.931} [HASH]
048235631.com {20.677} [HASH+NOHASH]
0933779000.com {1.156} [HASH] [NOHASH]
10th.holdgold.ru {9.283} [HASH]
116.125.120.57 {5.612} [HASH]
124sold.gr {26.361} [HASH]
1452arte.com {2.826} [NOHASH]
14.63.169.59 {39.634} [HASH]
163.24.21.143 {3.645} [HASH]
173.221.254.15 {4.923} [HASH]
1924.holdgold.ru {9.256} [HASH]
1.html
1shopram.ga {2.456} [HASH]

Ponieważ wierszy jest dużo - co można sprawdzić za pomocą len(filenames) - więc dla podglądu tylko części danych można wykorzystać, że teraz to jest lista wierszy i użyć przedziału [poczatek:koniec]. Przykładowo wyświetlenie pierwszych 10 wierszy [:10] i ostatnich 10 wierszy [-10:]

print('\n'.join(filenames[:10]))

print('\n'.join(filenames[-10:]))

Przeglądając listę zauważyłem, że nie wszystkie nazwy plików zawierają nazwę domeny a te z domenami mają postać domena {liczba} [HASH] tak więc zostawie tylko te które mają { i do tego z nazwy zostawie tylko część występującą przed { czyli wszystko przed pierwszą spacją. To powinno dać mi same nazwy domen.

# pozostawienie nazw zawierających ` { `
filenames_filtered = [item for item in filenames if ' {' in item]

# domeny
domains = [item.split()[0] for item in filenames_filtered]

print('\n'.join(domains))

Teraz mogę wybrać domeny kończące się na .pl. W ten sam sposób można też sprawdzać inne końcówki lub występowanie jakiś ciągów wewnątrz nazwy np. .co.

# polskie domeny
domains_pl = [item for item in domains if item.endswith('.pl')]

print('\n'.join(domains_pl))
cad-akademia.pl
fotografia.interklasa.pl
gmina.e-sochaczew.pl
iceneo.pl
jest-lirycznie.pl
mojetesty.pl
nieruchomoscikwiatkowski.pl
rapido.com.pl
skaner.p.lodz.pl
www.centrumdruku.com.pl
www.djembe.pl
www.eis.com.pl
www.ekonomicznie.pl
www.teatrgombrowicza.art.pl

Zauważyłem, że kilka domen zawiera ciąg xn-- co oznacza, że są to nazwy z użyciem znaków narodowych.

# domeny ze znakami unicode xn--
domains_xn = [item for item in domains if 'xn--' in item]

print('\n'.join(domains_xn))

Za pomocą .decode('idna') można przekonwertować domeny z xn-- na znaki narodowe. Przy okazji zauważyłem, że jedna nazwa ma niepotrzebne http na początku nazwy - co nie pozwala na jej dekodowanie - więc go usuwam.

for item in domains_xn:
    item = item.replace('http', '')
    item_unicode = item.encode().decode('idna')
    print('{0:55} | {1}'.format(item, item_unicode))
xn--76-vlci2a3g.xn--p1ai                                | ярик76.рф
www.xn--13-6kc3bfr2e.xn--90anbvlob.xn--p1ai             | www.школа13.уоирбит.рф
www.xn----7sbajczhbabsdpgnsadrv0b8t.xn--p1ai            | www.магазин-мотоэкипировки.рф
www.xn--c1akva.xn--p1ai                                 | www.гипп.рф
www.xn--j1adjbfs.xn--p1ai                               | www.мпкпру.рф
xn--55qy60o.tw                                          | 公雞.tw
xn--5-8sbgm4byg.com.ua                                  | 5авеню.com.ua
xn--72c6aal6b6cub7c1c1e.ga                              | รองเท้าบูท.ga
xn--76-vlci2a3g.xn--p1ai                                | ярик76.рф
xn----7sbabarxgef8afscas9amg1h.xn--p1ai                 | мотозапчасти-магазин.рф
xn--80aiggch7bar.xn-----8kciivmbg1bdd2adjgh9n.xn--p1ai  | картриджи.купить-с-доставкой.рф
xn--80asehdb.xn--80aeiluelyj.xn--p1ai                   | онлайн.анимевост.рф
xn--90aacmb0bd4a8i.xn--p1ai                             | брвмебель.рф
xn--90aacmb0bd4a8i.xn--p1ai                             | брвмебель.рф
xn--b1aabe4awacbof.xn--p1ai                             | городковров.рф
xn--b3cupa4b7cm1eua.tk                                  | ชุดนอนหมี.tk
xn--www-2ddamtijqp.vetlek.ru                            | синоптикwww.vetlek.ru

Jak wspomniałem wcześniej możemy użyć len() do sprawdzenia ile jest poszczególnych domen

print('ilość wszystkich nazw :', len(filenames))           # 2890
print('ilość po odfiltrowaniu:', len(filenames_filtered))  # 2701
print('ilość z .pl           :', len(domains_pl))      # 14
print('ilość z xn--          :', len(domains_xn))      # 17
ilość wszystkich nazw : 2890
ilość po odfiltrowaniu: 2701
ilość z .pl           : 14
ilość z xn--          : 17

Wśród nazw plików zauważyłem też takie, które nie zawierają nazwy domeny ale powtarzające się słowa jak accounts czy .mega. Zainteresowało mnie też w ilu nazwach występuje [NOHASH]

# (dla nazw plików przed filtrowaniem i obcięciem do samej domeny)
filenames_accounts = [item for item in filenames if 'accounts' in item]
filenames_mega     = [item for item in filenames if '.mega' in item]
filenames_nohash   = [item for item in filenames if '[NOHASH]' in item]

print('ilość z accounts:', len(filenames_accounts))
print('ilość z .mega   :', len(filenames_mega))
print('ilość z [NOHASH]:', len(filenames_nohash))
ilość z accounts: 32
ilość z .mega   : 9
ilość z [NOHASH]: 843

Na koniec postanowiłem policzyć ile razy występują poszczególne domeny najwyższego poziomu (TLD - Top Level Domain) więc do tego postanowiłem użyć klasy Counter ze standarowego modułu collections

from collections import Counter

domains_TLD = [item.split('.')[-1] for item in domains]

domains_TLD_count = Counter(domains_TLD)

for key, value in domains_TLD_count.most_common():
    print(' {:10} | {}'.format(key, value))

Początek wyniku

 com        | 953
 ru         | 313
 net        | 140
 de         | 116
 org        | 109
 uk         | 96
 br         | 67
 it         | 52
 cz         | 51
 kr         | 51
 info       | 45
 hu         | 43
 jp         | 38
 ua         | 37
 in         | 36
 au         | 31
 th         | 29
 fr         | 28
 tw         | 23
 eu         | 21
 ro         | 21
 ca         | 17
 ar         | 15
 be         | 14
 pl         | 14
 by         | 13
 az         | 13
 biz        | 13
 za         | 12
 sk         | 12
 xn--p1ai   | 12
 pt         | 11
 es         | 11
 nl         | 11

Sprawdzenie NameServer z użyciem Pythona

W komentarzach do kilku polskich artykułów na temat wycieku przeczytałem, że na liście są polskie strony, które nie mają końcówki .pl. Postanowiłem więc sprawdzić z jakich NameServerów korzytają domeny licząc, że polskie strony będą raczej korzystać z polskich hostingów, które mają .pl w nazwie. Choć to nie musi zawsze być prawda bo dostępny w polsce OVH.pl wykorzystuje ovh.net

Do tego celu doinstalowałem moduł dnspython

pip install dnspython

Przykład użycia

import dns.resolver

domain = 'onet.pl'

for ns in dns.resolver.query(domain, 'NS'):
    print(ns)

i wynik

dns3.onet.pl.
ns1.ikp.pl.
dns2.onet.pl.
dns1.onet.pl.

W trakcie sprawdzania pojawiały mi się wyjątki

  • dns.resolver.NoAnswer gdy sprawdzana była subdomena więc trzeba było obciąć pierwszy człon i spróbować ponownie,
  • dns.resolver.NXDOMAIN lub dns.resolver.NoNameservers gdy domena już nie istniała,
  • dns.resolver.Timeout gdy serwer nie odpowiedział na czas.
import dns.resolver

domain = 'onet.pl'

try:
    all_ns = [str(ns) for ns in dns.resolver.query(domain, 'NS')]
    print(all_ns)
except dns.resolver.NoAnswer as ex:
    print('[DEBUG] dns.resolver.NoAnswer:', ex)

    # to może być subdomena więc obcinamy pierwszy człon i sprawdzamy ponownie
    # o ile nie zostanie tylko ostatni człon czyli TLD

    parts = domain.split('.')

    if len(parts) > 2:
        subdomain = '.'.join(parts[1:])
        try:
            all_ns = [str(ns) for ns in dns.resolver.query(subdomain, 'NS')]
            print(all_ns)
        except Exception as ex:
            print('[DEBUG] Exception:', ex)
except dns.resolver.NXDOMAIN as ex:
    print('[DEBUG] dns.resolver.NXDOMAIN:', ex)
except dns.resolver.NoNameservers as ex:
    print('[DEBUG] dns.resolver.NoNameservers:', ex)
except dns.resolver.Timeout as ex:
    print('[DEBUG] dns.resolver.Timeout:', ex)

W ten sposób znalazłem następujące domeny z polskimi NameServerami:

falowniki.com
wsw.polskiearaby.com
www.dospelprofessional.com
www.fx-portal.com

indexcopernicus.com  # kilka subdomen

oraz jedną domenę .pl, która już nie istnieje:

iceneo.pl

Kod wymaga domains i domains_pl z wcześniejszych przykładów.

import dns.resolver

def get_all_ns(domain, debug=False):
    """Pobieranie NameServerów dla podanej domeny

    Return: (domain, all_ns)
    """

    if debug:
        print('[DEBUG] domain:', domain)

    try:
        all_ns = [str(ns) for ns in dns.resolver.query(domain, 'NS')]
        return domain, all_ns
    except dns.resolver.NoAnswer as ex:
        if debug:
            print('[DEBUG] dns.resolver.NoAnswer:')#, ex)

        # to może być subdomena więc obcinamy pierwszy człon i sprawdzamy ponownie
        # o ile nie zostanie tylko ostatni człon czyli TLD

        parts = domain.split('.')
        if debug:
            print('[DEBUG] parts:', parts)

        if len(parts) > 2:
            subdomain = '.'.join(parts[1:])
            if debug:
                print('[DEBUG]', domain, '->', subdomain)
            try:
                all_ns = [str(ns) for ns in dns.resolver.query(subdomain, 'NS')]
                return (subdomain, all_ns)
            except Exception as ex:
                if debug:
                    print('[DEBUG] Exception:', ex)
    except dns.resolver.NXDOMAIN as ex:
        print('[DEBUG] dns.resolver.NXDOMAIN:', ex)
    except dns.resolver.NoNameservers as ex:
        print('[DEBUG] dns.resolver.NoNameservers:', ex)
    except dns.resolver.Timeout as ex:
        print('[DEBUG] dns.resolver.Timeout:', ex)

    return (domain, [])


# sprawdzanie tylko domen .pl
for domain in domains_pl:
    print('domain:', domain)
    domain, all_ns = get_all_ns(domain)
    print('  ', domain)
    for item in all_ns:
        print('   ->', item)

# sprawdzanie domen .com
for domain in domains:
    if domain.endswith('.com'):
        print('domain:', domain)
        domain, all_ns = get_all_ns(domain)
        print('  ', domain)
        for item in all_ns:
            if item.endswith('pl.'):
                print('   ->', item)

Jeśli potrzebne są dodatkowe informacje o domenie to można jeszcze ręcznie sprawdzić na MXToolBox.com lub użyć requests + beautifulsoup/lxml lub selenium w tym celu. Ale to już pomijam.

Portal MXToolBox.com ma też RestAPI MojoDNS.com do darmowego sprawdzania DNS (ale nie więcej niż jedną domenę na sekundę). Tego narzędzia jednak nie sprawdzałem w działąniu.

Książki: python-dla-kazdego-podstawy-programowania python-wprowadzenie python-leksykon-kieszonkowy python-receptury python-programuj-szybko-i-wydajnie python-projekty-do-wykorzystania black-hat-python-jezyk-python-dla-hackerow-i-pentesterow efektywny-python-59-sposobow-na-lepszy-kod tdd-w-praktyce-niezawodny-kod-w-jezyku-python aplikacje-internetowe-z-django-najlepsze-receptury