Artikel van Bart Bons 2004-07-20

Web applicatie beveiliging met MySQL en PHP

Note: De MD5 functie staat onder druk vanwege onlangs ontdekte attack om MD5 collissions te verkrijgen. Hoewel dit soort collisions onderstaand artikel niet ondermijnen (dan zou je gebruik moeten maken van een 'brute-force' pre-image attack), willen we het toch even melden [red.]

Een tijd geleden is het me gelukt een webapplicatie te 'hacken'. Ik zou je graag vertellen dat de informatie die ik vond van nationaal belang was, maar de waarheid is iets minder spectaculair. Eigenlijk doet dat er ook niet toe. De les die ik van die (amateuristische) hack leerde is zeer waardevol...

Nadat ik ingelogd was in de web applicatie (als de welbekende 'demo demo' gebruiker), zag de url er ongeveer als volgt uit:

www.somewebsite.tld/app.htm?sess=283344

Ik herkende het 'sess=283344' gedeelte als een sessie variabele; een veel gebruikte techniek voor webapplicaties om acties en informatie van ingelogde gebruikers te onthouden. In de meeste webapplicaties zie je deze sessie ID's niet. Deze worden meestal opgeslagen als een cookie (in combinatie met wat gegevens op de server), waardoor het minder zichtbaar is voor een gebruiker. Een andere manier is om sessie ID's in de URL op te nemen. In PHP ziet dit uit als volgt:

www.somewebsite.tld/app.htm?PHPSESSID=e4bb2c00f829547ee9abf9f3b9d0efe1

Zoals je ziet is de PHPSESSID een 'gehashte' waarde, want het bestaat uit 32 karakters uit de hexidecimale reeks. Een van de leuke dingen van een gehashte waarde is dat het geen (overduidelijk) verband heeft met in het verleden gegenereerde, of in de toekomst gegenereerde waarden. Ze zijn niet-sequentieel.

De 'sess=283344' constructie zag er wel uit alsof het een sequentiele waarde was. Wat ik toen deed was het volgende. Ik ging terug naar oudere – nog steeds actieve – sessies door de url aan te passen en de waarde 28334 steeds met 1 te verlagen. Het koste me niet meer dan vijf seconden om een actieve sessie te vinden. Ik was in staat om informatie te lezen, te wijzigen en toe te voegen die klaarblijkelijk niet aan mezelf toebehoorde maar aan een gebruiker met sessie ID 2883341.

Nu heb ik daar geen misbruik van gemaakt, maar deze ervaring zorgde er wel voor dat ik mijn eigen applicaties nog eens ben gaan bekijken.

Out of sequence

De les die ik leerde is kortweg:
in een webapplicatie moet je nooit beschermde informatie identificeren met behulp van een sequentiële waarde.

Ik zal je een manier laten zien om dit te omzeilen door in real-time gehashte waarden te genereren met behulp van MySQL en PHP. Deze techniek zou je eenvoudig moeten kunnen vertalen naar andere programmeertalen of databases, aangezien de gebruikte functies in de meeste databasesystemen beschikbaar zijn.

Wat we willen voorkomen zijn HTML forms in de vorm van:

<input type="hidden" name="recordid" value="23">

Beter is het om identificatie tags op de volgende manier te schrijven:

<input type="hidden" name="recordid" value="37693cfc748049e45d87b8c7d8b9aacd">

Waarom? Omdat de relatie tussen recordid '23' en bijvoorbeeld recordid '24' makkelijk te bepalen is. De relatie tussen het gehashte recordid '37693cfc748049e45d87b8c7d8b9aacd' en '1ff1de774005f8da13f42943881c655f' is veel moeilijker te bepalen.

De volgende SQL zal de gehashte waarde opleveren:

select m.*, MD5(m.medid) as recordid from medewerker m

Deze Select geeft alle kolommen terug van de tabel medewerker, plus nog een extra kolom 'recordid' die de gehashte waarde van de sleutelkolom bevat. Deze waarde kun je vervolgens gebruiken in je HTML Form. Wanneer een tabel een of meer sleutelkolommen heeft, dan kunnen we deze kolommen samenvoegen (concateneren) om zo een gehashte waarde te krijgen:

select m.*, MD5(CONCAT(m.klantid, m.medid) as recordid from medewerker m

Om de MD5 functie meer random waarden te laten genereren kunnen we een extra tekenreeks meegeven aan de MD5 functie. Deze geheime tekenreeks is alleen bekend op de server en wordt verder niet aan de clientkant gebruikt.

select m.*, MD5(CONCAT(m.klantid, m.medid, 'ZoutEnPeper') as recordid from medewerker m

Hoe gebruiken we dit in een Form?

Met voorgaande gegevens kunnen we een HTML Form maken:

<form method="post" action="index.php">
<input type="hidden" name="recordid" value="e4bb2c00f829547ee9abf9f3b9d0efe1">
<input type="text" name="achternaam" value="Jansen">
<input type="submit" value="Opslaan">

Nadat de gebruiker de achternaam heeft veranderd en vervolgens op Opslaan heeft gedrukt, kunnen we het recordid gebruiken om het juiste record uit de database te halen en te wijzigen. Het volgende SQL statement voert dit voor ons uit.

update medewerker m
set achternaam = 'Pietersen'
where MD5(CONCAT(m.klantid, m.medid, 'ZoutEnPeper') = 'e4bb2c00f829547ee9abf9f3b9d0efe1'

Wanneer we dit gaan gebruiken in PHP is het handig om de hash-code in een aparte variabele te zetten, zodat we hem eenvoudig kunnen hergebruiken:

$_medewerkerhash = "MD5(CONCAT(m.klantid, m.medid, 'ZoutEnPeper')";
 
$_sqlMedewerkerSelect = "
  select e.*, $_medewerkerhash as recordid
  from medewerker m
";
 
$_sqlMedewerkerUpdate = "
  update medewerker m
  set achternaam = ?
  where $_medewerkerhash = ?
";

Dit zijn de basis ingrediënten die je nodig hebt om betere Form identificatie tag te maken. De PHP code die nodig is om de SQL statements uit te voeren, is vrij eenvoudig en heb ik voor het gemak even weggelaten.

Spiegeltje, spiegeltje...

Er is een interessant probleem dat kan optreden wanneer we deze techniek gebruiken. Stel, we hebben de volgende tabel:

    +----------+-------+------------+
    | klantid  | medid | achternaam |
    +----------+-------+------------+
    |      1   |   11  | Jansen     |
    |     11   |    1  | Devries    |
    +----------+-------+------------+

In dit voorbeeld hebben we twee records die dezelfde hash waarde opleveren.

MD5(CONCAT('1','11','ZoutEnPeper')) => MD5('111ZoutEnPeper')
MD5(CONCAT('11','1','ZoutEnPeper')) => MD5('111ZoutEnPeper')

Zoals je kunt zien leveren twee 'symmetrische' records hetzelfde resultaat op. Met andere woorden, of je nu '1' en '11' aan elkaar plakt of '11' en '1', het levert altijd '111' op. Na hashing krijgen we dezelfde tekenreeks en kunnen we de twee records niet identificeren met de gehashte code. We kunnen dit probleem oplossen door tussen elke sleutelkolom een extra teken toe te voegen.

MD5(CONCAT('1','_','11','ZoutEnPeper')) => MD5('1_11ZoutEnPeper')
MD5(CONCAT('11','_','1','ZoutEnPeper')) => MD5('11_1ZoutEnPeper')

Nu hebben we verschillende hash-waarden voor deze twee records. We zouden dit ook op een ander manier kunnen doen. Namelijk door op iedere individuele sleutelkolom de MD5 functie los te laten.

MD5(CONCAT(MD5('1'),MD5('11'),'ZoutEnPeper'))

Maar dit wordt waarschijnlijk erg traag wanneer je tabellen hebt met veel sleutelkolommen.

Ik hoop dat je iets aan deze techniek hebt en ik ben benieuwd naar commentaar of opmerkingen.

De volgende keer breiden we deze techniek uit met wat interessantere hashing reeksen. Bijvoorbeeld, het IP adres van de gebruiker of datum en tijd informatie.


Bart Bons houdt zich voornamelijk bezig met het ontwikkelen van business software, webservices en open standaarden.

recente artikelen
Stay Focused

– stay focused –

FeedCentral: RSS index + API webservice

2007-05-01 Onlangs heeft FocusFriends een negental nieuwe websites gelanceerd. Deze...   meer

Debster in Boomers

2005-07-06 6 juli kwam alweer de 4e editie van Boomers uit - het lijfblad van de...   meer

Debster in De Ster

2005-05-22 In De Ster , 'de grootste krant van Maastricht', verscheen onlangs een kort...   meer

Overzicht van alle artikelen