Python i atak na łańcuch dostaw
Napisał: Patryk Krawaczyński
06/02/2022 w Bezpieczeństwo Brak komentarzy. (artykuł nr 809, ilość słów: 1406)
N
a początku warto poruszyć temat Python2. Jeśli nadal jest on używany w naszym środowisku (nie)produkcyjnym, należy zdać sobie sprawę, że ataki na łańcuch dostaw z wykorzystaniem tej wersji obejmują już więcej niż tylko podatny interpreter, ale także pakiety innych firm w PyPI (ang. Python Package Index). Dlatego jeśli wykorzystujemy jeszcze drugą wersję w: procesie kompilacji; posiadamy narzędzia i usługi automatyzacji testów; skrypty wbudowane w strukturę CI/CD (ang. Continuous Integration / Continuous Delivery); interfejsy wiersza poleceń lub infrastrukturę wdrożeniową – musimy pogodzić się z świadomością, że jest to tykająca bomba z rosnącą liczbą luk.
Według serwisu pypistats.org średnia liczba pobrań pakietów Python2 (bazując na najpopularniejszym pakiecie: urllib3) nie zmieniła się dramatycznie w ciągu dwóch lat, odkąd wersja ta została uznana za EOL (ang. End of Life), czyli jej życie jako oprogramowanie dobiegło końca. Podczas, gdy odsetek pobrań urllib3 w drugiej wersji spadł z ~50% do ~16% w ciągu dwóch lat to w wartościach bezwględnych dzienna liczba pobrań pozostaje taka sama i wynosi ~1.5 mln. Wynika z tego, że nowi użytkownicy języka Python zaczynają od wersji trzeciej, ale starsi użytkownicy nadal pracują z wersją drugą. To duża liczba pobrań tego pakietu biorąc pod uwagę, że obecnie posiada on zarówno wysoką i jak średnią lukę bezpieczeństwa, której społeczność nie załatała.
Przed atakiem na Solarwinds w grudniu 2020 roku programiści rzadko uważali swoje wewnętrzne procesy tworzenia oprogramowania i infrastrukturę za wspólny cel cyberprzestępców. Mimo, że atak ten był relacjonowany przez różne media, istnieją dziesiątki podobnych ataków, o których nie zawsze mogliśmy się dowiedzieć. Na szczęście Cloud Native Computing Foundation śledzi je wszystkie w publicznie dostępnym repozytorium. Biorąc więc pod uwagę najnowsze trendy środowiska tworzenia oprogramowania stają się dla złych aktorów kluczowym sposobem na masowe łamanie zabezpieczeń innych firm. Bardzo często korzystają one z różnych dostawców oprogramowania, dlatego są one podatne na ataki poprzez wstrzykiwanie złośliwego oprogramowania do łat, aktualizacji, a nawet nowych wersji. Kiedy klienci danego dostawcy instalują nowy kod, atakujący uzyskują dostęp do tysięcy, a nawet dziesiątek tysięcy potencjalnych celów za pośrednictwem jednej modyfikacji w łańcuchu dostaw lub znalezieniu krytycznej luki w popularnej bibliotece. Dlatego jeśli Twój zautomatyzowany proces kompilacji kodu wykorzystuje język Python do czegoś takiego, jak nieszkodliwe skrypty, które weryfikują i rejestrują wyniki testów lub do bardziej krytycznych kroków, takich jak budowanie artefaktów / kontenerów / maszyn wirtualnych i ich dystrybucji – w zmieniającym się krajobrazie zagrożeń żaden z tych procesów nie powinien być uważany za nietykalny tylko dlatego, że są to procesy wewnętrzne.
Python3 – Atak:
Pisząc oprogramowanie w języku Python bardzo wiele bibliotek jest już wbudowanych. Jednak często będziemy potrzebować ich z zewnątrz – najcześciej pobierając je z PyPI, czyli oficjalnego indeksu pakietów. I już na tym etapie musimy zachować ostrożność. Ekosystem języka Python był już niejednokrotnie celem ataków, w szczególności dwóch technik: typosquatting, gdzie mimo wpisania nazwy pakietu z literówką jest on ściągany w szkodliwej wersji, ponieważ ktoś specjalnie utworzył taką wersję oraz przedstawiony w zeszłym roku dependency confusion, gdzie z powodu błędów w narzędziach lub wadliwej standardowo konfiguracji pakiety stworzone i dostępne w prywatnych sieciach są pobierane z internetu z nowszymi wersjami, które również ktoś specjalnie utworzył. Techniki te dotyczą polecenia, które każdy programista języka Python wykonał przynajmniej raz w swojej karierze, czyli:
pip install pip-audit
Wystarczy teraz, że cyberprzestępca opublikuje prawie taki sam pakiet (może on nawet posiadać taki sam kod, tylko z dodaną szkodliwą instrukcją) mając nadzieje, że użytkownik popełni literówkę i wpiszę np. pip-aydit lub pip-audid lub pip3-audit. Społeczność ciężko pracuje, aby zmniejszyć ryzyko z tym związane, ale ono nadal jest wysokie i takie błędy się zdarzają. Drugi atak wykorzystujący wersje w zależnościach pakietów jest bardziej wyrafinowany niż pierwszy i dotyczy tego, jak polecenie pip
działa pod maską. Spójrzmy na poniższe polecenie:
pip install pip-audit --extra-index-url https://artefakty.firmowego.pythona.pl
Wydając to polecenie należy mieć świadomość, że instalator pakietów dla języka Python przejdzie przez drogę:
PIP 1: sprawdzam, czy pakiet istnieje w artefakty.firmowego.pythona.pl PIP 2: sprawdzam, czy pakiet istnieje w PyPI.org PIP 3: instaluje pakiet z tego źródła, na którym go znalazłem.
Jeśli pakiet istnieje w obydwu źródłach zostanie zainstalowana wersja z wyższym numerem (“nowsza”). Dlatego w tym przykładzie pakiet od złośliwego aktora zostanie zainstalowany w naszym projekcie, gdy tylko osoba ta uzyska informację o nazwie naszego autorskiego pakietu, który jest używany wewnątrz naszej organizacji. Wydając znacznie wyższy numer jego wersji w publicznym indeksie pakietów przyzna sobie “pierwszeństwo” do instalacji jego pakietu. Czy oznacza to, że powinniśmy za wszelką cenę chować wewnętrzne nazwy pakietów? Nie. Co więcej, znalezienie odpowiednich nazw pakietów nie jest takie trudne, jak wykazał Alex Birsan. Polecenie pip
okazało się więc “niezabezpieczone z założenia” (ang. insecure by design), ponieważ atak ten nie ingeruje w żaden jego mechanizm, a tylko wykorzystuje funkcję menadżera pakietów Python – i tylko niewielu programistów tego języka zdaje sobie sprawę z tego zachowania.
Python3 – Obrona:
Aby bronić się przed powyższymi atakami – możemy zacząć od używania tzw. lock file, który zamyka nasze zależności opisując je w określonym formacie. Dobry plik blokady posiada następujące atrybuty:
- Przypięte wersje: sprawiają, że projekty podczas instalacji i kompilacji są przewidywalne i deterministyczne,
- Funkcje skrótu: są sposobem na weryfikację integralności pakietów,
- Pełny diagram zależności: pozwala w przejrzysty sposób kontrolować zależności innych zależnych pakietów.
Większość projektów, które używa plików requirements.txt
bardzo rzadko dba o te trzy punkty. Zazwyczaj mają one ubogą formę taką jak:
elasticsearch
Jeśli ktoś dba o przetestowane wersje i nie lubi być zaskakiwanym, że po instalacji wyższej wersji pakietu losowa funkcjonalność przestaje mu działać pokusi się o zawartość typu:
elasticsearch==7.10.1
Dobrze. A jak spełnić wszystkie trzy punktu? Dobrą wiadomością jest to, że od dawna istnieje pakiet: pip-tools
, który wspomaga cały proces tworzenia dla nas pliku blokady:
agresor@darkstar:~$ mv requirements.txt requirements.in agresor@darkstar:~$ cat requirements.in elasticsearch==7.10.1 agresor@darkstar:~$ pip install pip-tools==6.5.0 agresor@darkstar:~$ pip-compile --generate-hashes agresor@darkstar:~$ cat requirements.txt # # This file is autogenerated by pip-compile with python 3.9 # To update, run: # # pip-compile --generate-hashes # certifi==2021.10.8 \ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 # via elasticsearch elasticsearch==7.10.1 \ --hash=sha256:4ebd34fd223b31c99d9f3b6b6236d3ac18b3046191a37231e8235b06ae7db955 \ --hash=sha256:a725dd923d349ca0652cf95d6ce23d952e2153740cf4ab6daf4a2d804feeed48 # via -r requirements.in urllib3==1.26.8 \ --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c # via elasticsearch
Jak możemy zobaczyć teraz nasz plik zawiera wszystkie zależności wraz z powiązanymi wersjami i skrótami. Gdy teraz inny użytkownik będzie chciał bezpiecznie zainstalować nasz projekt na swoim środowisku wystarczy, że wyda następujące polecenie:
pip install --require-hashes -r requirements.txt
Zakładając, że zachowaliśmy ostrożność podczas pisania pliku wymagań (requirements.in
) inny użytkownik będzie miał pewność, że pakiety, których użyliśmy w swoim projekcie nie zostały zmienione. Jest to najlepsza praktyka, aby bronić się przed atakami polegającymi na literówkach i połowicznie przed zamieszaniem w zależnościach pakietów. Jak możemy uzupełnić obronę przed dependency confusion oprócz przypinania konkretnych wersji? Jeśli zajrzymy do dokumentacji narzędzia pip to zobaczymy, że istnieją dwie opcje, których możemy użyć do pobierania pakietów z wewnętrznego repozytorium: --extra-index-url
oraz --index-url
. Jak widać, jest między nimi niewielka różnica i na pierwszy rzut oka nie jest to oczywiste, którą opcję wybrać. Różnica polega na tym, że jeśli wykorzystamy opcję --index-url
, pip będzie pobierał tylko pakiety z repozytorium w podanym adresie URL lub lokalnego katalogu – nie skorzysta później z publicznych repozytoriów. Więc jeśli chcemy uniknąć publicznej komunikacji i narażenia się na ściągnięcie szkodliwej paczki powinniśmy używać opcji --index-url
zamiast --extra-index-url
.
Skoro jesteśmy już przy pakietach i ich wersjach to przede wszystkim powinniśmy śledzić w nich luki bezpieczeństwa. W tym celu możemy okresowo sprawdzać bazę danych GitHub lub wykorzystać do tego narzędzia takie jak: pip-audit, dependabot, safety lub serwisy: Snyk for Python, Python Dependency Security, które automatycznie wykrywają i powiadamiają nas o lukach znalezionych w zależnościach.
Więcej informacji: Dustin Ingram – Secure Software Supply Chains for Python The Python 2 Threat in Your Supply Chain Is Real, How to secure your Python software supply chain, Typosquatting and Supply Chains Vulnerabilities, Hunting for Malicious Packages on PyPI