Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

SQL crash course met Joomla

In een andere post gaf ik aan dat er kritieke SQL injections mogelijk waren in Joomla.
Dit gaf ook de aanleiding om verder te onderzoeken waar de bugs zaten en wat er precies fout is. Aan de hand van deze case, geven we een crash course SQL injections by example. We laten stap voor stap zien wat er nu precies fout is, hoe we de bug gaan vinden en hoe we het zelf kunnen patchen.

Alle drie de bugs lijden aan dezelfde mankement, niet valideren van user input. Echter de term user input is zooooo vaag, dat we dat even helder moeten uitleggen wat nou allemaal user input is.

Wat is user input?

Zoals de term user input het al zegt, het is invoer van de gebruiker. Nu zullen heel veel ontwikkelaars alleen denken aan $_GET. Okay, er zijn nog slimme onder ons die ook nog denken aan $_POST. Echter het blijft niet beperkt tot deze super globals. Een ander belangrijke vector zijn HTTP headers. Deze data wordt doorgegeven aan super global $_SERVER. Zoals Mathieu in "Hoe $_SERVER[’PHP_SELF’] een XSS probleem kan veroorzaken" laat zien is $_SERVER niet heilig. Een vaak over het hoofd gezien vector is $_COOKIE, ja de koekjes. Deze zijn ook te manipuleren en is dus ook user input. In feite zijn cookie gegevens gewoon HTTP headers. Om kort te zijn, alle super globals in PHP zijn user input en ga er altijd vanuit de user input niet te vertrouwen is en valideer deze altijd.

SQL injections

SQL injections komen vaak voor in web applicaties. Samen met Cross Site Scripting (XSS) spannen zij de kroon. Ongevalideerde user input dat wordt gebruikt in queries leiden tot SQL injections. Een voorbeeld:

PHP:
  1. $user_input = "scriptorama'; DELETE from auth;"; // user input
  2. mysql_query("SELECT * FROM blogs WHERE naam ='". $user_input ."'");

Ja, dit wil je ten alle tijden voorkomen! Een voordeel voor MySQL gebruikers is dat MySQL geen query stacking ondersteund. Met andere woorden, je kunt niet meerdere queries in een statement met mysql_query() gebruiken. Bij andere RDMS is dit wel mogelijk.

PHP heeft nog magic quotes, in PHP6 is deze verwijderd, dat backslashes zet voor single en dubbel quotes voor GPC ($_GET, $_POST en $_COOKIE). Alleen deze escaped niet alle karakters en deze feature is ook nog een optionele configuratie. Vertrouw daarom niet op magic quotes. Elke RDMS dat PHP ondersteunt hebben hun eigen escape functies, zoals mysql_real_escape_string() en pg_escape_string(). Gebruik deze altijd.

Een mooi voorbeeld van SQL injection is bij logins. Hieronder simuleer ik een login request.

PHP:
  1. $input_username = "Tri' --";
  2. $input_hash = 'sha1_hash';
  3. $query =
  4. "SELECT *
  5. FROM users
  6. WHERE
  7. username = '". $input_username ."'
  8. AND
  9. passw = '". $input_hash ."'";

In SQL zijn #, -- en /* comments. Alle wat dus in commentaar staat wordt genegeerd. In feite wordt dus de volgende query uitgevoerd: SELECT * FROM users WHERE username = 'Tri'
Het wachtwoord controle wordt omzeild en je login is succesvol. Het enige wat je dus moet hebben is een geldige loginnaam. Deze opzet komt heel veel voor en is heel makkelijk te omzeilen.

Hoe voorkom je SQL injections

In vier woorden: escape je user input. Voor strings, gebruik altijd de specifieke RDMS escape functies. Moet je integers gebruiken, gebruik dan intval() en niet string escape functies. Hieronder staat een voorbeeld met het escapen van integers met een string escape functie.

PHP:
  1. $input = "0; DELETE FROM users";
  2. $input = pg_escape_string($input); // 0; DELETE FROM users

Het escapen werkt dus niet met integers en met puntkomma, de laatste heeft namelijk geen betekenis en blijft dus intact. Je kunt de integers tussen quotes zetten, maar dat is geen correcte SQL. Strings horen tussen quotes, niet integers. De oplossing is dus valideren met ctype_digit() of intval() gebruiken of het casten van types. Ik zelf gebruik altijd intval() bij WHERE clauses, bij foute input geeft intval() 0 terug en deze bestaat niet (althans, daar ga ik vanuit. Oops, misschien is dat toch fout?). Dan krijg je meestal de melding "Kon niet gevonden worden blabla".

PHP:
  1. $num = 5;
  2. if (ctype_digit($num) === TRUE) {
  3. // geldig
  4. }
  5.  
  6. // casten
  7. $input = "1337; DELETE FROM users";
  8. $input = (int) $input; // 1337
  9.  
  10. //intval
  11. mysql_query("SELECT * FROM articles WHERE article_id = ". intval($_GET['id']));

Misschien heb je in het verleden een simpele zoekmachine geprogrammeerd. Een simpele zoek query kun je realiseren met de LIKE operator. Deze heeft twee tokens: % en _. Het trieste (of leuke, ligt eraan hoe je het bekijkt ;)) is dat string escape functies deze tokens negeren, wat dus bij de puntkomma ook het geval is. Ook is de underscore vaak een geldige karakter voor input. Je kunt hiermee een low-tech DoS aanval produceren. Voorbeeld:

PHP:
  1. mysql_query("SELECT * FROM customers WHERE name LIKE '". $input ."%'");

Wanneer je $input laat beginnen met de LIKE tokens vertraag je de boel, want de indexes werken dan niet. Ook kun je natuurlijk de tokens als laatste karakter gebruiken wat de relevantie van het zoeken ondermijnt. Om dit op te lossen moet je de tokens dus escapen en dat doen we met addcslashes().

PHP:
  1. $safe = addcslashes(mysql_real_escape_string("%foo_"), '%_');
  2. // \%foo\_

Je kunt aan addcslashes aangeven welke karakters je wilt escapen. We escapen eerst de input met de database escape functie en daarna met addcslashes.

Het kan zijn dat je met binaire data moet werken, hiervoor zijn ook specifieke escape functies. Zoek ze op en gebruik ze. Helaas hebben niet alle RDMS escape functies. Hiervoor moet dus een andere oplossing voor zijn. Die is er ook, namelijk prepared statements. Dit zijn in feite sjablonen voor queries. Eerder schreef Mathieu hier al over wat dit nou precies is. Prepared statements voorkomen SQL injections.

Real world example

We gaan de bugs van Joomla onderzoeken. Allereest, moeten we een hint hebben waar de bugs zitten. Sommigen gebruiken een patch file, dat is gegenereerd met een diff utility. In PHP land zie je dit niet vaak terug. Jammer, want zo kun je snel zien waar de bugs zitten. Met een diff utility kun je de verschillen zien tussen twee of meer bestanden. Joomla hanteert geen patch files, dus moeten we opzoek naar een andere manier. Hiervoor gebruiken we de changelog van Joomla 1.0.10 (nieuwste versie, met de bugs fixed), die hier te vinden is. Hierin staan de volgende gegevens:

03 HIGH Level Threats fixed in 1.0.10

A1 Unvalidated Input
* A1 - Secured `Remember Me` functionality against SQL injection attacks
* A1 - Secured `Related Items` module against SQL injection attacks
* A1 - Secured `Weblinks` submission against SQL injection attacks

03 HIGH Level Threats. Juist, dat moeten we onderzoeken! In feite komen alle drie de bugs op hetzelfde neer. Daarom zullen we een voorbeeld uitgebreid behandelen. We nemen de derde bug, die met weblinks. Voordat we aan de slag gaan, moeten we eerst twee versies van Joomla downloaden. Op deze pagina is een overzicht van alle 1.x releases, download de releases van 1.0.10 en 1.0.9.
Nu hebben we nog een diff utility nodig. Op UNIX-achtige bakken heb je diff, alleen heb je hiervoor geen grafische interface. Ik gebruik editor PSPad, dat alleen voor Windows beschikbaar is. Voor de UNIX-geeks hebben verschillende editors ook een diff feature, bekijk of je favoriete editor deze heeft.

Voor PSPad: Open nu van release 1.0.9 de volgende file: components/com_weblinks/weblinks.class.php. Klik nu met je rechtermuis op het tabblad van het bestand en kies optie "Text Diff with This File...". Kies nu hetzelfde bestand, maar dan van release 1.0.10. Zie screenshot.
Diff PSPad
Rond regel 84 moet je een verschil zien. Hieruit kan je dus afleiden dat de bug rondom deze regels zit. Het blijkt dat functie check() een SQL injection bug heeft.

PHP:
  1. /** check for existing name */
  2. $query = "SELECT id"
  3. . "\n FROM #__weblinks "
  4. . "\n WHERE title = '$this->title'"
  5. . "\n AND catid = $this->catid"
  6. ;
  7. $this->_db->setQuery( $query );

$this->title en $this->catid worden blijkbaar niet (correct) gecontroleerd word. Erger nog, ze worden helemaal niet gecontroleerd. Eerst escapen en dan pas in de database voeren, dat is de gouden regel. Zoals je bij de diff kan zien is bij release 10.0.10 dit wel het geval. Hun oplossing:

PHP:
  1. // SQL injection protection
  2. $this->catid = intval($this->catid);
  3. $this->title = $this->_db->getEscaped( $this->title );

getEscaped() is een wrapper voor verschillende database string escape functies. Fixed!

Reageer ook!

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>