Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Rozwiązanie konkursu unpickle
Kilka dni temu w artykule o unpickle ogłosiliśmy konkurs, polegający na wykorzystaniu omawianej podatności w celu uzyskania XSS-a. Jeszcze tego samego dnia przyszło do nas pięć rozwiązań (ale najszybszy był Adam Dobrawy). W rozwiązaniach zastosowano trzy różne podejścia do problemu; w tej notce przedstawię ja wraz z krótkim opisem.
Zanim jednak do tego przejdę, najpierw pokażę małą sztuczkę, przydatną do znalezienia odpowiedniej metody wykorzystania podatności. Jak wiadomo, Python dysponuje interaktywną konsolą, w której polecenia wpisywane przez użytkownika są natychmiast wykonywane. Konsola może być też wywołana z poziomu skryptu pythonowego – metodą code.interact(). Zatem ciało funkcji malicious() jest następujące:
def malicious(): import code code.interact()
Wówczas, gdy wkleimy kod wynikowy do aplikacji, uzyskamy na serwerze dostęp do pythonowego shella w kontekście naszej aplikacji.
Jeśli używacie IPythona, można przejść również do niego:
def malicious(): import IPython IPython.embed()
1. Utworzenie własnego obiektu z atrybutami year, month i day
Trzy rozwiązania wykorzystywały lukę w 39. linii kodu przykładowej aplikacji:
output = TEMPLATE.format(date.year, date.month, date.day, date_encoded)
Aplikacja wstawia elementy date.year, date.month oraz date.day do szablonu, nie chroniąc się w żaden sposób przed XSS-em (zakłada, że będą one liczbami). Można zatem utworzyć dowolny obiekt z tymi atrybutami i wykonamy XSS-a. W każdym z rozwiązań użyto innego sposobu, by to osiągnąć.
def malicious(): # sposob 1. NamedTuple from collections import namedtuple obj = namedtuple('obj', ['year', 'month', 'day']) # sposob 2. dynamiczny sposob tworzenia klas obj = type('obj', (object,), dict(year=None, month=None, day=None)) # sposob 3. utworzenie klasy starego typu class A: pass obj = A() obj.year = '<h1>XSS here</h1>' obj.month = 0 obj.day = 1 return obj
Jak widać, w takim rozwiązaniu panowała dość duża dowolność w osiągnięciu celu. Wynik wykonywania:
2. Przeładowanie cgi.escape
Rozwiązanie zwycięskie – całkiem ciekawe, bo trwale psujące serwer w taki sposób, że każdy, kto trafi na jakąkolwiek stronę błędu zostanie XSS-owany. Spójrzmy na funkcję wyświetlającą błędy na serwerze:
def output_error(self, msg): self.wfile.write('<p><strong style="color: red;">{}</strong></p>'.format(cgi.escape(msg)))
Funkcja cgi.escape() służy do ochrony przed XSS-ami (odpowiednik htmlentities() z PHP). Widzimy, że funkcja jest wywoływana za każdym razem, gdy wyświetlany jest błąd – możemy więc funkcję przeładować i zmienić jej zachowanie.
def malicious(): import sys sys.modules['cgi'].escape = lambda x: "<h1>XSS here</h1>"
Przez obiekt sys.modules możemy uzyskać dostęp do wszystkich zaimportowanych modułów w danej aplikacji. Bierzemy więc moduł cgi i zmieniamy działanie funkcji escape(). Zapis lambda x: „<h1>XSS here</h1>” jest równoważny:
def __anon(x): return "<h1>XSS here</h1>"
Wynik wykonania:
Jako że działanie cgi.escape() zostaje trwale zmienione, od tej pory każde wyświetlenie komunikatu o błędzie powoduje wykonanie XSS-a. (dzieje się tak testowym serwerze; efekt wcale nie musiałby być trwały na prawdziwych serwerach rozrzuconych w chmurach/farmach korzystających z jakichś form wielowątkowości).
3. Uzyskanie dostępu do obiektu klasy MyHandler
Ten sposób został również użyty na moim zrzucie ekranowym w oryginalnym artykule. Jak było widać w kodzie aplikacji, wyświetlanie tekstu na stronie jest obsługiwane przez klasę MyHandler, poprzez odwołania do atrybutu wfile instancji tego obiektu, na którym z kolei wykonywano metodę write().
class MyHandler(BaseHTTPRequestHandler): def output_error(self, msg): self.wfile.write('<p><strong style="color: red;">{}</strong></p>'.format(cgi.escape(msg)))
Zatem w chwili, gdy wykonujemy nasz kod poprzez unpickling, gdzieś w pamięci musi żyć obiekt tej klasy. Trzeba tylko znaleźć sposób na jego znalezienie.
Posłużymy się modułem inspect, który, jak podaje dokumentacja, służy do wyciągania informacji o żywych obiektach. W szczególności, metoda inspect.stack() zwraca pełny stos prowadzący do aktualnego miejsca w kodzie w postaci listy krotek, gdzie każda krotka składa się z sześciu elementów:
- Referencja do ramki stosu,
- Nazwa pliku,
- Numer linii kodu,
- Nazwa funkcji,
- Kontekst kodu,
- Indeks aktualnie wykonywanej linii w kontekście kodu.
Punkty 5. i 6. mogą brzmieć mało klarownie, ale i tak nie są nam potrzebne ;) Nas interesuje nazwa funkcji oraz referencja do ramki stosu. Jak pamiętamy, złośliwy kod w serwerze był wywoływany z funkcji do_GET() (tam były deserializowane dane). Poszukamy zatem w stosie wywołań funkcji „do_GET” i odwołamy się do jej ramki stosu. Z tej ramki stosu wyciągniemy z kolei zmienne lokalne (atrybut f_locals), co umożliwi uzyskanie referencji do obiektu self. Mając to wszystko za sobą, wystarczy już wywołać self.wfile.write() i możemy pisać na wyjściu cokolwiek chcemy ;)
def malicious(): import inspect for (frame, filename, lineno, func_name, context, contextno) in inspect.stack(): if func_name == 'do_GET': self = frame.f_locals['self'] self.wfile.write('Hey! I can write whatever <u>I want</u>!') return
I wynik wykonania: