Je zou kunnen zeggen dat we momenteel de Cambrische explosie van software meemaken. Opensource projecten schieten als paddenstoelen uit de grond, je hoeft maar te Googelen en iemand heeft jouw probleem al eens eerder gehad en er een paar regels code voor op GitHub gegooid. Klinkt goed zou je op het eerste ogenblik denken, maar ik krijg het gevoel dat we er de laatste tijd een stuk minder kritisch door geworden zijn. Steeds vaker bereikt dat zelfde zinnetje weer mijn oren: “Zolang het maar werkt is het goed toch?”.

Ik zou de vraag niet stellen als het antwoord niet een volmondig nee zou zijn. In deze serie van blogposts wil ik graag de meest voorkomende security issues langslopen en vertellen waarom de meeste, buiten het feit dat ze heel oud zijn, nog steeds razend effectief zijn.

SQL-Injection

Nu beginnen we direct met een klassieker! SQL-Injectie maakt misbruik van gebruikers input, een thema dat zich vaak zal herhalen. Hoe werkt het dat nou precies?

Als je een stukje software schrijft wil je vaak dat er data wordt opgeslagen. Nu zijn wij als programmeurs lui en willen we niet iedere keer het wiel opnieuw uitvinden. Voor opslag gebruiken we daarom ook vaak databases. Met databases communiceren we door middel van SQL. Dat is een aparte taal waarin een programmeur in staat is om de database opdrachten te geven.

Stel je hebt de volgende tabel genaamd users:

id  first_name  email                   is_admin
1   Jeanne      info@spaceleiden.nl     true
2   Niels       niels@stuifmail.com     false

Merk dat Jeanne admin is en Niels niet. Nu willen we een extra gebruiker registreren, hoe doen we dat? De SQL query die in dit geval zou werken gaat als volgt:

INSERT INTO users (first_name, email, is_admin) VALUES ('Raoul', 'raoul@example.com', false)

Nu krijgen we de volgende tabel:

id 	first_name 	email                   is_admin
1 	Jeanne 		info@spaceleiden.nl     true
2 	Niels 		niels@stuifmail.com     false
3 	Raoul 		raoul@example.com       false

Mooi we kunnen nu op een simpele manier data toevoegen aan onze tabel. Nu willen we natuurlijk niet iedere keer die query zelf schrijven dus daar kunnen we een script voor maken.

Het makkelijkste lijkt natuurlijk om de query hierboven te pakken en de variabele te vervangen. DON’T ACTUALLY DO THIS!!!!

first_name = "Niels"
email = "niels@stuifmail.com"

query = "INSERT INTO users (first_name, email, is_admin) VALUES ('" + first_name + "', '" + email + "', false)"

Dit maakt dan weer

INSERT INTO users (first_name, email, is_admin) VALUES ('Niels', 'niels@stuifmail.com', false)

Top! Niets meer aan doen, pushen naar productie en bier drinken met de jongens toch? Nou nee, want zoals je wellicht wel verwacht had hebben we hier een serieus security issue. De invoer van de gebruiker komt nu namelijk letterlijk in de query. Dat betekent dat als de gebruiker zich gedraagt er niet zo heel veel aan de hand is. Maar als de gebruiker iets minder vriendelijke bedoelingen heeft kan deze los gaan.

In de aanval

Nu gaan we in de aanval, stel je bouwt een applicatie die het bovenstaande systeem gebruikt. Iedereen mag zich gratis registreren! Hoe zouden we dit kunnen misbruiken? Het liefst zouden we natuurlijk een gebruiker aanmaken die admin is. Het zijn van admin levert je in de meeste applicaties privileges op zoals het inzien van persoonlijke gegevens wat voor hackers uiteraard interressant is.

Het liefst wil ik als aanvaller de volgende query uitvoeren:

INSERT INTO users (first_name, email, is_admin) VALUES ('Boefje', 'boefje@boefjesnetwork.nep', true)

Maar de applicatie maakt hard coded gebruik van false bij het inserten van admin. Hoe kunnen we hierom heen? Kinderlijk eenvoudig eigenlijk, we maken misbruik van het feit dat de gebruikers invoer direct in de query eindigt.

Stel ik vul het formulier als volgt in:

Voornaam: Boefje
Email: boefje@boefjesnetwork.nep

Dan krijg ik gewoon een account, maar stel ik vul een iets ander emailadres in:

Voornaam: Boefje
Email: boefje@boefjesnetwork.nep', true)

Dan krijg je dus de volgende situatie:

first_name = "Boefje"
email = "boefje@boefjesnetwork.nep', true)"

query = "INSERT INTO users (first_name, email, is_admin) VALUES ('" + first_name + "', '" + email + "', false)"

Dit levert de volgende query op:

INSERT INTO users (first_name, email, is_admin) VALUES ('Boefje', 'boefje@boefjesnetwork.nep', true), false)

“Ja oké je hebt de query kunnen beïnvloeden maar hij klopt nu niet en draait daardoor niet.” zou je nu wellicht terrecht denken. Maar daarvoor hebben we nog een redmiddel! Iedereen zou ze moeten gebruiken, niemand doet het eigenlijk dus om ze nog een beetje liefde te geven misbruiken we ze nu maar: Comments!

Hoe werken comments in SQL? Je kan ze overal plaatsen, ze werken d.m.v. 2 streepjes “–”. Ze maken vervolgens van de rest van de regel een comment.

-- Dit is een comment
INSERT INTO users (first_name, email, is_admin) VALUES ('Boefje', 'boefje@boefjesnetwork.nep', true) -- Dit is ook een comment

Dus stel ik zet 2 streepjes achter het emailadres:

Voornaam: Boefje
Email: boefje@boefjesnetwork.nep', true) --

Dan maak ik van de rest van de query een comment, oftewel:

INSERT INTO users (first_name, email, is_admin) VALUES ('Boefje', 'boefje@boefjesnetwork.nep', true) --, false)

En voila! We hebben door het aanpassen van ons email adres een valide admin account toegevoegd. Zo werkt SQL-Injectie in de basis!

Oplossing

Als we kijken naar de OWASP Top 10 (een lijst van de 10 meest voorkomende kwetsbaarheden in applicaties) staat deze kwetsbaarheid op nummer 1. Waarom is deze nog steeds zo’n populair?

Dit komt (in mijn ogen) door de vele brakke tutorials die rondzwerven op internet. Kijk deze bijvoorbeeld, eerste pagina op Google als je zoekt op: “how to make a registration form in php”. Mocht je deze tutorial volgen dan eindig je met een SQL-Injection en dus een groot probleem.

Dit is echter iets waarvan ik bang ben dat het niet snel zal uitsterven, maar hoe moet het dan wel? Het antwoord is prepared statements! Het idee hiervan is dat je eerst de query naar de server stuurt en dan pas de variabelen!

Dit ziet er als volgt uit (code werkt niet echt):

query = "INSERT INTO users (first_name, email, is_admin) VALUES (?, ?, ?)"

execute(query, ["Boefje", "boefje@boefjesnetwork.nep", True])

Nu is het niet mogelijk om de query te beïnvloeden met user input omdat deze apart van elkaar behandeld worden. Hier een aantal correcte tutorials voor wat populaire talen:

Taal 	URL
PHP 	https://www.w3schools.com/php/php_mysql_prepared_statements.asp
Python 	https://pynative.com/python-mysql-execute-parameterized-query-using-prepared-statement/
Java 	https://www.javatpoint.com/PreparedStatement-interface

Conclusie

We hebben een kleine duik genomen in SQL-Injecties en hebben kunnen zien wat er allemaal mis mee kan gaan. Dit is echter nog maar het tipje van de sluier, er zijn nog veel meer mogelijkheden waar nog veel meer blogposts over te schrijven zijn. Dit soort kennis delen is een van de redenen dat wij graag een makerspace op willen richten in Leiden. Ook geïnterresserd? Vul dan onze enquête in!

Reacties