SQL un PL/SQL injekcijas

Moto:
‘Why, what can be worse than cutting our throats?’ she asked, with pretty, naive surprise.
‘Cutting our purses,’ he answered. ‘Man is so made these days that his capacity for living is determined by the money he possesses.’

– Kā, kas tad var būt vēl ļaunāk kā laupīt mums dzīvību? – viņa jautāja diezgan naivā pārsteigumā.
– Nolaupīt mūsu makus, – viņš atbildēja. – Mūsu dienās cilvēks ir izveidojies tā, ka viņa spēju dzīvot nosaka nauda, kas viņam pieder.

The Sea-Wolf, Jack London.

Neba nu tā, ka es šim citātam pilnīgi piekristu, vēl jo vairāk tāpēc, ka Vilks Larsens ar savu filozofiju diezgan slikti beidza (starp citu saistošs stāsts, iesaku izlasīt). Taču savi maki (dati) ir jāaizsargā, jo citādi kāds tos atnāks un paņems tādā vai citādā veidā. Viens no veidiem, kā makus atstāt vaļā, ir atstāt caurumu, lai Jūsu aplikācijās varētu veikt SQL injekcijas.

Šis raksts galvenokārt ir domāts programmētājiem, kas raksta kodu, nevis lauzējiem, kas cenšas to uzlauzt. Šajā rakstā esošos piemērus bez īpašas piepūles var atrast simtiem vietās internetā, tāpēc nevajadzētu uzskatīt šo par iedrošinājumu “sliktajiem“, bet par aicinājumu “labajiem” neatstāt durvis puspievērtas, ja mājā iekšā vērtīgas lietas.

Visi turpmākie piemēri būs balstīti tikai uz Oracle SQL un PL/SQL. Tai pašā laikā šai rakstā demonstrētās tehnikas nebūt nav ierobežotas tikai ar šo konkrēto SQL implementāciju un programmēšanas valodu.

Kas tas ir?

SQL injekcijas rezultātā datubāzē tiek izpildīts pavisam cits SQL teikums, nevis tas, kas sākotnēji domāts. Tas var būt gan datu atlases (SELECT) gadījumā, gan arī, piemēram, datu koriģēšanas (UPDATE) gadījumā. Parasti “cits” nozīmē vai nu sensitīvu datu iegūšanu (SELECT gadījumā), vai nesankcionētu datu koriģēšanu, vai arī nesankcionētu tiesību piekļuves iegūšanu.

Kad tas notiek?

Parastākais un izplatītākais gadījums ir tad, kad programmētājs neuzmanīgi veido SQL vaicājumu un konkatenē to kopā kā tekstu. Līdz ar to potenciāli ir apdraudēts jebkurš koda gabals jebkurā programmēšanas valodā, kurā programmētājs “lipina” kopā SQL teikumus kā teksta virknes.

SQL injekcija Select vaicājumā nesankcionētai datu ieguvei

Visiem turpmākajiem piemēriem mēs lietosim šādas 2 tabulas,  kas simbolizēs visiem pieejamo informāciju un slepeno informāciju.

CREATE TABLE publiska (
  id NUMBER,
  dati VARCHAR2(100));
CREATE TABLE slepena (
  id NUMBER,
  dati VARCHAR2(100));
INSERT INTO publiska VALUES (1, 'publiska info');
INSERT INTO slepena VALUES (1, 'slepena info');
commit;

Refkursori ir ērts un parocīgs veids, kā no Oracle atdot datus izsaucošajai videi. Lai tā būtu .NET, PHP vai jebkas cits. Protams, ja to dara nekorekti, tad var iedzīvoties pamatīgās nepatikšanās. Skatamies piemēru, ko var pilnībā izpildīt no SQL*Plusa, bet tikpat labi izsaukums var tikt veikts no jebkuras citas vides.

Piemērs 1. Ievainojamas SQL funkcijas izveides piemērs.
CREATE OR REPLACE FUNCTION f_slikta (
  in_filtrs IN VARCHAR2)
RETURN sys_refcursor IS
  rcur sys_refcursor;
BEGIN
  OPEN rcur FOR 'SELECT *
                 FROM publiska
                 WHERE dati LIKE ''' || in_filtrs || '''';
  RETURN rcur;
END;
/
Piemērs 2. Ievainojamās SQL funkcijas sākotnēji paredzētais izsaukums.
SQL> variable cv refcursor
SQL> exec :cv := f_slikta('pub%')
PL/SQL procedure successfully completed.
SQL> print cv
        ID DATI
---------- ---------------
         1 publiska info

Viss kārtībā, vai ne? Dabūjām apmēram to, ko vēlējāmies? Taču paskatamies, kas notiek ar speciālu virkni.

Piemērs 3. Ievainojamās SQL funkcijas izsaukums izmantojot speciālu virkni – SQL injekcija.
SQL> exec :cv := f_slikta(''' UNION ALL SELECT * FROM slepena -- ')
PL/SQL procedure successfully completed.
SQL> print cv
        ID DATI
---------- -------------
         1 slepena info

Slepenā info vairs nav nekāds noslēpums! Kā tad tas notika? Tātad ir vairākas lietas, kas nepieciešamas:

  • tiekam vaļā no iepriekšējā vaicājuma rezultāta vienkārši LIKE salīdzināšanas mainīgā vietā padodot 3 apostrofus, kas SQL teikumā pārvērtīsies par vienu un gala rezultātā iegūstam šādu salīdzināšanu LIKE ” . Protams šis solis nebūt nav obligāts, galu galā jau publiskā info mūs arī briesmīgi netraucēja.
  • Izmantojot kopas operatoru UNION ALL lasam datus no slepenās tabulas.
  • Beigās izmantojam vienrindas komentāru, lai aizkomentētu pēdējo fiksēto apostrofu, kas ir ielikts funkcijas refcursora veidošanas izteiksmē.

Ja vēl joprojām nav skaidrs kā tas īsti notika, tad varam modificēt funkciju un izdrukāt (piemēram, izmantotojot dbms_output.put_line) ārā SQL teikumu. Tad mēs iegūtu šādu izpildīto SQL teikumu:

SELECT * FROM publiska
WHERE dati LIKE '' UNION ALL SELECT * FROM slepena -- '

Select vaicājums bez injekcijas iespējas

Redzam, ka ir slikti, ko darīt, lai būtu labāk? Atceramies, ka visa ļaunuma sakne slēpjas apstāklī, ka mums tika konkatenētas kopā teksta virknes, kur viena bija lietotāja iedotā. No tā var izvairīties lietojot bind mainīgos. Šajā gadījumā mainīgais tik tiešām būs tikai mainīgais, nevis kaut kāda teksta virkne, kuru lietotājs var uzdot kā vien vēlas un kas maina SQL teikuma jēgu un sākotnējo mērķi. Tātad veidojam labo funkciju.

Piemērs 4. NEIevainojamas SQL funkcijas izveides piemērs.
CREATE OR REPLACE FUNCTION f_laba (
  in_filtrs IN VARCHAR2)
RETURN sys_refcursor IS
  rcur sys_refcursor;
BEGIN
  OPEN rcur FOR 'SELECT *
                 FROM publiska
                 WHERE dati LIKE :y' USING in_filtrs;
  RETURN rcur;
END;
/

Skatamies, kas notiks ar iepriekšējiem izsaukumiem.

Piemērs 5. NEIevainojamās SQL funkcijas sākotnēji paredzētais izsaukums.
SQL> variable cv refcursor
SQL> exec :cv := f_laba('pub%')
PL/SQL procedure successfully completed.
SQL> print cv
        ID DATI
---------- ---------------
         1 publiska info

Kā redzams nekas vismaz šoreiz rezultātā nav mainījies.

Piemērs 6. NEIevainojamās SQL funkcijas izsaukums izmantojot speciālu virkni – SQL injekcijas mēģinājums.
SQL> exec :cv := f_laba(''' UNION ALL SELECT * FROM slepena -- ')
PL/SQL procedure successfully completed.
SQL> print cv
no rows selected

Nu re – nekas netika atrasts, kā jau sākumā bija cerēts. Pie tam šim variantam arī no ātradarbības viedokļa ir papildus bonusi – nelietojot bind mainīgos shared pool (atmiņas apgabals, kur tiek glabāti izpildītie SQL teikumi un to izpildes plāni) tiek pārpludināts ar ļoti līdzīgiem SQL teikumiem, kas atšķiras tikai par konstantes tiesu (t.i. katram mainīgajam savs SQL teikums). Ja lieto bind mainīgos, tad šādu problēmu nav.

Tātad redzam, ka pirmajā gadījumā tika atgriezts vēlamais rezultāts un arī otrajā gadījuma tika atgriezts vēlamais rezultāts, jo tikai un vienīgi tabulā publiska tika meklēta kaut kāda simbolu virkne, kura bija pagadījusies tāda diezgan speciāla un to nevarēja atrast.

SQL injekcija Select vaicājumā nesankcionētai jebkādai darbībai

Ja nu gadījumā Jūs iepriekšējais piemērs pilnībā nepārliecināja – sak mēs godīgi cilvēki, mums nav ko slēpt – tad paskatamies uz nākošo piemēru. Šeit lietotājs Zaglis jau ir iekļuvis sistēmā un viņam ir izveidots lietotāja konts. Atceramies, ka reālajā dzīvē tas var būt kāds no esošajiem noklusētajiem lietotājiem (scott, hr utt), kuram administrators nav nomainījis noklusēto paroli, vai arī parole ir bijusi pietiekami vienkārša un zaglis to ir uzminējis.

Piemērs 7. Zagļa mēģinājums nesankcionēti koriģēt datus.
SQL> create user zaglis identified by zaglis;
User created.
SQL> grant create procedure to zaglis;
Grant succeeded.
SQL> grant create session to zaglis;
Grant succeeded.
SQL> conn zaglis/zaglis
Connected.
SQL> INSERT INTO gints.slepena VALUES (2, 'ŠEIT BIJA ZAGLIS');
INSERT INTO gints.slepena VALUES (2, 'ŠEIT BIJA ZAGLIS')
                  *
ERROR at line 1:
ORA-00942: table or view does not exist

Tātad redzam, ka zaglis ir iekļuvis sistēmā un mēģina pievienot savu “parakstu”, bet viņam tas neizdodas, jo tiesību mehānisms to neļauj. Bet zaglis ir pietiekami gudrs, lai izveidotu savu speciālo funkciju (Savā shēmā!!!), kas viņam šīs tiesības iedos, un iedotu tiesības to izpildīt uzbrukuma mērķim.

Piemērs 8. SQL injekcijas funkcijas izveide un izpilde, kas dod nesankcionētas tiesības.
create or replace function dot_tiesibas return number
authid current_user is
  pragma autonomous_transaction;
begin
  execute immediate 'grant all on slepena to zaglis';
  return 1;
end;
/
SQL> grant execute on dot_tiesibas to gints;
Grant succeeded.
SQL> conn gints/gints
Connected.
SQL> exec :cv := f_slikta(-
> ''' UNION ALL SELECT zaglis.dot_tiesibas, null FROM dual --')
PL/SQL procedure successfully completed.
SQL> print cv
        ID DATI
---------- ------
         1
SQL> conn zaglis/zaglis
Connected.
SQL> INSERT INTO gints.slepena VALUES (2, 'ŠEIT BIJA ZAGLIS');
1 row created.
SQL> commit;
Commit complete.

Redzam, ka zaglis tagad no sava konta var bez problēmām darīt visu ar tabulu slepena. Kā tad tas īsti notika?

Tātad Zaglim savā kontā ir tiesības izveidot procedūras un funkcijas. Zaglis izveido funkciju dot_tiesibas, kurai ir šādas īpašības:

  • Tā ir lietotāja definēta funkcija un tādas ir iespējams izsaukt Select vaicājumos;
  • Zaglis ir iedevis tiesības lietotājam gints šo funkciju izpildīt, tāpēc lietotājs gints to var izdarīt (pats nemaz to neapzinādamies);
  • Select vaicājumā izpildāmās funkcijas nedrīkst veikt izmaiņas datubāzē un/vai ar datubāzes objektiem, ja vien šī funkcija nav izsaukta atsevišķas transakcijas ietvaros – ko arī norāda atslēgas vārdi pragma autonomous_transaction;
  • Šī funkcija speciāli ir veidota ar atslēgas vārdiem authid current_user, kas nozīmē to, ka funkcija tiks izpildīta tekošā lietotāja shēmā, nevis kā noklusēti – tā lietotāja shēmā, kas šo funkciju ir izveidojis;
  • execute immediate ļauj izpildīt dinamisko SQLu, tai skaitā arī DDL un DCL.

Nākošaja solī Zaglis izpilda ievainojamo funkciju f_slikta ar šoreiz mazliet citādu SQL injekciju, kas lietotāja gints shēmā izsauc zagļa izveidoto funkciju, kas atgriež 1, bet daudz svarīgāk – piešķir visas tiesības uz tabulu slepena lietotājam Zaglis. Tādējādi Zaglis ir nonācis pie sava vēlamā rezultāta.

Līdzīgu mehānismu var izmantot arī citos datu manipulēšanas teikumos (INSERT, UPDATE, DELETE), ja tie tiek konkatenēti kopā kā teksta virknes, nevis tajos tiek korekti izmantoti mainīgie.

Tātad stāsta morāle ir šāda – nevainosim zagli, ja paši rakstam sliktu kodu. Zagļi vienmēr ir bijuši un būs, bet tikai mūsu izvēle ir rakstīt kodu, kuru zagļi var viegli izmantot saviem mērķiem, vai arī rakstīt kodu, kuru šādi izmantot nevar.

Tālākā lasāmviela

SQL injekcijas un cita drošības informācija Oracle datubāzē:

Informācija par SQL injekcijām (ne tikai Oracle) latviski:

7 Responses to SQL un PL/SQL injekcijas

  1. Kaitnieks saka:

    Viss jau ir kārtībā, bet gribēju nedaudz piesieties pie tā, ka cilvēks, kas nezina par SQL injekcijām visdrīzāk ir iesācējs un viņu šādi (vismaz vizuāli) gari SQL piemēri apjucinātu un atbaidītu. Var taču izmantot īsākus un prastākus piemērus, shirley?
    Un vēl, varbūt es kļūdos, bet , ja cilvēks ir aizdomājies līdz tam, ka un kāpēc jālieto procedūras pretstatā SQL teikumu lipināšanai no fragmentiem, tad visdrīzāk jau nu viņam būs skaidrs, ka SQL teikuma lipināšana procedūras ietvarā no procedūras parametriem ne ar ko nav labāka par tāda paša teikuma salipināšanu no _GET vai _POST vai edUsername.Text.

  2. Gints Plivna saka:

    Hmm, droši vien, ka var arī īsāku un prastāku 😉 Tikai tāpēc jau virsrakstā ir ietverts vārds PL/SQL, ka bija doma izmantot mazdruscīt PL/SQLu. Un ar vēl isākiem piemēriem būtu grūtāk uzkonstruēt piemēru, kad persona reāli padod it kā parametru, jo būtu nepieciešams vēl kāda klienta vide php, .NET vai kaut kas tāds. A tagad ir funkcija, kam ir pilnīgi vienalga no kurienes to izsauc un kas paņem rezultātu, vai tas ir SQL*Plus vai php, vai .NET. Isāki un prastāki piemēri ir arī norādītajos resursos, tāpēc jau es parasti cenšos galā karināt norādes uz citiem rakstiem, lai cilvēki izlasītu manējo un varētu pasmelties arī idejas un domas no citiem.
    A kas attiecas par cilvēkiem, kas būs aizdomājušies par to ka vajag procu un nevajag lipināt kopā, tad Tev diemžēl nav taisnība, jo tieši nepilnu pusstundu atpakaļ vienā uzturamā procā atklāju, ka parametri pa taisno konkatenēti klāt. Ne par velti jau vairākus gadus Oracle vidē (un BTW ne tikai Oracle, to saka arī SQL Serverim), ka jāizmanto bind mainīgie!! Tas pasargās gan no injekcijām, gan shared pool (SQL Serverī, laikam bija kaut kāds cursor cache vai kaut kā līdzīgi) pārpludināšanas. MySQLā cik zinu, tad tāds jēdziens kā shared sql neeksistē, bet, protams, injekciju briesmas paliek arī tur.

  3. Aleksejs saka:

    Paldies, Gint, par šo rakstu!

  4. Viktors.s saka:

    Paldies. Prieks Gint par šo latvisko resursu.

Komentēt