Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
RCE w CurseForge – jak lokalny serwer WebSocket zarządzał launcherem i wykonywał kod
WebSockety to jeden ze sposobów na przekazywanie danych między klientem a serwerem “na żywo”, bez ciągłego odpytywania (pollingu), który wiąże się z wysyłaniem wielu niepotrzebnych żądań. Co jednak może się stać, gdy serwer WebSocket wystawi lokalnie aplikacja działająca na urządzeniu?
TLDR:
- Jeden z użytkowników odkrył, że desktopowy launcher CurseForge (sklep z modami do gier) wystawiał lokalny serwer WebSocket dostępny także z poziomu przeglądarki.
- Serwer ten nie weryfikował nagłówka Origin ani nie wymagał żadnego uwierzytelnienia.
- Dowolna strona internetowa mogła więc wysyłać do launchera polecenia przez localhost.
- Jedna z dostępnych metod pozwalała uruchamiać modpacki z dowolnymi argumentami Javy, co umożliwiało zdalne wykonanie kodu na urządzeniu użytkownika.
- Atak wymagał jedynie znalezienia losowo przydzielonego lokalnego portu WebSocketa.
- Luka była możliwa do wykorzystania przez ponad trzy miesiące i została załatana w wersji 1.289.3.
Użytkownik Elliott zauważył, że platforma CurseForge – jeden ze sklepów z modami (dodatkami) do gier wideo – wykorzystuje WebSockety właśnie w taki sposób. Desktopowa aplikacja do zarządzania i uruchamiania modpacków przy każdym takim uruchomieniu wysyłała następującą wiadomość przez połączenie z lokalnym serwerem WebSocket:
{
"args": [
{
"MinecraftInstanceGuid": "9ee1c6b8-f0f3-441c-b6be-6b03a7a6019a",
"ResolutionWidth": 1024,
"ResolutionHeight": 768,
"LauncherVisibility": "Close",
"LauncherType": "Classic",
"AdditionalJavaArguments": ""
}
],
"type": "method",
"name": "minecraftTaskLaunchInstance"
}
Listing 1 – treść komunikatu wysłanego przez WebSocket, źródło: elliott.diy
Sam fakt, że aplikacja wystawia lokalny serwer WebSocket i się z nim komunikuje, nie jest przecież niczym niebezpiecznym. Żądanie wyglądało jak zwykły mechanizm obsługujący uruchomienie aplikacji. Ale Elliott poszedł o krok dalej. Zdziwiło go, że w żądaniu nie znalazł się żaden token uwierzytelniający, klucz API ani identyfikator sesji.
Napisał skrypt, który wykonywał w zasadzie tę samą czynność – wysłał taką wiadomość przez WebSocket, tyle że z nagłówkiem Origin wskazującym jego własną domenę.
Launcher zaakceptował połączenie i uruchomił modpack. Skoro niezwiązany z CurseForge skrypt z fałszywym nagłówkiem origin mógł komunikować się z launcherem, to każda strona internetowa odwiedzona przez użytkownika potencjalnie mogła również wysyłać takie wiadomości.
Strony internetowe mogą (w kontekście przeglądarki użytkownika) wykonywać żądania do localhost tak samo, jak do innych domen. Przykładowo ING Bank Śląski wykorzystuje ten sposób do wykrywania TeamViewera i innych usług zdalnego pulpitu podczas logowania do bankowości.
No ale samo uruchomienie modpacka to nic strasznego. Elliott zaczął więc szukać innych możliwości wykorzystania WebSocketów w ramach launchera. Kod (lokalnego) serwera znajduje się w pliku CurseAgent.exe i uruchamia się wraz z otwarciem launchera.
Po dekompilacji oprócz potwierdzenia braku walidacji nagłówka Origin, w kodzie znalazła się obsługa kilku dodatkowych akcji poza samym uruchamianiem modpacka. W tym m.in.:
- minecraftGetDefaultLocation zwracająca domyślną ścieżkę instalacji Minecrafta
- createModpack tworząca nowy modpack i zwracająca jego GUID
- minecraftTaskLaunchInstance uruchamiająca modpack z dostarczonym GUID, typem launchera oraz dowolnymi argumentami Javy
Najbardziej problematycznie wygląda minecraftTaskLaunchInstance, ponieważ pozwala na dostarczenie dowolnych argumentów JVM (w polu AdditionalJavaArguments).
Elliott stworzył więc prosty Proof-of-concept na przykładzie gry Minecraft. CurseForge obsługuje kilka innych gier i zapewne po adaptacji technika ta zadziałałyby również dla nich. Na tym etapie do powodzenia ataku musimy więc wiedzieć, że ofiara używa Minecrafta i modów z CurseForge, co można łatwo potwierdzić korzystając z funkcji minecraftGetDefaultLocation.
PoC łączy ze sobą dwie wystawione metody WebSocket: jedną do stworzenia nowego modpacka i drugą do jego uruchomienia z argumentami Javy kontrolowanymi przez atakującego.W pierwszym kroku konieczne jest wywołanie akcji createModpack, która tworzy nowy modpack w systemie ofiary i zwraca GUID wymagany do uruchomienia gry w kolejnym etapie.
{
"args": [{
"GameId": 432,
"Name": "PWNED",
"Author": "Elliott <3",
"GameVersion": "1.21.8",
"ModloaderVersionString": "forge-58.0.1",
"ProfileImagePath": null,
"InstallSource": 0,
"ModsToInstall": [],
"GroupId": null
}],
"type": "method",
"name": "createModpack"
}
Listing 2 – tworzenie modpacku przez WebSocket, źródło: elliott.diy
Na potrzeby PoC Elliott użył następujących flag:
-XX:MaxMetaspaceSize=16m
-XX:OnOutOfMemoryError=”cmd.exe /c calc”
Wymuszenie małego rozmiaru metaspace (pierwsza flaga) powoduje, że zostaje on szybko przekroczony podczas startu. Druga flaga obsługuje ten błąd i wykonuje w tym momencie dowolne polecenie. W tym przypadku efektem było jedynie uruchomienie kalkulatora, ale można było wstawić w tym miejscu dowolny inny kod.
Złośliwe argumenty oraz uzyskany wcześniej GUID są następnie przekazywane w wywołaniu minecraftTaskLaunchInstance. To ta funkcja przyjmuje dowolne argumenty poprzez AdditionalJavaArguments.
{
"args": [{
"MinecraftInstanceGuid": "GUID",
"ResolutionWidth": 1024,
"ResolutionHeight": 768,
"LauncherVisibility": "Close",
"LauncherType": "Classic",
"AdditionalJavaArguments": "-XX:MaxMetaspaceSize=16m -XX:OnOutOfMemoryError=\"cmd.exe /c calc\""
}],
"type": "method",
"name": "minecraftTaskLaunchInstance"
}
Listing 3 – uruchomienie gry i wykonanie kodu przez WebSocket, źródło: elliott.diy
Gdy lokalny serwer WebSocket otrzyma tę wiadomość, CurseForge uruchamia nowo utworzony modpack z dostarczonymi argumentami Javy, powodując wykonanie kodu na urządzeniu ofiary podczas startu gry. W zależności od ustawień launchera użytkownika może to wymagać minimalnej interakcji użytkownika, ale w wielu przypadkach odbywa się automatycznie.
Żeby nie było zbyt “kolorowo”, serwer WebSocket wystawiany przez CurseForge nie działa na stałym porcie. Numer portu jest losowany przy każdym uruchomieniu launchera.
Atakujący musi więc przeskanować lokalne porty, aby ustalić gdzie serwer faktycznie nasłuchuje. Gdy właściwy port zostanie znaleziony, reszta łańcucha exploita przebiega normalnie.
Wymaga to przeskanowania około 16 tys. portów, co w przeglądarkach opartych na Chromium działa bez większych problemów. Taka liczba jest jednak nie do udźwignięcia dla przeglądarki Firefox, co praktycznie uniemożliwia wykonanie ataku (chyba, że innym sposobem poznamy numer portu, na którym w danej chwili działa serwer WebSocket).
Przykład wykonania takiego ataku można zobaczyć w tym nagraniu. Badacz udostępnił także kod pokazanego PoC, który można (jako stronę HTML+JS) przetestować na własnym urządzeniu. Dla chętnych podajemy link do repozytorium, jednak należy pamiętać że komunikacja WebSocket z podatną wersja CurseForge może skończyć się uruchomieniem złośliwego kodu na naszym urządzeniu. Pokazany PoC nie wykonywał podczas naszych testów żadnych złośliwych operacji, ale nie mamy wpływu na ewentualne zmiany/modyfikacje w kodzie. TLDR: testujesz na własną odpowiedzialność.
W pokazanym PoC atak wymaga kliknięcia przycisku, ale bez problemu mógłby zostać przeprowadzony przy samym odwiedzeniu strony kontrolowanej przez atakującego.
Elliott zgłosił problem do CurseForge wkrótce po odkryciu. Pełny harmonogram zdarzeń:
- 29 lipca 2025 – podatność odkryta i wstępnie zgłoszona.
- 11 sierpnia 2025 – dalsza komunikacja z CurseForge. W tym miejscu warto zaznaczyć, że Elliott nie został skontaktowany z osobami odpowiedzialnymi za bezpieczeństwo, a całość eskalował wewnętrznie community manager.
- 31 sierpnia 2025 – podatność formalnie potwierdzona.
- 15 września 2025 – Elliott otrzymuje koszulkę CurseForge w podziękowaniu.
- 2 listopada 2025 – CurseForge wydaje poprawkę w wersji 1.289.3.
- 23 grudnia 2025 – Elliott publikuje tekst (odstęp czasowy po rollout poprawionej wersji).
Choć od zgłoszenia do wdrożenia poprawki atak RCE był możliwy przez nieco ponad trzy miesiące zespół CurseForge ostatecznie rozwiązał problem. W przypadku tej podatności jedynym sposobem na minimalizację ryzyka jest regularne aktualizowanie wykorzystywanego oprogramowania. I to właśnie polecamy robić.
Źródło: elliott.diy
~Tymoteusz Jóźwiak
