1. Start
  2. Unternehmen
  3. Blog
  4. Security - Lücken durch SQL Injection in PL/SQL vermeiden

Security - Lücken durch SQL Injection in PL/SQL vermeiden

Das Problem

Der heutige Blogeintrag wird sich mit einem Security Thema, SQL Injection, beschäftigen. Aber was genau ist das eigentlich? Im Grunde geht es um das Einschleusen und/oder Ausführen von fremden Statements auf der Datenbank. Da stellt sich die Frage, wie Statements in die Datenbank eingeschleust werden können so dass diese dort dann auch fehlerfrei ausgeführt werden. Im Prinzip basiert diese Angriffsmethode auf dem dynamischen Zusammensetzen von Zeichenketten. Sobald ein SQL-Statement zur Laufzeit dynamisch aus mehreren Zeichenketten zusammengebaut und ein Teil dieser Zeichenketten von außen per Parameter übergeben wird, ist das Statement angreifbar. 

Nehmen wir folgendes einfaches Beispiel: Um eine bestimmte Applikation benutzen zu können, muss der Benutzer in einer Tabelle existieren. Um das zu prüfen, wird eine PL/SQL-Funktion erstellt deren Ergebnis dann entsprechend 0 oder 1 zurückliefert, je nachdem ob der Benutzer existiert oder nicht.

 

SQL> create table app_users(
  2    username varchar2(30)
  3* );

Table APP_USERS erstellt.

SQL> insert into app_users(username) values ('mischke');

1 Zeile eingefügt.

SQL> commit;
Commit abgeschlossen.

SQL> create or replace function fnc_is_user_valid(p_username in varchar2)
  2  return number
  3  as
  4    v_cnt pls_integer;
  5  begin
  6    execute immediate 'select count(*) from app_users where username =''' || p_username || '''' into v_cnt;
  7
  8    if v_cnt > 0 then
  9      return 0;
 10    else
 11      return -1;
 12    end if;
 13  end;
 14* /

Function FNC_IS_USER_VALID kompiliert

 

Die Funktion lässt sich nun ganz einfach verwenden um einen als Parameter übergebenen Benutzer zu validieren.

 

SQL> select fnc_is_user_valid('mischke') from dual;

   FNC_IS_USER_VALID('MISCHKE')
_______________________________
                              0

SQL> select fnc_is_user_valid('hugo') from dual;

   FNC_IS_USER_VALID('HUGO')
____________________________
                          -1

 

Aber wie lässt sich diese Funktion nun angreifen so dass die Prüfung auf den Benutzer pratkisch umgangen wird? Die Lösung steckt im Zusammensetzen des SQL-Statements. Denn der Benutzername wird als Text übergeben und ohne jegliche Prüfung in das Statement eingebaut. Das kann man sich zu Nutze machen.

 

SQL> select fnc_is_user_valid('hugo'' or 1=1 --') from dual;

   FNC_IS_USER_VALID('HUGO''OR1=1--')
_____________________________________
                                    0

 

Obwohl "hugo" kein zulässiger Benutzername ist, hat die Funktion trotzdem eine 0 zurückgegeben. Was ist also passiert? Das erklärt  sich, wenn man sich das finale SQL-Statement nach dem Zusammensetzen der Zeichenketten anschaut.

 

SQL> select 'select count(*) from app_users where username =''' || 'hugo'' or 1=1 --' || '''' from dual;

   'SELECTCOUNT(*)FROMAPP_USERSWHEREUSERNAME='''||'HUGO''OR1=1--'||''''
_______________________________________________________________________
select count(*) from app_users where username ='hugo' or 1=1 --'

 

Der Wert des Parameters hat also dafür gesorgt, dass die zu prüfende Zeichenkette mit einem Hochkomma beendet und eine weitere Prüfung, die immer TRUE ergibt, per OR angehängt wird. Die WHERE-Bedingung wird also nun immer TRUE sein. Außerdem wurde mit einem "--" als Kommentar alles Folgende im SQL-Text praktisch unwirksam gemacht. Somit ergibt sich ein valides SQL-Statement, das aber etwas völlig anderes tut, als es eigentlich sollte.

Dieses einfache Beispiel zeigt, wie einfach es sein kann, Schadcode einzuschleusen. Auf ähnliche Art und Weise könnten auch DDL Statements eingeschleust werden. So könnten Benutzer angelegt, Rechte vergeben oder Datenbank-Objekte verändert werden.  Es ist also dringend erforderlich, die Datenbank vor solchen Angriffen zu schützen. Dieser Schutz muss bei der Entwicklung von Datenbank-Applikationen berüclsichtigt werden. Die Datenbank kann nur das Werkzeug bereitstellen, am Ende hängt es an der Implementierung, ob der Code angreifbar ist oder nicht.

Lösungsansatz #1: Bind Variablen

Nun aber zu den möglichen Lösungen für das Beispiel. Die einfachste Lösung hier ist, das Statement gar nicht dynamisch zusammenzusetzen sondern Bind Variablen zu verwenden. Somit ist das Statement statisch, der im Parameter übergebene Wert wird einfach einer Variablen zugewiesen. Damit ist der Code vom Wert des Parameters unabhängig.

 

SQL> create or replace function fnc_is_user_valid(p_username in varchar2)
  2  return number
  3  as
  4    v_cnt pls_integer;
  5  begin
  6    execute immediate 'select count(*) from app_users where username = :p1' into v_cnt using p_username;
  7
  8    if v_cnt > 0 then
  9      return 0;
 10    else
 11      return -1;
 12    end if;
 13  end;
 14* /

Function FNC_IS_USER_VALID kompiliert

SQL> select fnc_is_user_valid('hugo'' or 1=1 --') from dual;

   FNC_IS_USER_VALID('HUGO''OR1=1--')
_____________________________________
                                   -1

 

Es ist also egal, welcher Wert an die Funktion übergeben wird, der Text wird immer 1:1 als Wert der Variablen verwendet ohne das Statement inhaltlich zu verändern.

Lösungsansatz #2: Prüfung der Parameter

Natürlich ist es nicht immer möglich, Bind Variablen zu verwenden. Dann muss der Inhalt jedes Parameters, der in SQL-Statements eingebaut wird, vorher geprüft werden. Die Oracle Datenbank bringt dafür das Package DBMS_ASSERT mit, dass verschiedene Funktionen zur Prüfung enthält. Im Beispiel soll ein Parameter als Textwert verwendet werden. Es braucht also Hochkommas am Beginn und Ende des Textes. Die Funktion ENQUOTE_LITERAL ergänzt genau diese Hochkommas.

 

SQL> create or replace function fnc_is_user_valid(p_username in varchar2)
  2  return number
  3  as
  4    v_cnt pls_integer;
  5  begin
  6    execute immediate 'select count(*) from app_users where username =' || dbms_assert.enquote_literal(p_username) into v_cnt;
  7
  8    if v_cnt > 0 then
  9      return 0;
 10    else
 11      return -1;
 12    end if;
 13  end;
 14* /

Function FNC_IS_USER_VALID kompiliert

SQL> select fnc_is_user_valid('hugo'' or 1=1 --') from dual;

Fehler beim Start in Zeile: 1 in Befehl -
select fnc_is_user_valid('hugo'' or 1=1 --') from dual
Fehlerbericht -
ORA-06502: PL/SQL: numerischer oder Wertefehler
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 493
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 583
ORA-06512: in "HR.FNC_IS_USER_VALID", Zeile 6

 

Da der Wert selbst Hochkommas enthält, wirft die Funktion ENQUOTE_LITERAL nun eine Exception und die Funktion ist so nicht mehr angreifbar. Eine weitere Funktion SIMPLE_SQL_NAME des Package kann ermitteln, ob ein Wert ein einfacher SQL-Name ist, also ob er als Bezeichner für ein Objekt zulässig wäre. Wenn man diese Funktion kombiniert mit ENQUOTE_LITERAL wird die Funktion noch etwas robuster.

 

SQL> create or replace function fnc_is_user_valid(p_username in varchar2)
  2  return number
  3  as
  4    v_cnt pls_integer;
  5  begin
  6    execute immediate 'select count(*) from app_users where username =' || dbms_assert.enquote_literal(dbms_assert.simple_sql_name(p_username)) into v_cnt;
  7
  8    if v_cnt > 0 then
  9      return 0;
 10    else
 11      return -1;
 12    end if;
 13  end;
 14* /

Function FNC_IS_USER_VALID kompiliert

SQL> select fnc_is_user_valid('mischke') from dual;

   FNC_IS_USER_VALID('MISCHKE')
_______________________________
                              0

SQL> select fnc_is_user_valid('hugo'' or 1=1 --') from dual;

Fehler beim Start in Zeile: 1 in Befehl -
select fnc_is_user_valid('hugo'' or 1=1 --') from dual
Fehlerbericht -
ORA-44003: Ungültiger SQL-Name
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 215
ORA-06512: in "HR.FNC_IS_USER_VALID", Zeile 6

 

Somit ist die Funktion nun gegen SQL Injection geschützt und kann nicht mehr angegriffen werden.

Schwachstellen finden

Nun ist es aber nicht unbedingt trivial, solche möglichen Schwachstellen zu identifizieren. Hier kann SQLcl, dass in einem vorigen Blog-Eintrag vorgestellt wurde, unterstützen. Dazu muss lediglich die Option CODESCAN aktiviert werden. Das Kompilieren dauert dann zwar etwas länger, bringt aber Hinweise auf mögliche Schwachstellen.

 

SQL> help codescan
SET CODESCAN
---------

set CODESCAN [ON | OFF]
        Kontrolliert bei Problemen mit der Codequalität ausgegebene Warnmeldungen.
        ON aktiviert Warnungen bei möglichen Sicherheitslücken durch SQL-Injection.
        OFF deaktiviert Warnungen.
        Standardwert ist "Off".
SQL> set codescan on
SQL> create or replace function fnc_is_user_valid(p_username in varchar2)
  2  return number
  3  as
  4    v_cnt pls_integer;
  5  begin
  6    execute immediate 'select count(*) from app_users where username =''' || p_username || '''' into v_cnt;
  7
  8    if v_cnt > 0 then
  9      return 0;
 10    else
 11      return -1;
 12    end if;
 13  end;
 14* /

SQLcl security warning: SQL injection P_USERNAME line :1 -> P_USERNAME line :6

Function FNC_IS_USER_VALID kompiliert

 

Man sieht also direkt beim Einspielen von PL/SQL-Code, ob dort Verbesserungspotential besteht oder nicht. Allerdings funktioniert das nur mit Variablen, die wirklich einen Text-Datentyp haben. Werden Datentypen der Art <Tabelle>.<Spalte>%TYPE verwendet, so kann SQLcl solche Schwachstellen nicht finden.

Fazit

Das Härten der Datenbank endet nicht beim Enspielen von Security Patches und einem vernünftigen Nutzer/Rechte-Konzept. Gerade auch innerhalb der eigentlichen Datenbankapplikationen besteht eine zwingende Notwendigkeit, sich gegen mögliche Angriffe zu schützen. Die Datenbank bietet die Werkzeuge dazu, man muss sie noch korrekt und gezielt einsetzen.

Kommentare

Keine Kommentare

Kommentar schreiben

* Diese Felder sind erforderlich