Opowieść o tym, jak chciałem naprawić prosty błąd w oprogramowaniu, a skończyłem, odkrywając szereg podatności o niepokojąco dużej sile rażenia. W pierwszej części skupimy się na analizie popularnego dodatku do przeglądarki, który przez niedopatrzenia w algorytmie weryfikacji integralności umożliwiał zdalne wykonanie kodu przy minimalnej interakcji użytkownika, a niekiedy nawet bez niej.
Seria “Badanie e-podpisów”
Artykuł stanowi część cyklu opisującego mój hobbystyczny projekt badania bezpieczeństwa oprogramowania związanego z obsługą podpisów kwalifikowanych.
- Zdalne wykonanie kodu w SzafirHost – [Badanie e-podpisów, cz. 1]
- Hakowanie e-Sądu YubiKeyem – [Badanie e-podpisów, cz. 2]
- Ominięcie uwierzytelniania w ZUS-ie i systemach e-Zdrowia, czyli o krok od cyberchaosu – [Badanie e-podpisów, cz. 3]
| Streszczenie całej serii w postaci niewymagającej od czytelnika posiadania rozległej wiedzy technicznej znajduje się w odrębnym artykule: Krytyczna podatność umożliwiająca całkowite ominięcie logowania w ZUS-ie, e-Sądzie i systemach e-Zdrowia. |
Prolog
Jako osoba prowadząca jednoosobową działalność gospodarczą jestem zobowiązany do podpisywania i składania różnego rodzaju raportów, takich jak chociażby JPK_VAT. Do generowania niezbędnych dokumentów księgowych używam popularnej, komercyjnej aplikacji internetowej.
Raporty VAT są raczej tematem przyziemnym i mało fascynującym, natomiast pod względem technicznym interesujący jest sam proces ich podpisywania – mogę to zrobić bezpośrednio z przeglądarki. Podłączam czytnik kart do komputera, wkładam moją kartę z podpisem kwalifikowanym i klikam “Podpisz” w aplikacji internetowej.


Dalej następuje skomplikowana reakcja łańcuchowa: aplikacja internetowa wywołuje dodatek do przeglądarki, który uruchamia program w Javie, który z kolei pobiera z internetu jakieś komponenty o groźnie brzmiących nazwach. Wreszcie, cały ten moloch uruchamia się, prosi mnie o PIN, składa podpis na deklaracji i… gotowe.
To, co od dłuższego czasu zwracało moją uwagę, ale nigdy nie miałem czasu się temu przyjrzeć, to fakt, że oprogramowanie do składania podpisów pobiera aktualizacje niemal za każdym razem, kiedy potrzebuję go użyć. W monotonii comiesięcznego podpisywania raportów dałbym sobie rękę uciąć, że od lat patrzę na ten sam pasek postępu, pobierający w kółko te same biblioteki… Zaraz…

Zacząłem faktycznie notować wyświetlane nazwy i wersje pobieranych bibliotek, a później wyszukałem je w repozytorium paczek do Javy (Maven Repository).

Nie myliłem się. Co miesiąc pustym wzrokiem patrzę na pasek postępu, obserwując, jak oprogramowanie do podpisu ściąga bibliotekę wydaną w 2007 roku w ramach “aktualizacji oprogramowania”.
Naturalnie, postanowiłem zajrzeć do środka i spróbować naprawić irytujące zachowanie. I tutaj rozpoczyna się nasza przygoda…
Dlaczego ciągle mam aktualizacje?
Możliwość składania podpisów kwalifikowanych w aplikacji internetowej zapewnia dodatek do przeglądarki “Szafir SDK Web”, który do poprawnego działania wymaga natywnej aplikacji “SzafirHost” napisanej w Javie. Cały ten stos technologiczny opracowała Krajowa Izba Rozliczeniowa S.A. (KIR) – najbardziej znana z systemu przelewów ELIXIR oraz BLIK-a.
Sama wtyczka do przeglądarki jest dosyć minimalistyczna – wystawia pewne funkcje w JavaScripcie, które pozwalają na wywołanie aplikacji “SzafirHost” i komunikację z nią. Funkcjonalnie wtyczka praktycznie ogranicza się do roli pośrednika pomiędzy stroną internetową a natywnym programem.
Natomiast, jak się okazuje, sama natywna część również nie zawiera właściwej funkcjonalności. Jedyną rolą “SzafirHosta” jest pobranie faktycznych komponentów z internetu, zweryfikowanie ich integralności i wreszcie uruchomienie docelowego oprogramowania, które będę roboczo nazywał “podpisywarką”.

W tym miejscu warto dodać, że to aplikacja internetowa wskazuje miejsce, z którego SzafirHost będzie pobierał faktyczne pliki “podpisywarki”. U mnie będą to serwery programu księgowego. Brzmi to w miarę racjonalnie: skoro twórcy aplikacji chcą używać takiego rozwiązania, to jednocześnie są odpowiedzialni za hostowanie wszystkich “binarek” na swoim serwerze, płacenie za transfer i zapewnienie dostępności oraz satysfakcjonującej prędkości pobierania. Integralność ściągniętych plików i tak zostanie sprawdzona przez SzafirHosta, tym samym pobieranie ich z potencjalnie niezaufanego źródła nie jest problemem.
Krótka dygresja: analogicznie działają aktualizacje w systemach opartych na Debianie. Same pliki możemy pobierać chociażby przez HTTP z osiedlowego serwera pana Zdziśka – na koniec system operacyjny i tak upewni się, czy wszystkie pliki są autentyczne i nie zostały w locie podmienione czy też zmodyfikowane.
Przejdźmy do sedna. Dlaczego co miesiąc widzę aktualizację? Wyjaśnienie jest bardzo proste, pliki “podpisywarki” pobierane są do katalogu tymczasowego (dokładnie do %TEMP%). Nowy Windows posiada natomiast funkcję “Storage Sense”, która co jakiś czas ten katalog opróżnia. Błąd powstał organicznie w następstwie dodania nowej funkcji w Windowsie. Wyłączenie tej funkcji rozwiąże mój problem “wiecznej aktualizacji”. Zagadka rozwiązana.
Analiza zabezpieczeń
Skoro znalazłem już odpowiedź na moje pytanie, byłem o krok od zakończenia mojego śledztwa i wzięcia się za prawdziwą pracę. No prawie, gdyby nie to, że minutę później, w tym samym pliku znalazłem kod pętli weryfikującej pobrane komponenty, który można streścić mniej więcej w ten sposób:
// Pseudokod (~Java), opracowanie własne
// executed for each downloaded file:
void verifyFile(DownloadedFile downloadedFile) {
if (downloadedFile.isJAR()) {
if (isTrustedFileHash(downloadedFile)) {
print(downloadedFile.getName() + " - HASH OK");
} else if (checkDigitalSignature(downloadedFile)) {
print(downloadedFile.getName() + " - SIGNATURE OK");
} else {
throw new IntegrityException("JAR is not properly signed.");
}
} else if (downloadedFile.isNativeLib() && isTrustedFileHash(downloadedFile)) {
print(downloadedFile.getName() + " - HASH OK");
} else {
print();
}
}
Co tu jest nie tak? Przeanalizujmy po kolei.
Pliki JAR są weryfikowane z użyciem kaskady dwóch bezpiecznych mechanizmów – jeżeli weryfikacja któregokolwiek pliku nie powiedzie się, to program zgłosi wyjątek i cały proces pobierania zostanie przerwany w wyniku błędu. Świetnie.
A pozostałe pliki? Aplikacja obliczy sumę kontrolną każdego z nich, używając bezpiecznego algorytmu SHA-256 i porówna z listą zaufanych plików. Natomiast jeśli pliku nie ma na liście zaufanych, to… nie zrobi absolutnie nic i przejdzie dalej. Tutaj już mniej świetnie, ale to samo w sobie jeszcze niczego złego nie oznacza. Dlaczego? Już wyjaśniam.
Paczka oprogramowania z “podpisywarką” może zawierać dowolne komponenty o rozszerzeniach: JAR, DLL, SO, JNILIB oraz DYLIB. SzafirHost ściąga wszystkie pliki wchodzące w skład paczki na dysk, a później weryfikuje integralność wszystkich pobranych plików JAR i ładuje kod maszynowy – również wyłącznie z plików JAR.
Tym samym pliki o pozostałych rozszerzeniach, co prawda, zostaną pobrane na dysk użytkownika bez żadnej weryfikacji, ale też nie zostaną uruchomione. No prawie…
Po uruchomieniu się “podpisywarki” strona internetowa może poprosić ją o załadowanie dowolnej biblioteki DLL/SO/DYLIB z dysku twardego. W normalnej sytuacji ten mechanizm umożliwia doczytanie bibliotek niezbędnych do obsługi określonego modelu karty/urządzenia USB służącego do podpisu. Dzięki temu “podpisywarka” jest uniwersalna i obsługuje urządzenia od różnych wystawców podpisów kwalifikowanych.
Piszemy exploit
Co się stanie dalej, tego zapewne niejeden się już domyśla.
- Odtwarzamy paczkę z oprogramowaniem, między innymi na podstawie plików, które są w moim %TEMP%, oraz paru innych elementów.
- Dorzucamy do paczki własną bibliotekę DLL. Celem jest jedynie demonstracja koncepcji, więc DLL-ka nie będzie robiła nic złego. Wyświetli tylko komunikat z napisem “Hello from RCE!”.
- Pożyczamy fragmenty kodu JavaScript wywołującego proces podpisywania dokumentu ze źródła strony mojej aplikacji księgowej.
- Składamy z tego wszystkiego prostą stronę internetową i testujemy.
Po wejściu na stronę moim oczom ukazał się taki dialog:

Okazuje się, że zanim SzafirHost zacznie działać, zapyta użytkownika o zgodę na uruchomienie w kontekście określonej strony internetowej. Jeżeli użytkownik kliknie “Uruchom” i odczeka kilka sekund, to exploit faktycznie zadziała i złośliwy plik DLL zostanie uruchomiony w systemie (z uprawnieniami aktualnie zalogowanego użytkownika).
W tym miejscu moglibyśmy stwierdzić, że jeszcze nie jest tragicznie, albowiem żaden świadomy użytkownik przecież nie zatwierdzi “w ciemno” żądania o odpalenie Szafira z jakiejś losowej strony internetowej.
Ku wielkiemu zdziwieniu, adres wyświetlany jako “lokalizacja wywołania” jest… zwykłym parametrem w JavaScripcie, przekazywanym przez aplikację webową, która chce zainicjować proces podpisu.
const _szafirConfig = {
document_base_url: window.location.origin, // ?!
sdk_location_url: "/szafir_build_directory/",
// ...
}
EXT_postMsg({command: "load", config: _szafirConfig});
Pomysł? Ustawiamy “document_base_url” na “https://podatki.gov.pl” – bo taki adres chciałbym wyświetlić w okienku z pytaniem o zgodę, natomiast w “sdk_location_url” podamy pełny URL do mojego serwera, bo to stamtąd chcemy pobrać złośliwą paczkę z oprogramowaniem.
Absurdalne? Tak. Niemniej jednak to faktycznie działa, a nasze dzieło zniszczenia jest gotowe.
Dialog z pytaniem o zgodę
Powróćmy jeszcze na chwilę do kwestii dialogu, w którym wyrażamy zgodę na uruchomienie SzafirHosta. Znajduje się tam opcja “zapamiętaj zgodę na uruchomienie” i jeżeli użytkownik ją zaznaczy, to kolejnym razem pytanie się już w ogóle nie pojawi. Ponownie, nie chodzi o prawdziwą lokalizację na pasku przeglądarki, tylko o lokalizację definiowaną parametrem “document_base_url” przez wywołującą stronę internetową.
Atakujący może tam ustawić dowolną wartość, na przykład adres jakiejś prawdziwej, popularnej aplikacji używającej SzafirHosta, a większość użytkowników zapewne będzie miała już zapamiętaną zgodę na uruchamianie pod tym adresem, tym samym exploit uruchomi się w pełni automatycznie.
Mechanizm udzielania zgody nie istniał w aplikacji od zawsze, lecz został dodany w nowszych wersjach SzafirHosta. Starsza wersja (chociażby 1.0.7) zawsze uruchomi się automatycznie.
Skutki
Istnieją dwa zasadnicze kierunki wykorzystania podatności. Naturalnie w obu przypadkach zakładamy, że ofiara ma zainstalowany zarówno dodatek do przeglądarki Szafir Web SDK, jak i SzafirHosta.
Wariant #1: Kampania phishingowa
Atakujący celuje w konkretną grupę osób, która często używa “podpisywarki” i robi to w ramach konkretnej aplikacji internetowej, więc najprawdopodobniej ma już zapamiętaną zgodę na automatyczne uruchamianie pod tym adresem. To otwiera możliwość stworzenia złośliwego linku, który bez pytania wgra wirusa w formacie DLL na komputer ofiary, a jedynym warunkiem jest kliknięcie w złośliwy link w mailu.
Wariant #2: Eskalacja z ataku Stored XSS
Atakujący dysponujący podatnością typu stored XSS (zwykle kojarzoną z niskim ryzykiem) w dowolnej aplikacji używającej SzafirHosta może uruchamiać exploit, w momencie gdy następowałoby normalne uruchomienie aplikacji. Użytkownik na pewno udzieli zgody na uruchomienie, bo jest to dokładnie ta akcja, którą w tym momencie chce wykonać.
Zasięg podatności
Według Chrome Web Store w lutym 2026 r. wtyczka Szafir SDK Web posiadała 900 tysięcy użytkowników. Konkretniej, jest to liczba unikalnych instalacji przeglądarek, które załadowały rozszerzenie w ciągu ostatniego tygodnia .
Wszystko wskazuje więc na to, że w środowiskach profesjonalnych oprogramowanie jest raczej szeroko rozpowszechnione, a potencjalne skutki wykorzystania podatności byłyby bardzo poważne.

Dalsze losy tej podatności
Odkrytą podatność zgłosiłem do zespołu CERT Polska w ramach procesu Coordinated Vulnerability Disclosure i została ona już załatana.
Zespół CVD CERT.PL w związku ze zgłoszeniem opublikował CVE:
- CVE-2026-26927 – URL (HTTP Origin) call location spoofing in Szafir SDK Web;
- CVE-2026-26928 – Lack of Dynamic Library Validation in SzafirHost;
Oś czasu obsługi incydentu:
| 2026-01-13 | Odkrycie podatności i opracowanie działającego exploita. |
| 2026-01-15 | Przesłanie raportu do producenta oprogramowania – KIR S.A. oraz do CVD CERT.PL. |
| 2026-01-18 | Uzupełnienie raportu przesłane do KIR S.A. oraz CVD CERT.PL. |
| 2026-01-22 | Potwierdzenie występowania podatności przez CSO KIR S.A. |
| 2026-01-27 | Spotkanie osobiste w KIR S.A. – omówienie podatności. |
| 2026-02-03 | Kontakt ze strony SOC KIR – producent przesłał poprawioną wtyczkę Szafir SDK Web, program SzafirHost oraz build client-side z prośbą o zweryfikowanie, czy podatność została poprawnie załatana. |
| 2026-02-03 | Przesłałem opinię na temat sposobu załatania podatności do producenta (brak zasadniczych uwag). |
| 2026-02-11 | Wydanie aktualizacji SzafirHost 1.1.0. |
| 2026-02-11 | Wydanie załatanej wersji Szafir SDK Web 0.0.17.3 dla Chrome. |
| 2026-03-11 | Wydanie załatanej wersji Szafir SDK Web 0.0.17.7 dla Firefoxa (opóźnienie spowodowane koniecznością oczekiwania na zatwierdzenie aktualizacji przez Mozillę). |
| 2026-04-02 | Publikacja CVE-2026-26927 oraz CVE-2026-26928 przez CVD CERT.PL. |
Podsumowanie
Na tym kończy się pierwsza część mojej analizy, ale to definitywnie nie jest koniec całej sprawy. Przypadek opisany dzisiaj to najłagodniejsza spośród trzech podatności, jakie udało mi się znaleźć w obszarze implementacji podpisów kwalifikowanych.
Dalsza część tej historii znajduje się w kolejnych częściach artykułu, a im dalej w las, tym gorzej…
Następna część: Hakowanie e-Sądu YubiKeyem – [Badanie e-podpisów, cz. 2]
Hashe SHA-256 analizowanych plików
04554ebc55e07c146c5ca27a282d92ec4cf416a33332d0ae8544d626ac3aaf14 *szafir_sdk_web_chrome_ext_0_0_17_2.crx
0d44272747f9b56af46953b930c95bda5d19b767bc598375b6022d63b42a8eae *szafir_sdk_web_chrome_ext_0_0_17_2.zip
a8d8e0b92eaee62071caff5e5a0a15105b2bcdd946896949f0e3e2b41ce21b2c *szafir_sdk_web-0_0_17_2.xpi
ff4c89405d51b41df09dd22179e45a1838c69cc59942adb953194f9b20214ce6 *SzafirHost.jar [version 1.0.15]
fca67396c5edd28c20061584523efe1edf412b5615a9bba7a354bc5bf7dd63ae *SzafirHost.jar [version 1.0.7]