Wykorzystanie Burp Pythone Scipts do szyfrowania żądań za pomocą kluczy RSA
W tym artykule zamierzam przedstawić kolejny mechanizm zabezpieczający żądania API, którego implementacja była znaczącym elementem projektu w świecie rzeczywistym. Omawiany mechanizm wyróżnia się dodatkową warstwą bezpieczeństwa poprzez szyfrowanie treści żądań za pomocą kluczy RSA. Stanowi to dodatkowe zabezpieczenie, które może skutecznie uzupełnić standardowy protokół HTTPS. Ponadto, w późniejszej części artykułu zostaną wprowadzone podpisy przy użyciu kluczy RSA jako wisienka na torcie, zapewniające weryfikację integralności wysyłanych komunikatów. Cała komunikacja będzie również dodatkowo zabezpieczona przed potencjalnymi atakami typu replay.
Wprowadzenie
W poprzednim artykule, omówiłem wykorzystanie skryptów Pythonowych w Burp Suite do podpisywania żądań za pomocą kluczy RSA. Dziś chciałbym wrócić do projektu, w którym miałem przyjemność uczestniczyć, a który wykorzystywał API do komunikacji z aplikacją mobilną. Podczas przesyłania wrażliwych danych, takich jak na przykład szczegóły karty kredytowej, autor API zdecydował się wprowadzić dodatkowe mechanizmy bezpieczeństwa mające na celu ochronę przed modyfikacją danych i utrudnienie manipulacji żądaniami w celu ich dalszego wykorzystania.
Ten temat wydał mi się interesujący, zwłaszcza biorąc pod uwagę brak podobnych implementacji w kontekście Burp Suite. Dlatego postanowiłem podzielić się tym doświadczeniem.
Podsumowując, autor projektu, oprócz mechanizmu podpisywania żądań (nagłówek X-Signature
) i ochrony przed atakami typu replay (nagłówki X-Nonce-Value
i X-Nonce-Created-At
), które opisałem w poprzednich artykułach, zdecydował się również na szyfrowanie treści żądań za pomocą kluczy RSA. Wszystkie te środki bezpieczeństwa znacząco utrudniają atakującym zrozumienie komunikacji, szczególnie przy braku wewnętrznej wiedzy na temat implementacji.
Ominięcie pinningu w aplikacjach mobilnych może być wyzwaniem, a w przypadkach, gdy autor aplikacji używa niestandardowych rozwiązań, dostęp do API staje się jeszcze bardziej skomplikowany, czasami wymagając tak jak w tym projekcie również odszyfrowania treści wiadomości.
Aby przeprowadzić skuteczne testy bezpieczeństwa API, początkowo rozważałem użycie dostarczonej biblioteki. Jednak to rozwiązanie miało swoje ograniczenia, takie jak potrzeba częstych zmian w kodzie biblioteki oraz ograniczenia co do ilości i rodzaju testów, które mogłem przeprowadzić. Ostatecznie zdecydowałem się wykorzystać wtyczkę Burp o nazwie Python Scripter.
Opis zaimplementowanego algorytmu
W porównaniu do mechanizmu opisanego w wcześniejszym artykule “Wykorzystanie Burp Python Scripts do podpisywania żądań za pomocą kluczy RSA”, obecny algorytm wprowadza dodatkowe etapy związane z szyfrowaniem i deszyfrowaniem treści żądań. W nowej wersji cały proces wygląda następująco:
- Aplikacja szyfruje treść żądania za pomocą klucza publicznego otrzymanego od odbiorcy (naszego serwera).
- Aplikacja konstruuje ustandaryzowany schemat żądania zawierający zaszyfrowaną wiadomość.
- Aplikacja dodaje do żądania wartość nonce, używając uuid4 (nagłówek
X-Nonce-Value
). - Aplikacja dodaje do żądania aktualną datę (nagłówek
X-Nonce-Created-At
). - Aplikacja generuje podpis żądania, używając swojego klucza prywatnego, treści wiadomości, wartości nonce oraz aktualnej daty.
- Aplikacja dodaje podpis do żądania (nagłówek
X-Signature
). - Serwer API weryfikuje podpis, używając publicznego klucza klienta.
- Jeśli podpis jest prawidłowy, serwer API akceptuje żądanie. Jeśli podpis jest nieprawidłowy, żądanie jest odrzucane.
- Serwer API deszyfruje wiadomość, używając swojego klucza prywatnego.
Algorytm omówiony tutaj pomija szczegóły dotyczące wymiany kluczy publicznych między stronami komunikacji. Jako że jest to poza zakresem tego artykułu, zakładamy, że osoby implementujące ten mechanizm już rozważyły wszystkie dostępne metody wymiany kluczy. Dla pentesterów weryfikacja tego w praktyce pozostaje jedynym zadaniem :)
W następnej części artykułu skupimy się na uproszczeniu całego procesu, używając tylko jednej pary kluczy RSA w kontekście PoC.
Implementacja
W wcześniejszym artykule, przedstawiłem rozbudowę prostego skryptu, który aktualizuje wartość nagłówka X-Nonce-Value
w każdym wysłanym żądaniu, do etapu, gdzie każde wysłane żądanie było podpisywane za pomocą prywatnego klucza RSA i zabezpieczane przed atakami typu replay. Wprowadziłem również sposób wywoływania poleceń systemowych za pomocą biblioteki subprocess
, umożliwiając wykonanie dowolnego polecenia w systemie z poziomu Pythona. Funkcjonalność wywoływania dodatkowych poleceń systemowych jest niezwykle istotna, biorąc pod uwagę ograniczenia pod względem dostępnych bibliotek i wersji Pythona 2.7 w środowisku Python Scripts i Burp Sute.
Następnie skupiłem się na procesie budowania podpisu. Pokazałem, jak prawidłowo sformułować wartość podpisu, uwzględniając kodowanie oraz białe znaki w treści żądania. Dodatkowo omówiłem kwestię deserializacji JSON w kontekście różnic między Pythonem 2.7 a Pythonem 3.
Do szyfrowania treści wiadomości również wykorzystamy możliwość wywołania dowolnego polecenia systemowego. Jednak zanim to zrobimy, musimy odpowiednio przygotować naszą wiadomość do szyfrowania. W tym przypadku mam dobre wieści - nie musimy już martwić się różnicami w serializacji JSON między Pythonem 2.7 a Pythonem 3. Możemy zdeserializować wiadomość, aby usunąć niepotrzebne dodatkowe białe znaki, a następnie zserializować i zakodować ją w base64. Kod będzie wyglądał następująco:
msg = helpers.bytesToString(requestBody)
msg = json.loads(msg)
msg_b64 = base64.standard_b64encode(json.dumps(msg, ensure_ascii=False).encode()).decode()
Następnie wywołujemy polecenie openssl, aby zaszyfrować wiadomość. Z otrzymanego wyniku, musimy jeszcze usunąć znaki nowej linii (\n
):
cmd = 'printf %s "{}" | openssl pkeyutl -encrypt -pubin -inkey {} | openssl base64'.format(msg_b64, PUBLIC_KEY)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
encrypted_body = output.decode().replace("\n", "")
Ostatnim krokiem jest uzupełnienie szablonu wiadomości, która zostanie wysłana do serwera:
newPayload = '{\"keyId\": \"init\", \"encryptedPayload\": \"PAYLOAD\"}'.replace("PAYLOAD", encrypted_body)
Dlaczego budujemy wiadomość w ten sposób, zamiast użyć serializacji? Ze względu na wcześniej wspomniane różnice w serializacji pomiędzy Python 2.7 a Python 3, chcemy być pewni, że kolejność kluczy w wiadomości nigdy się nie zmieni.
Z implementacji, to już jest wszystko. Dla zainteresowanych, poniżej znajduje się cały kod napisany w Pythonie, który implementuje opisany mechanizm szyfrowania żądań:
# Request body encryption script with RSA for Burp Suite
# Python-Scripter
# Michal Walkowski - https://mwalkowski.com/
# https://github.com/mwalkowski
#
# Tested Burp Professional v2023.11.1.4 and Python Scripter 2.1
# For RSA Encrypting Process
# Based On: https://mwalkowski.com/post/using-burp-python-scripts-to-encrypt-requests-with-rsa-keys/
import base64
import subprocess
import json
PUBLIC_KEY = "public.pem"
if messageIsRequest:
requestInfo = helpers.analyzeRequest(messageInfo.getRequest())
headers = requestInfo.getHeaders()
requestBody = messageInfo.getRequest()[requestInfo.getBodyOffset():]
msg = helpers.bytesToString(requestBody)
msg = json.loads(msg)
msg_b64 = base64.standard_b64encode(json.dumps(msg, ensure_ascii=False).encode()).decode()
cmd = 'printf %s "{}" | openssl pkeyutl -encrypt -pubin -inkey {} | openssl base64'.format(msg_b64, PUBLIC_KEY)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output, err = process.communicate()
if err.decode() != "":
raise Exception(err)
encrypted_body = output.decode().replace("\n", "")
newPayload = '{\"keyId\": \"init\", \"encryptedPayload\": \"PAYLOAD\"}'.replace("PAYLOAD", encrypted_body)
request = helpers.buildHttpMessage(headers, newPayload)
messageInfo.setRequest(request)
Przykład użycia
Poniżej prezentuję działanie opisanych skryptów, dołączam również wideo:
Na Rysunku 1 przedstawiono wysyłanie żądania w postaci zwykłego tekstu - niezaszyfrowanego, które zgodnie z implementacją jest przetwarzane przez serwer. Jak widać w odpowiedzi serwera, przetwarzanie żądania nie powiodło się. Serwer oczekuje wiadomości z polem encryptedPayload
.
Na Rysunku 2 przedstawiono to samo żądanie, tym razem z wykorzystaniem napisanego skryptu. Jak wynika z otrzymanej odpowiedzi serwera, żądanie zostało poprawnie przetworzone.
Na Rysunku 3 przedstawiono faktyczne żądanie wysłane do serwera, które zostało zalogowane w Burp Logger. Możemy zaobserwować, że treść wiadomości została całkowicie przekształcona do formatu oczekiwanego przez nasz serwer.
Jak wspomniałem wcześniej, jako ostatni szlif do wysłanej wiadomości, zostanie dodany podpis przy użyciu kluczy RSA. Do tego celu użyjemy skryptu, który został przedstawiony w poprzednim artykule. Aby zapewnić prawidłowe działanie w Python Scripts, należy zachować kolejność dodawania skryptów, jak pokazano na Rysunku 4. Kolejność ta jest istotna, ponieważ skrypty będą wykonywane w tej kolejności podczas wysyłania naszego żądania. Najpierw zostanie wykonane szyfrowanie, a następnie dodany zostanie podpis.
Na Rysunku 5 przedstawiono wysłane żądanie z perspektywy Burp Logger, gdzie brakuje wcześniej wspomnianych nagłówków odpowiedzialnych za nasz podpis, mianowicie X-Nonce-Value
i X-Nonce-Created-At
, oraz X-Signature
.
Wysłane żądanie, które jest poprawnie interpretowane przez serwer, przedstawiono na Rysunku 6.
Na Rysunku 7 pokazano wysłane żądanie zalogowane w Burp Logger. Jak widać, wiadomość wysłana do serwera została zaszyfrowana. Dodatkowo dodano nagłówki X-Nonce-Value
i X-Nonce-Created-At
, a wiadomość została podpisana.
Podsumowanie
W niniejszym artykyle przedstawiłem mechanizm szyfrowania treści żądań API przy użyciu kluczy RSA, które może służyć jako dodatkowa warstwa bezpieczeństwa obok standardowego protokołu HTTPS. Mechanizm tutaj przedstawiony został wdrożony w rzeczywistym projekcie.
Przedstawiony w niniejszym artykule fragment kodu do wtyczki Burp Suite Python Scripts znacznie ułatwia dalsze testowanie API, ponieważ nie ma już potrzeby skupiania się na szyfrowaniu wiadomości czy zapewnieniu poprawności wszystkich nagłówków.
Ten artykuł ma również na celu przedstawienie alternatywnych mechanizmów zabezpieczających integrację wysyłanych żądań poza HMAC i pokazuje sposób ich testowania. Jeśli ktoś jest zainteresowany testowaniem funkcjonalności skryptu, może znaleźć prosty kod po stronie serwera implementujący weryfikację podpisu i szyfrowanie wiadomości pod tym linkiem.
Bibliografia
- https://en.wikipedia.org/wiki/Replay_attack
- https://www.zimuel.it/blog/sign-and-verify-a-file-using-openssl
- https://sereysethy.github.io/encryption/2017/10/23/encryption-decryption.html
- https://httpwg.org/http-extensions/draft-ietf-httpbis-message-signatures.html#name-rsassa-pkcs1-v1_5-using-sha
- https://github.com/PortSwigger/python-scripter
- https://github.com/lanmaster53/pyscripter-er/tree/master/snippets
- https://github.com/mwalkowski/api-request-security-poc/
- https://stackoverflow.com/questions/51769239/why-json-dumps-in-python-3-return-a-different-value-of-python-2
- https://mwalkowski.com/post/using-burp-python-scripts-to-sign-requests-with-rsa-keys/