Przejdź do głównej zawartości

Bezpieczeństwo

Klient FiveM to program na cudzym komputerze. Loggery eventów i executory Lua to zwykłe, swobodnie dostępne narzędzia, a serwer, który jest bezpieczny tylko dopóki rozmawia z nim własna strona telefonu, nie jest bezpieczny. Oto, co to znaczy w praktyce w tym zasobie, i gdzie leżą reguły.

Wszystko poniżej zostało przejrzane handler po handlerze i wyszło z tego jedenaście rzeczy. Są wymienione na końcu, nie dlatego, że lista jest ciekawa, ale dlatego, że ciekawy jest jej kształt: cztery z nich były tym samym błędem w czterech różnych miejscach.

Potem to samo pytanie zadano w drugą stronę — nie co klient może wysłać, ale w co telefon wierzy na temat tego, co dostaje — i to jest reguła szósta.


1. Kim jesteś, pochodzi z serwera, nigdy z wiadomości.

Phone.Handler(name, fn) wywołuje fn(src, data, player), gdzie src to połączenie, a player jest z niego wyszukiwany. Żaden handler nie bierze od klienta identyfikatora, numeru telefonu ani postaci, by zdecydować, czyich danych dotknąć. Odbiorca jest rozwiązywany z numeru telefonu przez bazę danych; nadawcą jest zawsze wywołujący.

Jedyny raz, gdy to zostało złamane, mic_phone:boot był zarejestrowany jako samo Phone.Boot — a drugi argument Phone.Boot to rekord, z którego buduje się telefon, podczas gdy callback przekazuje handlerowi jako jego drugi argument to, co wysłał klient. Każdy mógł poprosić o telefon dowolnej osoby po nazwie. server/tests.lua zawiera test, który wywołuje zarejestrowany callback ze spreparowaną postacią i oblewa, jeśli dostanie z powrotem telefon.

2. Własne eventy frameworka nie są drzwiami.

RegisterNetEvent otwiera nazwę eventu dla sieci dla tego zasobu. ESX wywołuje esx:playerLoaded i esx:playerLogout lokalnie, przez TriggerEvent, i nigdy ich nie otwiera — więc zarejestrowanie ich tutaj jako net eventów sprawiło, że mic_phone przyjmował je od dowolnego klienta, z dowolnym id gracza w środku. To prowadzi wprost do Phone.OnDropped: zakończ rozmowę tego gracza, wyrzuć go z radia, zapomnij jego telefon.

AddEventHandler dla wszystkiego, co framework wywołuje sam. Linter powie Ci coś przeciwnego; jest w błędzie, a posłuchanie go przywraca dziurę.

3. Nic, co wysyła klient, nie jest przekazywane dalej w takiej postaci, w jakiej przyszło.

Wszystko, co zostanie zapisane lub pokazane komuś innemu, jest odbudowywane z pól, które znamy, zamiast być przekazywane w całości: Nearby.Shared dla tego, co jeden telefon przekazuje drugiemu, attachment() dla tego, co niesie wiadomość, żądanie w services:request, element w citylist:save. Pole, którego nikt nie nazwał, nie przeżywa podróży.

4. Adres jest sprawdzany względem hostów, których używa ten serwer.

Photos.AllowedUrl — https, poniżej 600 znaków i host z ServerConfig.Media.Hosts. Przez to przechodzi każdy obraz, który trafia do wiersza bazy danych lub na ekran innego gracza: aparat, udostępnienia w pobliżu, zdjęcia ogłoszeń, załączniki wiadomości, posty i profile społecznościowe.

Bez tego spreparowany klient może umieścić wybrany przez siebie adres przed każdym graczem, który obok przewinie — co jest sposobem na zbieranie adresów wszystkich na serwerze — albo, ponieważ te kolumny to LONGTEXT, przenieść cały obraz wewnątrz samego linku.

5. Wszystko ma rozmiar i tempo.

DB.Fit(value, width)string, przycięty do swojej kolumny
DB.Json(value, limit)tabela, zakodowana, albo nic, jeśli się nie zmieści
DB.SetJson128 KB na blob; największy widziany prawdziwy ma 929 bajtów
Phone.TooFast100 wywołań na 10 sekund, na gracza
Photos.Chunk3 przesyłania w toku, 24 MB, sprzątane po minucie ciszy
Contacts.Most200
Calls.MostRecents100
MOST_ATTACHMENTS8

Rozmiary mają większe znaczenie, niż się wydaje. Baza danych działa w trybie strict, więc wartość o jeden znak szersza niż jej kolumna nie jest przycinana — wstawienie się nie udaje, w środku tego, co ten handler jeszcze robił. W przelewie ta linijka jest po przesunięciu pieniędzy.

6. To, co napisał inny gracz, jest tekstem, a strona jest zbudowana tak, by nie mogło przestać nim być.

Pięć powyższych dotyczy tego, co dociera do serwera. Ta dotyczy tego, co dociera do telefonu, i jest groźniejszym kierunkiem: strona, która rysuje cudzy profil, to ta sama strona, która może wywołać każdy handler jako swój właściciel. Tag, który się tam wykona, może czytać wiadomości tego gracza i przesuwać jego pieniądze. Nie potrzebuje executora — potrzebuje nazwy.

Telefon rysuje się za pomocą template literali, więc każda wartość jest znacznikami, chyba że coś ją escapuje. escapeHtml w core/app.js jest tym czymś, a rpEsc w aplikacjach miejskich to ta sama funkcja pod lokalną nazwą. Dwie połowy:

Escapuj w miejscu zapisu. Każda interpolacja niosąca czyjeś słowa — nazwa, handle, podpis, notatka, dzielnica, adres — przechodzi przez jedną z tych dwóch. Sprawdzono to, parsując każdy template literal w html/, a nie czytając je, bo jest ich 2918.

I nie polegaj na tym. html/index.html zawiera Content-Security-Policy bez 'unsafe-inline' w script-src. Nic w tym telefonie nie jest skryptem inline ani handlerem inline — każdy skrypt jest plikiem — więc polityka nic nie kosztuje i sprawia, że onerror, który strona została nakłoniona do zapisania, po prostu się nie wykona. To ta połowa, która nie polega na tym, że następna osoba, która pisze funkcję renderującą, cokolwiek zapamięta. server/web.js dodaje dwie rzeczy, których <meta> nie potrafi powiedzieć: frame-ancestors 'none', ponieważ link do połączonego telefonu to cały sekret, a ten telefon może wysyłać pieniądze, oraz nosniff.

tests/verify-injection.js podaje każdej aplikacji ciąg, który byłby jednoznaczny, gdyby kiedykolwiek przestał być tekstem, a potem przeszukuje wyrenderowaną stronę pod jego kątem.

I to, co serwer wysyła bez proszenia. shared/config/*.lua to shared_scripts: każda linijka z nich trafia do klienta każdego gracza i może zostać odczytana z gry executorem. Config.Web przechowywał nazwę, pod którą ten serwer odpowiada, adres, na który wskazuje jego QR, i — dwukrotnie — adres e-mail jego właściciela, z czego żadne nie było nigdy czytane przez klienta. Teraz żyją w shared/server_config.lua, który fxmanifest.lua wymienia w server_scripts i który miał już obok pusty przykład do dostarczenia. Plik configu nie jest prywatnym miejscem, a ustawienia opisujące ten serwer to te, które trzeba z niego wyłączyć.


Ostrożna gałąź i ta obok niej. Cztery z jedenastu były tym samym błędem: funkcja, która obsłużyła przypadek, o którym ktoś pomyślał, i przepuściła drugi. data: było przesyłane, a url nie, w czterech osobnych miejscach, każde napisane w innym czasie przez kogoś, kto trzy linijki wyżej był właśnie ostrożny. Jeśli dodajesz gałąź do któregokolwiek z nich, zapytaj, co robi ta druga.

Hojny limit to nadal limit. Lista kontaktów czeka na zapytanie na każdy kontakt, a wstawienie zajmuje około jednej trzeciej sekundy na serwerze, na którym to napisano: pięćset kontaktów to prawie trzy minuty bazy danych zajętej przez jedno żądanie. Liczba nie była zła dlatego, że była duża — była zła dlatego, że nikt nie pomnożył jej przez cokolwiek.

Reguły żyją w jednej funkcji, a nie w każdym handlerze. Wszystko w tabeli powyżej jest w jednym miejscu. To jedyny sposób, by reguła przetrwała dodanie handlera przez następną osobę.

Sprawdzenie, które kończy się wcześniej, nie jest sprawdzeniem. Photos.AllowedUrl odczytywało host i zatrzymywało się na pierwszym ukośniku — a wszystko po tym ukośniku to część, którą pisze klient. https:// + dozwolony host + /x.jpg" onerror="… przechodziło przez nie, a telefon wstawiał ten ciąg do src elementu img każdemu graczowi, który otworzył ogłoszenie. Reguła była dobra; zapytano o jedną trzecią adresu.


Co pozwalało zrobićGdzie
Kończyć rozmowy dowolnego gracza i wyrzucać go z radia, kiedy się chceesx:playerLogout zarejestrowany jako net event
Zapełnić pamięć serwera, aż się przewróciłprzesyłanie bez limitu i bez sprzątacza
Przesuwać pieniądze bez śladu i dostać informację, że się nie udałonotatka szersza niż jej kolumna, w trybie strict
Umieścić dowolny adres w bibliotece zdjęć innego graczaudostępnienia w pobliżu
Umieścić dowolny adres przed każdym graczem w mieściezdjęcia ogłoszeń
Umieścić dowolny adres przed wszystkimi w czaciezałączniki wiadomości
Umieścić dowolny adres przed wszystkimi w aplikacjiposty i profile społecznościowe
Wywoływać dowolny handler tak szybko, jak się dało wysyłaćbrak limitu tempa na ścieżce w grze
Zapisywać tyle, ile się chciało, wielokrotnieustawienia, miejsca, albumy, preferencje
Dodawać cokolwiek do żądania wychodzącegotime wklejony do URL bez escapowania
Czytać telefon dowolnej postacimic_phone:boot, naprawione wcześniej

Wszystkie jedenaście ma teraz test w server/tests.lua, wcześniej było ich pięć.

Dwa z nich warto wyodrębnić. Zdjęcia ogłoszeń i posty społecznościowe są osiągane wyłącznie przez Photos.AllowedUrl — więc test tej funkcji mówi, że reguła jest dobra, i nic nie mówi o tym, czy te handlery nadal o nią pytają. the handlers that take a photo still ask whether it is one wchodzi drzwiami, tak jak robi to połączony telefon, i odczytuje odmowę: po wycofaniu sprawdzenia spreparowany adres jest akceptowany, ogłoszenie jest zapisywane i rozgłaszane do każdego telefonu w mieście, post jest zapisywany i pokazywany wszystkim w aplikacji, a profil odpowiada

without the picture that was not an address — got https://<a host we allow>/x.jpg" onerror="alert(1)

co jest ciągiem, który trafiłby do img. Udany przebieg niczego nie zapisuje; sprzątanie obok jest dla przebiegu nieudanego, czyli tego, w którym to ma znaczenie.

Test eventu frameworka jest tym osobliwym: nie ma sposobu, by zapytać runtime, które nazwy eventów zasób otworzył dla sieci, więc czyta bridge/framework/server.lua i szuka słów. Słabszy niż reszta, a mimo to jest — błąd, przed którym chroni, to taki, który linter aktywnie rekomenduje.

Co pozwalało zrobićGdzie
Uruchomić kod na telefonie każdego, kto spojrzał na ich profilnazwa, handle, dzielnica, praca lub zainteresowanie wklejone w znaczniki
Uruchomić kod na telefonie każdego, kto otworzył ogłoszenieadres zdjęcia wklejony w src oraz sprawdzenie adresu, które dopuszczało cudzysłów
Uruchomić kod na telefonie każdego, kto czytał rachunek, garaż lub firmęwystawcy, tytuły, tablice, lokalizacje i szczegóły firm wklejone surowo
Sprawić, by cokolwiek z powyższych faktycznie się wykonałobrak jakiejkolwiek polityki treści na stronie
Umieścić telefon wewnątrz strony innej witrynybrak frame-ancestors z drzwi webowych

Pierwsze trzy to po jednym błędzie; ostatnie dwa to powód, dla którego pierwsze trzy miały znaczenie. Wszystkie pięć jest pokrytych przez tests/verify-injection.js, który sprawdza stronę, jaką zbudowałaby przeglądarka, a nie ciągi, które do niej trafiły.

Oba testy najpierw zaobserwowano jako oblewające, po wycofaniu poprawki: ten od adresów wymienia swoje trzy brakujące odmowy, a ten od injection zlicza cztery elementy, które uciekły ze swojego atrybutu — i nic nie mówi o tym, by któryś się wykonał, bo wtedy była już też polityka.