Jeśli pojęcie koniunkcji nie jest Wam obce, doskonale wiecie, że jest ona przemienna. Przemienna, czyli a i b == b i a (że to w taki sposób zapiszę). W zasadzie jest to prawda. Prawie. A prawie robi różnicę.
Kolejność czasem ma znaczenie
Jedną z cech koniunkcji jest fakt, że jeśli pierwsze zdanie jest fałszywe, to cała koniunkcja będzie fałszywa, niezależnie od wartości drugiego zdania. W takim razie po co liczyć jego wartość? Ma to znaczenie również w programowaniu, choćby po to, by sensownie układać warunki, tak by uniknąć niepotrzebnych obliczeń. A teraz pora przejść do sedna sprawy. Słowo-klucz: SQL Injection.
Opisując kiedyś ogólną koncepcję blind SQL Injection, napisałem między innymi takie zdanie:
(...) Ja z kolei często wykorzystuję technikę celowego wywołania błędu w bazie danych, moim "ulubionym" sposobem jest wywołanie błędu dzielenia przez 0 (...)
W typowym przypadku wygląda to mniej więcej tak:
SELECT 1/0 FROM DUAL WHERE 1=0 SELECT 1/0 FROM DUAL WHERE 1=1
W pierwszym przypadku błędu nie ma, bo warunek 1=0 jest fałszywy, w drugim błąd dzielenia przez zero występuje, bo 1=1 jest prawdą.
Czasami zdarzają się ciekawsze przypadki. SQLi aż rzuca się w oczy i wrzeszczy "tu jestem, tu jestem!", ale skonstruowanie przydatnego dla blind SQL Injection warunku z wykorzystaniem "klasycznego" wzorca się nie udaje. W aplikacji nie widać żadnej przydatnej różnicy między "klasycznymi" prezentowanymi wcześniej zapytaniami.
Ale z drugiej strony widać różnicę między czymś takim:
SELECT 1 FROM DUAL WHERE 1=1/0 SELECT 1 FROM DUAL WHERE 1=1/1
Nie mam pojęcia dlaczego tak jest, jak wygląda kod aplikacji i jak obsłużone są poszczególne sytuacje wyjątkowe. Nie wiem dlaczego różnicy nie widać, gdy dzielenie przez 0 występuje w rezultacie SELECT, natomiast widać ją, gdy błąd taki wystąpi w warunku WHERE. Nie wiem i nie bardzo mnie to interesuje. Ważne, że można rozróżnić te dwa stany i przeprowadzić atak metodą blid SQL Injection.
Czy taki przypadek można wykorzystać? Można:
SELECT 1 FROM DUAL WHERE 1=1 AND 1=1/0 SELECT 1 FROM DUAL WHERE 1=0 AND 1=1/0
Jak to działa? Przecież w obu przypadkach jest dzielenie przez 0, prawda?
No właśnie, nie w obu przypadkach... Dowód przy pomocy Pythona:
In [4]: 0 == 1 and 1/0 Out[4]: False In [5]: 1 == 1 and 1/0 --------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last)
Jeśli ktoś ma wątpliwości dlaczego tak się dzieje, krótkie wyjaśnienie. W pierwszym przypadku przez AND występuje coś, co ma wartość False, więc niezależnie od tego co znajduje się po AND, wartość całości będzie False właśnie. Mimo, że w zasadzie jest dzielenie przez 0 to w praktyce go nie ma, bo ten fragment kodu się nie wykona. W drugim przypadku już tak nie jest, bo wartość pierwszej części to True, więc należy wyliczyć wartość tego, co znajduje się po AND, a tu, niespodzianka, mamy dzielenie przez 0, czyli błąd i wyjątek.
Dokładnie tak samo dzieje się w bazie danych. No, może nie w każdej bazie danych, bo niektóre są bardziej wyrozumiałe dla różnych błędów.
Przykład z sqlite:
sqlite> SELECT 1/0 WHERE 1=1; sqlite> SELECT 1/0 WHERE 1=0; sqlite>
Tu zauważalna różnicą jest "pusta linia" w pierwszym przypadku. Błędu nie wywołamy również po WHERE:
sqlite> SELECT 1 WHERE 1=1/0; sqlite>
Z tej techniki bez problemu można skorzystać na przykład w MS SQL:
SELECT 1/0 WHERE 1=1 Msg 8134, Level 16, State 1, Line 1 Divide by zero error encountered. SELECT 1 WHERE 1=0 AND 1=1/0 Query executed successfully SELECT 1 WHERE 1=1 AND 1=1/0 Msg 8134, Level 16, State 1, Line 1 Divide by zero error encountered.
Oczywiście, nie jest to jedyny sposób wykorzystania takiej podatności. Po prostu pokazuję to w ramach "ciekawostki".