W miarę bezpieczne przekazywanie sekretów w powłoce
Napisał: Patryk Krawaczyński
Wczoraj w Bezpieczeństwo Brak komentarzy. (artykuł nr 945, ilość słów: 1332)
W
wielu przypadkach zachodzi potrzeba wpisania wrażliwych danych / sekretów do poleceń w interaktywnej powłoce systemowej, np. bash. Często są to klucze API, tokeny lub hasła używane do sprawdzenia poprawności uwierzytelnienia, pobierania testowych danych itd. Niestety bezpośrednie wpisywanie tego typu danych do poleceń udostępnia je wszystkim osobom w współdzielonym systemie, które mogą widzieć Twoje procesy. Tego typu dane również lądują w plikach historii, które w przypadku udanej infekcji bardzo często są celem atakujących. W standardowej konfiguracji systemu Linux wiersze poleceń procesów są widoczne dla wszystkich użytkowników poprzez system plików /proc. W ten sposób działają takie narzędzia jak ps lub pgrep – przeszukują rekurencyjnie katalogi poszczególnych identyfikatorów procesów i odczytują pliki (np. cmdline, environ, status) opisujące każdy proces. Oczywiście możemy użyć specjalnej opcji montowania (hidepid) dla systemu plików proc, aby uniemożliwić innym użytkownikom inspekcję procesów. Jednak ten zabieg nadal powoduje „wyciek” danych do pliku historii, o którego czyszczeniu nie zawsze się pamięta.
Zresztą lepszym rozwiązaniem wydaje się całkowicie unikać przekazywania sekretów w wierszu poleceń, ponieważ można je zastosować w systemach, w których nie mamy dostępu do zmiany opcji montowania ani sterowania historią powłoki. Ale po kolei. Zacznijmy od prostego przykładu, który zarówno spowoduje wyciek sekretu do listy procesów oraz pliku historii:
agresor@darkstar:~$ curl -H 'APIKEY: top_secret' http://localhost/cgi-bin/login.cgi
abjuzer@darkstar:~$ ps aux | grep curl
agresor 1102 0.0 0.2 24176 10624 pts/4 S+ 18:02 0:00 \
curl -H APIKEY: top_secret http://localhost/cgi-bin/login.cgi
agresor@darkstar:~$ history 2
2732 curl -H 'API-KEY: top_secret' http://localhost/cgi-bin/login.cgi
2733 history 2
Możemy teraz wprowadzić lekkie usprawnienie, które usunie nam przekazywanie sekretu z procesów powłoki. Do tego wykorzystamy wbudowaną funkcję powłoki (ang. shell builtin), czyli echo. Kiedy wpiszemy w powłokę:
echo "top_secret"
bash nie utworzy nowego procesu (nie wykona rozwidlenia – fork()) tylko „samemu” wpisze tekst. Dlatego inny użytkownik zalogowany na serwerze i posługujący się programem ps (z dowolnymi parametrami) nie zobaczy procesu o nazwie echo z naszym sekretem jako argumentem:
agresor@darkstar:~$ umask 077 agresor@darkstar:~$ echo "APIKEY: top_secret" > api-key.txt agresor@darkstar:~$ curl -H @api-key.txt http://localhost/cgi-bin/login.cgi agresor@darkstar:~$ history 3 2743 echo "APIKEY: top_secret" > apikey.txt 2744 curl -H @apikey.txt http://localhost/cgi-bin/login.cgi 2745 history 3
Niestety mimo naszego wysiłku pozbycia się sekretu z listy procesów nasza powłoka nadal zapisuje wrażliwe dane w historii poleceń i dodatkowo pozostawiamy plik tekstowy, który wymaga zapamiętania, aby go usunąć – czyli na święte nigdy. Zróbmy teraz krok w tył i pozbądźmy się pliku tekstowego. Jak wiemy w Linuksie wszystko jest plikiem. Wiele powłok systemowych w tym bash obsługuje tzw. podstawienie procesu (ang. process substitution), które polega na przekazaniu danych wyjściowych procesu (lub procesów) do standardowego wejścia (stdin) innego procesu. Jest to o tyle wygodne, że tego typu funkcja tworzy tymczasowy deskryptor pliku (np. /dev/fd/63), który znika natychmiast po zakończeniu przekazywania danych (dane są przekazywane przez potok (ang. pipe) w pamięci, więc nie trafiają fizycznie na dysk):
agresor@darkstar:~$ rm apikey.txt
agresor@darkstar:~$ curl -H @<(echo "APIKEY: top_secret") http://localhost/cgi-bin/login.cgi
abjuzer@darkstar:~$ ps aux | grep curl
agresor 1417 0.0 0.2 24176 10624 pts/4 S+ 19:28 0:00 \
curl -H @/dev/fd/63 http://localhost/cgi-bin/login.cgi
agresor@darkstar:~$ ls -al /dev/fd/63
ls: cannot access '/dev/fd/63': No such file or directory
agresor@darkstar:~$ history 3
2750 curl -H @<(echo "APIKEY: top_secret") http://localhost/cgi-bin/login.cgi
2751 ls -al /dev/fd/63
2752 history 3
Pora uporać się z zapisywaniem wrażliwych danych do pliku historii powłoki. W tym celu wykorzystamy kolejną wbudowaną funkcję read:
agresor@darkstar:~$ read -s apikey
agresor@darkstar:~$ curl -H @<(echo "APIKEY: $apikey") http://localhost/cgi-bin/login.cgi
abjuzer@darkstar:~$ ps aux | grep curl
agresor 1635 0.0 0.2 24176 10624 pts/4 S+ 20:53 0:00 \
curl -H @/dev/fd/63 http://localhost/cgi-bin/login.cgi
agresor@darkstar:~$ history 2
2771 curl -H @<(echo "APIKEY: $apikey") http://localhost/cgi-bin/login.cgi
2772 history 2
W ten sposób unikamy przekazania sekretu do listy procesów oraz pliku historii. W dodatku po zamknięciu powłoki zmienna $apikey sama zniknie (w razie potrzeby możemy ją usunąć za pomocą: unset apikey). Analogicznie możemy postępować z programami czytającymi dane ze standardowego wejścia:
agresor@darkstar:~$ read -s token
agresor@darkstar:~$ cat <(echo $token) | openssl enc -aes-256-cbc -salt -pbkdf2 -in \
data.json -out data.json.enc -pass pass:stdin
Oprócz wbudowanej funkcji read do wprowadzenia sekretu możemy wykorzystać program systemd-ask-password. Jest on o tyle lepszy, że potrafi wyświetlić monit o wprowadzenie sekretu w różnych środowiskach: w terminalu, w oknie graficznym, a nawet konsoli szeregowej. Spójrzmy na jego przykład wykorzystania. Po wydaniu pierwszego polecenia za pomocą przycisku tabulatora sami decydujemy, czy wpisywane dane mają być widoczne w terminalu, czy nie:
agresor@darkstar:~$ apikey=$(systemd-ask-password "Key:") Key: (no echo) agresor@darkstar:~$ curl -v -H @<(echo "APIKEY: $apikey") http://localhost/cgi-bin/login.cgi
Jednak to nie wszystko. System Linux zawiera potężny mechanizm zarządzania kluczami w jądrze systemu znany jako Kernel Key Retention Service. Pozwala on systemowi i aplikacjom na bezpieczne przechowywanie wrażliwych danych bez konieczności trzymania ich w przestrzeni użytkownika. Dzięki zastosowaniu tego mechanizmu zyskujemy izolację uprawnień oraz możliwość zarządzania czasem życia sekretu (TTL - Time To Live). Powtórzmy teraz powyższe przykłady stosując polecenie keyctl
agresor@darkstar:~$ sudo apt install -y keyutils agresor@darkstar:~$ stty -echo; keyctl padd user apikey @u; stty echo top_secret [ENTER] Ctrl+D 788700497 agresor@darkstar:~$ keyctl list @u 1 key in keyring: 788700497: --alswrv 1000 1000 user: apikey agresor@darkstar:~$ keyctl show Session Keyring 301204522 --alswrv 1000 1000 keyring: _ses 734385337 --alswrv 1000 65534 \_ keyring: _uid.1000 788700497 --alswrv 1000 1000 \_ user: apikey
Przed chwilą dodaliśmy prosty klucz typu user o nazwie apikey i przypisaliśmy mu wartość top_secret umieszczając go w breloku na klucze, który jest przypisany do konkretnego identyfikatora użytkownika (@u). Oznacza to, że sekret ten będzie współdzielony przez wszystkie sesje tego użytkownika (jeśli zalogujemy się trzy razy przez SSH na tego samego użytkownika, we wszystkich tych terminalach będziemy mieć dostęp do tego klucza). Klucz będzie istniał tak długo, jak długo użytkownik jest zalogowany w systemie przynajmniej jedną sesją. W celu ograniczenia klucza tylko do bieżącej sesji terminala i jej procesów potomnych możemy użyć session keyring, czyli: @s.
agresor@darkstar:~$ keyctl timeout 788700497 60
agresor@darkstar:~$ curl -v -H @<(echo "APIKEY: $(keyctl pipe 788700497)") \
http://localhost/cgi-bin/login.cgi
Przed przesłaniem sekretu do programu curl ustawiliśmy jeszcze czas wygaśnięcia klucza na 60 sekund. Po upływie tego czasu próba odczytu tego klucza zakończy się błędem: keyctl_read_alloc: Key has expired. Oprócz czasu wygaśnięcia możemy pozbyć się klucza od razu za pomocą dwóch opcji:
agresor@darkstar:~$ keyctl revoke 788700497 agresor@darkstar:~$ keyctl unlink 788700497 @u
Pierwsza z nich unieważnia klucz. Będzie on nadal widniał na liście kluczy, ale będzie bezużyteczny: keyctl_read_alloc: Key has been revoked. Druga natomiast całkowicie usuwa powiązanie klucza z danym brelokiem kluczy. Jeśli klucz nie jest nigdzie podpięty, zostanie sam usunięty z pamięci.
Podsumowanie:
Z premedytacją zostały pominięte takie sztuczki jak: ustawianie zmiennej HISTCONTROL na ignorowanie poleceń wydawanych ze spacją jako prefiksem; tymczasowe wyłączanie (set +o history) i włączenie (set -o history) historii; zapisywanie plików do pamięci RAM (/dev/shm); export zmiennych środowiskowych, które później są widoczne w ścieżce: /proc/$PID/environ oraz inne techniki omijania audytowalności, które bardziej przypominają działanie szkodliwego oprogramowania niż poruszanie się po poprawnie skonfigurowanym systemie. Wiele osób zakłada, że skoro są jedynymi użytkownikami swojej maszyny lub konta (albo mają uprawnienia administratora), to to, co wpisują w terminalu, zostanie "między nimi, a komputerem". W rzeczywistości linia poleceń to jedno z najbardziej publicznych miejsc w systemie operacyjnym. Każde argumenty przekazane do polecenia są traktowane przez system jako dane jawne, a nie poufne, a ich historia to kronika błędów bezpieczeństwa, która nie wygasa sama z siebie.
Więcej informacji: The Linux Kernel Key Retention Service and why you should use it in your next application, systemd-ask-password, Handling secrets (somewhat) securely in shells
Poprzedni wpis Brak nowszych postów

