Trwa atak? Skontaktuj się z zespołem reagowania na incydenty

Bezpieczne programowanie

Znajdź każde wywołanie shella budowane z danych użytkownika

Jak znaleźć wywołania shella budowane z danych użytkownika i zastąpić je API bibliotek lub tablicami argumentów zamiast polegać na escapingu.

Także w językach:EnglishTürkçeDeutschFrançaisEspañolItalianoNederlandsEesti

Ilustracja: Znajdź każde wywołanie shella budowane z danych użytkownika

Niezawodna obrona przed OS command injection polega na tym, by w ogóle nie przekazywać do shella ciągów znaków zależnych od użytkownika. Lepszy escaping to rozwiązanie słabsze. Praktyczna praca sprowadza się do znalezienia każdego miejsca, w którym kod uruchamia proces, a potem do decyzji, czy dane wywołanie w ogóle powinno istnieć.

Command injection występuje wtedy, gdy aplikacja przekazuje niezweryfikowane dane użytkownika do powłoki systemowej, a wstrzyknięte polecenia zwykle wykonują się z uprawnieniami aplikacji. CWE-78 dodaje niewygodny szczegół: ponieważ polecenia wykonuje aplikacja, aktywność może wyglądać tak, jakby pochodziła od samej aplikacji lub jej właściciela.

Dlaczego escaping to zła pierwsza odpowiedź

Funkcja escapeshellarg() w PHP opakowuje wartość w apostrofy, dzięki czemu shell traktuje ją jako jeden argument. To powstrzymuje uruchomienie & echo "hello" jako drugiego polecenia. Nie powstrzymuje jednak atakującego przed wyborem tego, jaki jeden argument otrzyma program.

Każdy OS command injection jest zarazem argument injection. W ściądze OWASP wywołanie escape("--help") przekazane do curl nadal wypisuje tekst pomocy. W zależności od programu skutki sięgają od ujawnienia informacji po zdalne wykonanie kodu.

Uwaga czytelnika na stronie podręcznika PHP o escapeshellarg() pokazuje tę samą wadę na przykładzie tar. Każda nazwa pliku jest poprawnie escapowana, a mimo to wartość zaczynająca się od myślnika jest nadal odczytywana jako opcja, a tar ma opcję uruchamiającą inne polecenie. Dodanie -- przed nazwami plików rozwiązuje problem, ale autor uwagi zauważa, że nie każdy program obsługuje taki separator.

W systemie Windows, jak podaje podręcznik, escapeshellarg() używa cudzysłowów i zastępuje spacjami znaki procenta, wykrzykniki oraz cudzysłowy. Uwaga czytelnika dodaje, że wynik jest bezpieczny, ale stratny. Tak czy inaczej, to powód, by unikać shella, a nie poprawka.

Znajdź wywołania

Uruchom poniższe polecenia na każdym repozytorium. Dostosuj typy plików do swojej struktury:

rg -n 'shell=True|os\.system|os\.popen' --type py
rg -n "subprocess\.(run|Popen|call|check_output)\(\s*[f\"']" --type py
rg -n '\b(system|exec|shell_exec|passthru|popen|proc_open)\s*\(' --type php
rg -n '`[^`]*\$' --type php
rg -n 'Runtime\.getRuntime\(\)\.exec|ProcessBuilder' --type java
rg -n 'ProcessStartInfo|Process\.Start|UseShellExecute' --type cs
rg -n '\bsystem\s*\(|popen\s*\(|exec[lv]p?e?\s*\(' --type c

Drugi wzorzec dla Pythona wyłapuje pojedynczy ciąg znaków przekazany jako polecenie. Bez shell=True działa to tylko wtedy, gdy ciąg wskazuje sam program i nic więcej, więc każdy wynik wymaga ręcznego przejrzenia. Wzorzec dla backticków w PHP generuje dużo szumu, bo pasuje także do backticków w komentarzach i w SQL. Przeszukaj również skrypty shellowe, skrypty wdrożeniowe i wrappery cron, które budują polecenia ze zmiennych równie często jak kod aplikacji.

JęzykCzego szukaćCzym zastąpić
PHPsystem(), exec(), shell_exec(), passthru(), popen(), proc_open(), operator backtickWbudowane funkcje, np. mkdir()
Pythonshell=True; os.system; os.popen; pojedynczy ciąg z poleceniemsubprocess.run z listą
JavaRuntime.exec(String)ProcessBuilder z programem i każdym argumentem jako osobnym elementem
.NETcmd.exe, powershell.exe, sh -c, zbudowany ciąg ArgumentsProcessStartInfo z ArgumentList
Csystem()Wywołanie biblioteczne

Uwzględnij też polecenia zakodowane na stałe. Opis społeczności OWASP przedstawia uprzywilejowany program CGI, który uruchamia cd /var/yp && make bez żadnych danych użytkownika w ciągu. Nie użyto w nim ścieżki bezwzględnej do make, więc atakujący mógł zmienić $PATH i sprawić, by jego własny plik binarny wykonał się z uprawnieniami programu.

Przeprowadź triaż wyników

Surowa lista trafień to jeszcze nie lista podatności. Dla każdego trafienia prześledź wartość wstecz i zapytaj, czy może do niej dotrzeć żądanie. Oczywiste źródła to parametry trasy i zapytania, pola formularzy, ciasteczka i nagłówki HTTP. Łatwo zapomnieć o nazwach przesyłanych plików, wartościach z bazy danych, do której użytkownicy mogą zapisywać, wiadomościach z kolejek i zmiennych środowiskowych.

Podziel trafienia na trzy grupy: osiągalne z żądania bez uwierzytelnienia, osiągalne dopiero po zalogowaniu i całkowicie nieosiągalne. Najpierw napraw pierwszą grupę. Trzecią zostaw na liście, bo późniejszy refaktoring może połączyć ją z żądaniem, a nikt tego nie zauważy.

Zastąp wywołanie API

Wbudowanych funkcji bibliotecznych nie da się nakłonić do niczego poza ich zamierzonym zadaniem. Używaj mkdir() zamiast system("mkdir /dir_name"). Używaj Java mail API z javax.mail.* zamiast wywoływać polecenie mail przez shell. CWE-78 daje tę samą radę na poziomie projektowania: używaj wywołań bibliotecznych zamiast zewnętrznych procesów.

Gdy musisz uruchomić proces

Niektóre narzędzia nie mają odpowiednika w bibliotece. Wtedy stosuj razem dwie warstwy: listę argumentów bez shella oraz walidację.

Dokumentacja modułu subprocess w Pythonie mówi, że zwykle lepsza jest sekwencja argumentów, ponieważ moduł sam zajmuje się escapingiem i cytowaniem. Zawiera też zalecenie, by przed użyciem shell=True przeczytać sekcję Security Considerations. Zaleca również pełną ścieżkę do pliku wykonywalnego, co eliminuje wyszukiwanie w PATH, które wykorzystuje opisany wyżej przykład z make.

import re, subprocess

if not re.fullmatch(r"[a-z0-9]{3,10}", name):
    raise ValueError("rejected")

subprocess.run(["/usr/bin/tar", "cf", "out.tar", "--", name], check=True)

Wzorzec już odrzuca wiodący myślnik, więc -- wygląda na zbędne. To jednak defence in depth: jeśli ktoś później poluzuje wyrażenie regularne, separator nadal uniemożliwi odczytanie name jako opcji.

W .NET użyj stałej, pełnej ścieżki w FileName, ustaw UseShellExecute = false i dodawaj każdy argument przez ArgumentList. Nigdy nie przekazuj niezaufanych danych do cmd.exe, powershell.exe ani sh -c. Samo rozdzielenie argumentów nie jest walidacją, ani w Javie, ani w .NET.

Następnie zwaliduj wartości. Sprawdź polecenie względem listy dozwolonych poleceń, a każdy argument względem dozwolonych wartości lub wzorca z maksymalną długością, bez metaznaków i białych znaków.

W przypadku curl samo -- nie wystarczy, ponieważ kolejne argumenty nadal są operandami URL i możliwe są dalsze transfery. Przekaż dokładnie jeden zwalidowany URL, użyj --globoff i zastosuj zabezpieczenia przed SSRF.

Uruchamiaj aplikację z najniższymi uprawnieniami, jakich potrzebuje, a zadania jednorazowe wykonuj na osobnym, odizolowanym koncie. Piaskownica, taka jak chroot, AppArmor czy SELinux, pomaga, ale CWE-78 ocenia jej skuteczność jako ograniczoną. Zawęża to, do czego może dotrzeć atakujący, ale nie usuwa podatności.

Od czego zacząć

  1. Uruchom powyższe wyszukiwania na każdym repozytorium, łącznie ze skryptami wdrożeniowymi, zadaniami CI i wrapperami cron. Wypisz każde trafienie wraz z danymi wejściowymi, które do niego docierają, i posortuj listę według tego, czy żądanie może je osiągnąć.
  2. Dla każdego trafienia zapytaj, czy biblioteka lub funkcja wbudowana wykonuje to samo zadanie. Jeśli tak, zastąp wywołanie i usuń uruchamianie procesu.
  3. Przepisz pozostałe wywołania tak, by używały listy argumentów, żadnego shella i bezwzględnej ścieżki do pliku wykonywalnego. Dodaj walidację opartą na allowlist oraz separator -- tam, gdzie program go obsługuje.
  4. Uruchamiaj procesy na dedykowanym koncie o niskich uprawnieniach, aby pominięty przypadek dawał atakującemu mniej możliwości.
  5. Przetestuj każde pozostałe wywołanie wartością w rodzaju bob.txt;sleep 5 oraz wartością zaczynającą się od myślnika. Mierz czas odpowiedzi zamiast czekać na wynik. Wartość z wiodącym myślnikiem powinna zostać odrzucona lub potraktowana jako nazwa pliku. Jeśli chcesz niezależnej weryfikacji, test penetracyjny sprawdzi oba przypadki, a nasz zespół bezpiecznego wytwarzania oprogramowania może przejrzeć z Tobą kod.

Jak możemy pomóc

Testy penetracyjne

Atakujemy Państwa sieci, aplikacje i ludzi tak, jak zrobiłby to prawdziwy przeciwnik, a potem pokazujemy, jak zamknąć każdą lukę.

Napisz do nas
Zespół AngelForce

Artykuł przygotował zespół AngelForce: konsultanci ds. bezpieczeństwa z certyfikatami CEH, LPT Master i CISA, którzy testują, monitorują i chronią systemy organizacji w całej Europie.

Tekst przygotowany z pomocą AI na podstawie publicznych źródeł wymienionych poniżej i sprawdzony przed publikacją przez zespół AngelForce.

Źródła

  1. cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
  2. community.owasp.org/attacks/Command_Injection
  3. cwe.mitre.org/data/definitions/78.html
  4. docs.python.org/3/library/subprocess.html
  5. www.php.net/manual/en/function.escapeshellarg.php

Artykuły w temacie: Bezpieczne programowanie

Prosimy napisać, co chcą Państwo chronić

Prosimy opisać swoje środowisko i cele. Odpowiemy e-mailem.

Napisz do nas
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonia