/OD0DOPENTESTERA

Problematyczne znaki UTF - Unicode Case Mapping Collisions

Jeżeli nie korzystasz z menadżera haseł to zapewne przynajmniej raz spotkała Cię sytuacja w której zapomniałeś hasła do jakiejś strony internetowej, zwłaszcza jeżeli nie korzystasz z niej zbyt często. Wybawieniem w takich sytuacjach jest funkcja przywracania hasła.

Unicode Case Mapping Collisions

Dzisiaj opowiem o jednym ze sposobów obejścia tej funkcjonalności - czyli zresetowania hasła innego użytkownika. W odpowiednich okolicznościach błąd tego rodzaju może doprowadzić do przejęcia konta administratora i zaatakowania całej aplikacji. Wykorzystam tutaj mało znany błąd Unicode Case Mapping Collisions.

from flask import Flask, render_template, request
from flask_mysqldb import MySQL
app = Flask(__name__)
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = ''
app.config['MYSQL_DB'] = 'test'
mysql = MySQL(app)
@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == "POST":
        details = request.form
        email = details['email']
        cur = mysql.connection.cursor()
        cur.execute("SELECT email from users WHERE email = %s", [email.lower()])
        rv = cur.fetchall()
        if len(rv) == 1:
            return ("Wysyłam mail do {}".format(email))
        else:
            return ("Email nie istnieje")
    else:
        return '<form method="post" action=""><input type="text" name="email"><input type="submit"></form>'
if __name__ == '__main__':
    app.run()

Mamy tutaj prosty kod symulujący resetowanie hasła użytkownika. Jeżeli zapomniałeś hasła, podajesz swój adres email. Dalej system zamienia wszystkie znaki adresu na małe litery. Jest to zatem przykład typowej normalizacji danych. Jeden email to jedno konto użytkownika - nie chcemy aby użytkownik zakładał wiele kont odpowiednio modyfikując swój adres. Gdybyśmy tego nie zrobili system traktowałby adres [email protected] i [email protected] (pisane z dużych liter) jako osobne adresy email.

1 email to 1 konto

Jeżeli zamienimy wszystkie znaki na małe litery upewniamy się, że jeden email to jedno konto w systemie. Następnie poszukujemy tego adresu w bazie danych. Jeżeli odnajdziemy adres w bazie, generujemy unikalny link służący do resetu hasła i wysyłamy email do użytkownika.

Teraz wystarczy już tylko odczytać pocztę internetową i kliknąć w znajdujący się tam odnośnik. System sprawdza czy wygenerowany kod jest prawidłowy i pozwala na zmianę hasła użytkownika. Gdzie zatem znajduje się dzisiejszy błąd?

Resetowanie konta

Na początku ery komputerów litery zapisywano przy użyciu kodowania ASCII. System ten pozwala na zapisanie ograniczonej liczby znaków. A co z językami, które oprócz liter z alfabetu łacińskiego języka angielskiego używają swoich dodatkowych? Chociażby - co z naszymi polskimi “ogonkami” czyli literami ć, ż, ś? I tak powstał UTF - system kodowania wykorzystujący od 1 do 4 bajtów do zakodowania pojedynczego znaku. Dzięki niemu można zapisać większość znaków wykorzystywanych przez ludzi na całym świecie. Nie dziwi, że większość języków programowania posiada wbudowana obsługę tego standardu.

Eszett

Jednak to wszystko nie jest takie proste. Weźmy pod uwagę zamianę liter z dużych na małe. W przypadku ASCII - sprawa była prosta. Dużą literę A zamieniamy na małe a. Dużą literę B zamieniamy na małe b. A co w przypadku innych alfabetów?

W języku niemieckim istnieje litera Eszett. Jest wymawiana w identyczny sposób jak dwuznak ss. Co zatem stanie się jeżeli spróbujemy zamienić tą literę na jej duży odpowiednik przy użyciu funkcji upper? Popatrzmy na konsolę języka Python.

>>> 'ß'.upper()
'SS'

Odpowiedź nie jest taka oczywista. W języku Python otrzymujemy dwie litery `SS`. Zamieniliśmy zatem jeden znak na dwa różne. Podobna sytuacja występuje również w odwrotnym przypadku - kiedy to jakiś znak próbujemy zamienić na jego mały odpowiednik przy użyciu funkcji lower.

Pełna lista takich dwuznaczności znajduje się na stronie Awsome Unicode

Dwuznaczności unicode

Przykładem może być symbol Kelwin - jednostki temperatury w układzie SI. Przypomina on literę K - jednak jest to zupełnie inny znak. Przy próbie jego zamienienia na małe znaki przy użyciu odpowiedniej funkcji otrzymujemy jednak małą literę k.

Ale jak to się ma do resetu hasła? Na wstępie powiedziałem, że pobrany przez użytkownika adres email zamieniamy na jego reprezentację zapisaną przy pomocy mały znaków. Jeżeli więc w adresie podamy znak kelwina zamiast litery k - baza danych nie odnajdzie takiego rekordu. Tylko, że my przed poszukiwaniami w bazie zamieniamy wszystkie znaki na ich małe odpowiedniki.

Symbol Kelwin

System zamieni więc symbol kelwin na literę k - a taki wpis istnieje w bazie - poszukujemy bowiem zwykłego emaila [email protected] - gdzie k jest teraz zwyczajną literą k. Samo takie zachowanie nie jest jeszcze podatnością. Problem w tym na jaki adres wysyłamy email z kodem do resetowania hasła. W prawidłowym rozwiązaniu powinniśmy pobrać adres zwrócony przez bazę danych i właśnie na ten adres wysłać wiadomość z kodem. Wtedy mamy pewność, że wysyłamy email na właściwy adres, który rzeczywiście istnieje w bazie. W przykładowym kodzie jednak wysyłamy wiadomość na adres, który podał użytkownik bezpośrednio przy pomocy argumentu email przy pomocy formularza.

Dziwny email

Programista założył tu bowiem, że ta wartość i wartość zwracana z bazy danych są identyczne. Teoretycznie jest bowiem niemożliwe że baza zwróci inny rekord. Programista nie wiedział jednak, że tak nie jest - bo funkcja lower zachowuje się niestandardowo.

Jak zatem może wyglądać realny atak na aplikację? Wiemy, że konto administratora w serwisie używa adresu [email protected]. Załóżmy, że email.local to darmowy serwer pocztowy umożliwiający założenie darmowego konta. Zakładamy więc nową skrzynkę mailową - na adres kacperszurek - gdzie K pisane jest przy użyciu znaku kelwin. Teraz możemy już przejść do procedury resetowania hasła.

Drobne różnice

W polu email podajemy nasz adres - ze znakiem kelwin. Teraz system zamienia wszystkie znaki na małe odpowiedniki. Dzięki temu w bazie poszukiwany jest rekord [email protected] - pisany przy użyciu liter z alfabetu łacińskiego. Taki rekord istnieje i należy do administratora - system generuje więc kod do resetowania hasła i wysyła je pod nasz adres email - czyli ten zapisany z użyciem symbolu kelwin.

Atak

Nie pobiera bowiem adresu z bazy, a posługuje się tym przekazanym przez użytkownika. Teraz możemy już odebrać wiadomość i zresetować hasło admina.

Jeżeli zatem masz do czynienia ze znakami UTF w swojej aplikacji - sprawdź czy i ona nie jest podatna na tego rodzaju błąd. Mało kto spodziewa się bowiem jakie ciekawostki może kryć funkcja lower i upper w niektórych językach.

Mój przykład bazuje na języku Python. Dokładnie taki błąd występował na GitHubie1