Jó lesz az, csak másra
A klasszikus SQL injection nyomozás nem várt fordulatot vesz
Ez alkalommal egy fordulatos kis nyomozásra invitálom a kedves olvasót, ami kezdetben SQL injection vadászatnak indult, de hamar átcsapott a lét hiábavalóságának megtapasztalásába, a világunkat működtető szabályok megkérdőjelezésébe majd a lehetetlen újraértékelésébe.
Bár az est sztárvendége nem maga az SQL injection lesz, történetünk - mint sok hasonló történet - mégis egy egyszerű query-vel kezdődik.
$query = "SELECT * FROM users WHERE id={intval($id)}";
$user = $db->query($query)->fetch();
A projekt konvencióitól függően ez akár simán át is csusszanhatna egy code review-n, főleg ha az érintettek nem annyira járatosak a PHP-ban. Talán felmerülne a bátortalan kérdés, hogy az az intval
hívás ott biztos, hogy működik-e, de a zöld tesztek mindenkit megnyugtatnak. Nincs itt semmi probléma.
Vagy mégis? Talán jobb lesz, ha utánajárunk...
$ php -a
php > $id = 'almafa';
php > var_dump("{intval($id)}");
string(16) "{intval(almafa)}"
Úgy tűnik a PHP-s változó behelyettesítés nem olyan okos, mint más nyelvek hasonló szerkezetei (pl. a Template literals JavaScript-ben, vagy a Python-os Literal String Interpolation).
Az intval
nem igazán fut le és ezzel meg is érkeztünk az SQL injection lehetőségéhez. Például ha az $id
változó értéke valami olyasmi lenne, hogy 1)} OR 1=1 --
, akkor... de már nem is ez az érdekes, igaz? Engem legalábbis ezen a ponton már sokkal jobban foglalkoztatott az, hogy miért voltak zöldek a tesztek?
Az első gondolatom az volt, hogy talán a MySQL-nek is van intval
függvénye és az futhat le, de erről hamar meggyőződtem, hogy nem így van.
$ mysql
> SELECT intval("1234");
ERROR 1305 (42000): FUNCTION mysql.intval does not exist
Biztos a kapcsos zárójelekhez lesz valami köze. Nem kevés keresgélés után végül találtam is egy ígéretes nyomot a MySQL dokumentációjában:
{identifier expr}
is ODBC escape syntax and is accepted for ODBC compatibility.
A szimatot fogott kopó lelkesedésével vetettem bele magam az "ODBC escape syntax" utáni kutatásba, ahelyett, hogy inkább a dokumentációt olvastam volna egy kicsit tovább.
Az ODBC egy egységesített API, amivel függetleníthetjük a kódunkat attól, hogy konkrétan milyen adatbázis szervert használunk. Ez a kapcsos zárójelek közötti szerkezet is ezt hivatott elősegíteni azzal, hogy a konkrét driver az adatbázis szerver által elfogadott formára hoz bizonyos kifejezéseket.
Mi a PHP kódban viszont nem ODBC driver-t használunk, úgyhogy nem volt semmi, ami az előfeldolgozást elvégezte volna a MySQL szervernek. Így ő maradt az egyetlen gyanúsított, belőle kell kiszednünk, hogy hogyan és miért is futtat le ilyen query-ket.
$ mysql
> SELECT {intval("1234")};
+------+
| 1234 |
+------+
| 1234 |
+------+
1 row in set (0.00 sec)
> SELECT {intval("asdf")};
+------+
| asdf |
+------+
| asdf |
+------+
1 row in set (0.00 sec)
> SELECT {hmmm(1234)};
+------+
| 1234 |
+------+
| 1234 |
+------+
1 row in set (0.00 sec)
> SELECT {erdekes 1234};
+------+
| 1234 |
+------+
| 1234 |
+------+
1 row in set (0.00 sec)
Mint az látszik, a kapcsos zárójelek között működik az intval
, ránézésre csinálja is a dolgát... kivéve ha valami szöveget adunk be neki... vagy nem is intval
-nak hívjuk... esetleg még a zárójeleket is elhagyjuk.
Ahogy ezt a dokumentáció rákövetkező mondata is megerősíti:
The value is
expr
.
Tehát bármilyen identifier
-t adunk meg, ugyanaz fog kijönni ebből a kifejezésből, mint amit beleküldtünk. Nem csinál vele semmit. Valószínűleg csak azért dolgozza fel, ha esetleg az ODBC driver benne hagyna valami ilyesmit a query-ben, akkor se haljon el.
Ezzel ennek a rejtélynek a végére is értünk. Már csak annyi a dolgunk, hogy elgondolkozzunk egy kicsit azon, hogy hogyan ne szaladjunk bele ebbe a pofonba legközelebb. Jó ötlet lehet például a prepared statement használata intval
helyett vagy olyan tesztekkel kiegészíteni a meglévőket, amik az intval
szerepére is építenek és szöveges $id
-val próbálják feszegetni a rendszer határait.