Az SQL injection-t gyakran használják rosszul megtervezett alkalmazások adatbázisaiban rosszindulatú kód futtatására. Ezt úgy érik el, hogy a hibásan szűrt (esetleg nem is szűrt) felhasználói inputba SQL kódot illesztenek. Ezzel módosítva az adatbázis lekérdezés kimenetét, esetleg magát az adatbázist. A támadó nem csak olyan adatokat tud kinyerni az adatbázisból amihez egyébként nem lenne joga, hanem akár módosíthatja vagy meg is gátolhatja, semmisítheti azt.
Sokan élnek abban a hitben, hogy a NoSQL egyik jó tulajdonsága, hogy védett az SQL injection támadás ellen. Igaz, hogy ezen adatbázisokban hatástalan az SQL injection, mert a NoSQL nem használja ezt a lekérdező nyelvet, de ez nem jelenti azt, hogy nem lehet manipulálni a lekérdezés eredményét! De lehet-e az SQL injection-hoz hasonlóan megváltoztatni egy NoSql adatbázis lekérdezés eredményét? A válasz: IGEN
A támadást egy egyszerű webes alkalmazással, egy beléptetéssel, fogom demonstrálni. Igaz, hogy az ilyen jellegű támadásoknak a weblapok fokozottabban vannak kitéve, de ez nem azt jelenti, hogy bármilyen más, alkalmazásnál ez nem kivitelezhető.
Ehhez szükségles egy egyszerű html űrlapra.
<!DOCTYPE html>
<html>
<head>
<title>SQL - NoSQL injection</title>
<meta charset="UTF-8" />
<meta name="author" content="author spapp - visit us online @ http://spappsite.hu" />
<meta name="description" content="" />
<meta name="keywords" content="" />
</head>
<body>
<pre>
<?php
session_start();
if (isset($_GET['logout'])) {
unset($_SESSION['user']);
} elseif (isset($_POST['username']) and isset($_POST['password'])) {
// ide fog jönni az autentikáció
}
if (!isset($_SESSION['user'])) {
?>
</pre>
<form enctype="application/x-www-form-urlencoded" action="index.php" method="post">
<label for="username">name:</label><input type="text" name="username"/><br/>
<label for="password">password:</label><input type="password" name="password"/><br/>
<input type="submit" value="log in"/>
</form>
<?php
} else {
echo '<b>logged in: </b>' . $_SESSION['user']['real_name'] . '<br/>';
echo '<a href="?logout">logout</a>';
}
?>
</body>
</html>
A példában egy autentikációt fogunk szimulálni. A felhasználó beírja a felhasználónevét és a jelszavát majd elküldi azt a szervernek. A szerver megnézi, hogy szerepel-e a felhasználó az adatbázisban, s ha igen, akkor az adatait eltárolja és beengedi az azonosított felhasználót a védett oldalra.
Először nézzük meg hogyan történik egy ilyen folyamat relációs adatbázis esetében. Ez esetben egy MySQL adatbázissal. Mielőtt nekikezdenénk hozzuk létre az adatbázist és benne a szükséges táblát, mely alapján az azonosítást fogjuk végezni.
CREATE DATABASE `test_db` ;
CREATE TABLE `test_db`.`users` (
`userid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 60 ) NOT NULL ,
`password` VARCHAR( 32 ) NOT NULL ,
`real_name` VARCHAR( 255 ) NOT NULL
) ENGINE = MYISAM ;
INSERT INTO `test_db`.`users` (
`userid` ,
`username` ,
`password` ,
`real_name`
)
VALUES (
NULL , 'root', '********', 'root user'
), (
NULL , 'other', '********', 'other user'
);
Az azonosítás folyamata egyszerű. Vegyük észre, hogy az SQL lekérdezésbe ellenőrzés nélkül bele kerül a felhasználói input!
$connect = mysql_connect('localhost', 'db_user', '********');
if($connect){
mysql_select_db('test_db', $connect);
$sql = "SELECT * FROM `users` WHERE ";
$sql .= "`username` = '" . $_POST['username'] ."' ";
$sql .= "AND `password` = '" . $_POST['password'] . "'";
echo $sql . "\n\n";
$result = mysql_query($sql);
if ($result and mysql_num_rows($result) === 1) {
$_SESSION['user'] = mysql_fetch_assoc($result);
}
mysql_free_result($result);
mysql_close($connect);
}
Mindaddig, míg rendeltetésszerűen használjuk az alkalmazást nem is történik semmi baj.
Most próbáljuk meg becsapni az alkalmazást és jussunk be felhasználói név és jelszó nélkül!
Ez elsőre nehéznek tűnik, de próbáljuk ki, hogy jelszó-nak írjunk be valamit felhasználói név-nek pedig az alábbiakat (a végén szóközzel):
nem tudom a felhasználó nevet' OR 1 ORDER BY 1 LIMIT 1 --
És máris root-ként vagyunk bent a védett oldalon!
A lekérdezés így alakul:
SELECT * FROM `users` WHERE `username` = 'nem tudom a felhasználó nevet' OR 1 ORDER BY 1 LIMIT 1 -- ' AND `password` = 'nem tudom a jelszót' LIMIT 1
Lekérdezzük az összes felhasználót nem tudom a felhasználó nevet' OR 1, sorba rendezzük az első mező alapján ORDER BY 1, limitáljuk listát egy rekordba LIMIT 1, mert nekünk csak egy felhasználó kell, majd a lekérdezés többi részét figyelmen kívül hagyjuk -- .
Az alábbi megszokásokat használjuk ki:
A hiba nem ezekben a megszokásokban van! Hanem abban, hogy megbíztuk a felhasználóban és ellenőrzés nélkül illesztettük az SQL lekérdezésbe az általa megadottakat.
Mindig ellenőrizzük a felhasználó álltal megadott adatokat és változatlan formában soha ne illesszük SQL lekérdezésbe! Így már nem működik az SQL injection:
$sql .= "`username` = '" . mysql_real_escape_string($_POST['username']) ."' "; $sql .= "AND `password` = '" . mysql_real_escape_string($_POST['password']) . "' ";
Az elején azt mondtam, hogy ezt egy NoSQL adatbázisnál is könnyedén meg lehet csinálni. Ez esetben a MongoDB-t fogom használni. A telepítése egy korábbi cikkben megtalálható: MongoDB használatba vétele .
Először készítsük el az korábbi séma NoSQL megfelelőjét:
Nyissunk egy terminált és lépjünk be az adatbázisba. Hozzuk létre az adatbázist és hozzá a felhasználót. Majd ellenőrizzük, hogy sikerült-e!
username@host:~$ mongo
MongoDB shell version: 2.0.2
connecting to: test
> use test_db
switched to db test_db
> db.addUser("root", '********')
{ "n" : 0, "connectionId" : 4, "err" : null, "ok" : 1 }
{
"user" : "root",
"readOnly" : false,
"pwd" : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"_id" : ObjectId("4f3793850320a147fe1181c5")
}
> db.auth("root", "********")
1
>
Hozzuk létre a sémát, majd ellenőrizzük azt!
> db.users.insert({"username":"root","password":"********","real_name":"root user"})
> db.users.insert({"username":"other","password":"********","real_name":"other user"})
> db.getCollection("users").find()
{ "_id" : ObjectId("4f37948c0320a147fe1181c6"), "username" : "root", "password" : "********", "real_name" : "root user" }
{ "_id" : ObjectId("4f37949e0320a147fe1181c7"), "username" : "other", "password" : "********", "real_name" : "other user" }
>
Ellenőrizzük, hogy le tudjuk-e kérdezni a felhasználót jó jelszó és név megadásával!
Majd azt, hogy rossz adatokkal nem ad vissza semmit!
> db.users.findOne({username:"root",password:"correct password"});
{
"_id" : ObjectId("4f37948c0320a147fe1181c6"),
"username" : "root",
"password" : "********",
"real_name" : "root user"
}
> db.users.findOne({username:"root",password:"incorrect password"});
null
> db.users.findOne({username:"incorrect username",password:"********"});
null
>
Most ezt ültessük át az alkalmazásunkba, és teszteljük le, hogy működik!
Az alábbi kódra cseréljük le a korábbi MySql-es részt:
$connect = new Mongo('mongodb://localhost:' . Mongo::DEFAULT_PORT);
if ($connect) {
$testDb = $connect->selectDB('test_db');
$testDb->authenticate('root', '********');
$usersCollection = $testDb->users;
$result = $usersCollection->findOne(array(
'username' => $_POST['username'],
'password' => $_POST['password']
));
if ($result) {
$_SESSION['user'] = $result;
}
$connect->close();
}
Ez esetben is vegyük észre, hogy a felhasználói input ellenőrzés nélkül kerül felhasználásra!
Hogyan lehetne befolyásolni a lekérdezés eredményét? Hogyan tudnánk jelszó és felhasználó név nélkül bejutni a védett oldalra? Próbáljuk ki terminálban az alábbi lekérdezést:
> db.users.findOne({username:{"$ne":""},password:{"$ne":""}});
{
"_id" : ObjectId("4f37948c0320a147fe1181c6"),
"username" : "root",
"password" : "********",
"real_name" : "root user"
}
>
A lekérdezés jó, de hogyan vegyük rá a végrehajtásra? Ha a {"$ne":""} szöveget küldjük, akkor jól működik, tehát elutasítja a bejelentkezést. Vegyük észre, hogy a korábbi szöveg egy asszociatív tömb, melynek $ne ($ne = not equals - nem egyenlő)a kulcsa és az üres szöveg az értéke. Ahhoz hogy ezt bejuttathassuk meg kell változtatnunk a html form-ot, ami most így néz ki:
<form enctype="application/x-www-form-urlencoded" action="index.php" method="post">
<label for="username">name:</label><input type="text" name="username"/><br/>
<label for="password">password:</label><input type="password" name="password"/><br/>
<input type="submit" value="log in"/>
</form>
Ez lenne a kívánt kinézet:
<form enctype="application/x-www-form-urlencoded" action="index.php" method="post">
<label for="username">name:</label><input type="text" name="username[$ne]"/><br/>
<label for="password">password:</label><input type="password" name="password[$ne]"/><br/>
<input type="submit" value="log in"/>
</form>
Persze ezt a webhelyen nem lehet és nem is tudjuk, de nekünk bőven elegendő, ha csak lokálisan nálunk meg tudjuk tenni. Ehhez segítségül hívjuk a Firefox Firebug extension-jét. Amivel egy gyerekjáték, mint az a képen is látszik.
Így már működik is!
Mindig ellenőrizzük a felhasználó álltam megadott adatokat és változatlan formában soha ne illőeszük a lekérdezésbe! Így már nem működik az injection:
'username' => (string)$_POST['username'], 'password' => (string)$_POST['password']
Mint látható volt nem csak SQL injection létezik hanem NoSQL injection is. A NoSQL adatbázisok biztonságára legalább annyi figyelmet kell szentelni mint a relációs adatbázisokéra. Bár a példa egy webes alkalmazás volt, és a webes alkalmazások fokozottabban vannak kitéve támadásoknak ez nem jelenti azt, hogy egyéb más alkalmazásainknál nem kell erre ügyelni!
Felhasznált irodalom: