DBMS_SQL_FIREWALL - Firewall dentro da Base de Dados (Parte IV)

Este será o ultimo texto sobre o SQL firewall. Irei ver como impedir um possível ataque de SQL injection através do uso da SQL Firewall
Criei uma stored procedure que será usada nos diferentes testes, quer de execução quer de tentativa de SQL Injection e usarei ainda a opção de top_level_only a false para verificar o que é capturado com essa opção.

Vou começar por criar um procedimento simples, no schema HR, que executa alguns selects depois de receber um parâmetro de entrada, devolvendo um valor relacionado.
O execute immediate do final do procedimento está apenas como exemplo para ser usado com o top_level_only a false e fazer alguns testes. 

        
           
 create or replace procedure empname(lst_name in varchar2) is
          comando varchar2(2000);
          type c_ref is ref cursor;
          c c_ref;
          ult_nome employees.last_name%type;
		  		  
  Begin
          comando :='select first_name from hr.employees '||
                         'where last_name = '''||lst_name||'''';
          open c for comando;
          loop
                  fetch c into ult_nome;
                  if(c%notfound) then
                          exit;
                  end if;
                  dbms_output.put_line('Primeiro Nome :='||' '|| ult_nome ||'');

          end loop;
          close c;
		execute immediate 'select first_name, salary from hr.employees';  
  end;
  /     
         

 Depois de criado o procedimento com o utilizador HR, abro nova sessão com o utilizador FWALLADMIN e vou  criar uma nova captura e iniciá-la. Nesta fase ainda com top_level_only a true.

        
           
SQL> exec dbms_sql_firewall.create_capture(username => 'HR', top_level_only => true, start_capture => true);
     
         

 Vou iniciar sessão com o utilizador HR e começar a captura da actividade de execução do PLSQL

        
           
SQL> set serveroutput on
SQL> exec hr.empname('Whalen');
Primeiro Nome := Jennifer


PL/SQL procedure successfully completed.
     
         

 Ao executar este procedimento foram capturados os dados relativos à execução do PLSQL e em seguida vou gerar e activar a lista de SQL permitido:

        
           
SQL> select username, command_type, sql_text from dba_sql_firewall_capture_logs;

USERNAME    COMMAND_TYPE     SQL_TEXT                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
___________ ________________ _________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ 
HR          SELECT           SELECT PARAMETER,VALUE FROM NLS_SESSION_PARAMETERS UNION ALL SELECT :"SYS_B_0" NAME,DBTIMEZONE VALUE FROM DUAL UNION ALL SELECT :"SYS_B_1" NAME,SESSIONTIMEZONE VALUE FROM DUAL UNION ALL SELECT :"SYS_B_2" NAME,TZ_OFFSET (SESSIONTIMEZONE) VALUE FROM DUAL UNION ALL SELECT PARAMETER,VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER=:"SYS_B_3"                                                                                                                                                                                                                             
HR          EXECUTE          DECLARE L_LINE VARCHAR2 (?); L_DONE NUMBER; L_BUFFER VARCHAR2 (?) :=?; L_LENGTHBUFFER NUMBER :=?; L_LENGTHLINE NUMBER :=?; BEGIN LOOP DBMS_OUTPUT.GET_LINE (L_LINE,L_DONE); IF (L_BUFFER IS NULL) THEN L_LENGTHBUFFER :=?; ELSE L_LENGTHBUFFER :=LENGTH (L_BUFFER); END IF; IF (L_LINE IS NULL) THEN L_LENGTHLINE :=?; ELSE L_LENGTHLINE :=LENGTH (L_LINE); END IF; EXIT WHEN L_LENGTHBUFFER +L_LENGTHLINE >:MAXBYTES OR L_LENGTHBUFFER +L_LENGTHLINE >? OR L_DONE=?; L_BUFFER :=L_BUFFER||L_LINE||CHR (?); END LOOP; :DONE :=L_DONE; :BUFFER :=L_BUFFER; :LINE :=L_LINE; END;    
HR          EXECUTE          BEGIN DBMS_OUTPUT.ENABLE (?); END;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
HR          EXECUTE          DECLARE SQLDEVBIND1Z_1 VARCHAR2 (?) :=:SQLDEVBIND1ZINIT1; BEGIN BEGIN EMPNAME (TO_CHAR (SQLDEVBIND1Z_1)); END; :AUXSQLDBIND1 :=SQLDEVBIND1Z_1; END;                                                                                                                                                                                                                                                                                                                                                                                                                               
HR          ALTER SESSION    ALTER SESSION SET TIME_ZONE=?                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
HR          SELECT           SELECT DBTIMEZONE FROM DUAL                                                                                                                                                                                                                            

SQL> exec dbms_sql_firewall.stop_capture('HR');

PL/SQL procedure successfully completed.

SQL> exec dbms_sql_firewall.generate_allow_list('HR');

PL/SQL procedure successfully completed.

SQL> exec dbms_sql_firewall.enable_allow_list('HR',dbms_sql_firewall.enforce_all,true);

PL/SQL procedure successfully completed.     
         

 Neste momento tenho uma lista de comandos que podemos executar com uma sessão do utilizador HR e vamos testar a execução do PLSQL e verificar se tudo funciona.

        
           
SQL> set serveroutput on
SQL> exec hr.empname('Whalen');
Primeiro Nome := Jennifer


PL/SQL procedure successfully completed.

SQL> exec hr.empname('Urman');
Primeiro Nome := Jose Manuel


PL/SQL procedure successfully completed.


SQL> select first_name, salary from hr.employees;

Error starting at line : 1 in command -
select first_name, salary from hr.employees
Error at Command Line : 1 Column : 1
Error report -
SQL Error: ORA-47605: SQL Firewall violation     
         

 Depois de criar uma sessão, executamos o procedimento sem qualquer problema, visto estar na lista de possíveis comandos a executar, mesmo mudando o valor do parâmetro de entrada funciona. No entanto, se tentarmos executar o sql do execute immediate que está dentro do plsql isoladamente teremos o SQL bloqueado pela firewall.

Vou tentar em seguida executar o mesmo procedimento, mas tentando fazer SQL injection através da passagem de parâmetros. É um exemplo simples em que acrescento uma union e um outro select que neste caso me daria também o job_id da tabela employees.

        
           
SQL> exec hr.empname('x'' union select job_id from employees--');

Error starting at line : 1 in command -
BEGIN empname('x'' union select job_id from employees--'); END;
Error report -
ORA-47605: SQL Firewall violation     
         

 A SQL firewall foi capaz de detectar que o parâmetro é diferente na sua forma e a tentativa de SQL injection não tem sucesso.
Através da view dba_sql_firewall_violations vou tentar perceber o que se passou.

        
           
SQL> select username, command_type, sql_text, cause from dba_sql_firewall_violations;

USERNAME    COMMAND_TYPE    SQL_TEXT                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    CAUSE            
___________ _______________ ___________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ ________________ 
HR          SELECT          SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES                                                                                                                                                                                                                                                                                                                                                                                                                                                                  SQL violation    
HR          EXECUTE         DECLARE L_THECURSOR INTEGER DEFAULT DBMS_SQL.OPEN_CURSOR; L_STATUS INTEGER DEFAULT -?; INSQLSIX VARCHAR2 (?); BEGIN INSQLSIX :=UPPER (SUBSTR (LTRIM (:1),?,?)); IF ((INSQLSIX IN (?,?,?,?)) OR (SUBSTR (INSQLSIX,?,?)=?) OR (SUBSTR (INSQLSIX,?,?)=?) OR (SUBSTR (INSQLSIX,?,?)=?)) THEN BEGIN L_STATUS :=-?; DBMS_SQL.PARSE (L_THECURSOR,:2,DBMS_SQL.NATIVE); EXCEPTION WHEN OTHERS THEN L_STATUS :=DBMS_SQL.LAST_ERROR_POSITION; END; DBMS_SQL.CLOSE_CURSOR (L_THECURSOR); END IF; :3 :=L_STATUS; END;    SQL violation    
HR          EXECUTE         BEGIN HR.EMPNAME (?); END;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  SQL violation    
     
         

 Ao analisar a view, percebemos que a forma como é executado o procedimento é diferente quando o parametro de entrada não é uma string directa, mas uma composição de strings para criar o SQL injection.
Quando temos uma string directa ocomo o exmplo 'Whalen' a execução é feita com o seguinte código:

DECLARE SQLDEVBIND1Z_1 VARCHAR2 (?) :=:SQLDEVBIND1ZINIT1; BEGIN BEGIN EMPNAME (TO_CHAR (SQLDEVBIND1Z_1)); END; :AUXSQLDBIND1 :=SQLDEVBIND1Z_1; END;

Mas com a string composta é apenas executada com o codigo:

BEGIN HR.EMPNAME (?); END;

Fazendo o tratamento e verificação do parâmetro de entrada num outro passo, o que leva a ser bloqueado pela SQL Firewall.
Ainda que não seja um teste exaustivo, podemos confirmar que o SQL Firewall consegue detectar e bloquear formas de SQL INJECTION tentadas a partir do parâmetro de entrada.
Mais uma vez não devemos ter como primeira proteção para este tipo de ataque o SQL Firewall, mas sim um conjunto de tecnicas conhecidas de programação de PLSQL que deverão prevenir esse tipo de ataques.

Só falta testar a opção de captura com top_level_only a false, isto é, capturar também o SQL executado dentro do PLSQL.
Depois de limpar toda a informação dos logs dos testes anteriores vou iniciar uma nova captura com o parâmetro a false.

        
           
exec dbms_sql_firewall.create_capture(username => 'HR', top_level_only => false, start_capture => true);

PL/SQL procedure successfully completed.     
         

 Faço novamente a execução numa sessão do HR do mesmo procedimento do teste anterior.

        
           
SQL> set serveroutput on
SQL> exec hr.empname('Whalen');
Primeiro Nome := Jennifer


PL/SQL procedure successfully completed.     
         

 Ao verificar a actividade capturada, verifico que além do capturado no teste anterior, tenho também os dois comandos de SQL que são executados dentro do PLSQL.

        
           
SQL>  select username, command_type, sql_text from dba_sql_firewall_capture_logs;

USERNAME    COMMAND_TYPE     SQL_TEXT                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
___________ ________________ _________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ 
HR          SELECT           SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
HR          SELECT           SELECT PARAMETER,VALUE FROM NLS_SESSION_PARAMETERS UNION ALL SELECT :"SYS_B_0" NAME,DBTIMEZONE VALUE FROM DUAL UNION ALL SELECT :"SYS_B_1" NAME,SESSIONTIMEZONE VALUE FROM DUAL UNION ALL SELECT :"SYS_B_2" NAME,TZ_OFFSET (SESSIONTIMEZONE) VALUE FROM DUAL UNION ALL SELECT PARAMETER,VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER=:"SYS_B_3"                                                                                                                                                                                                                             
HR          EXECUTE          DECLARE L_LINE VARCHAR2 (?); L_DONE NUMBER; L_BUFFER VARCHAR2 (?) :=?; L_LENGTHBUFFER NUMBER :=?; L_LENGTHLINE NUMBER :=?; BEGIN LOOP DBMS_OUTPUT.GET_LINE (L_LINE,L_DONE); IF (L_BUFFER IS NULL) THEN L_LENGTHBUFFER :=?; ELSE L_LENGTHBUFFER :=LENGTH (L_BUFFER); END IF; IF (L_LINE IS NULL) THEN L_LENGTHLINE :=?; ELSE L_LENGTHLINE :=LENGTH (L_LINE); END IF; EXIT WHEN L_LENGTHBUFFER +L_LENGTHLINE >:MAXBYTES OR L_LENGTHBUFFER +L_LENGTHLINE >? OR L_DONE=?; L_BUFFER :=L_BUFFER||L_LINE||CHR (?); END LOOP; :DONE :=L_DONE; :BUFFER :=L_BUFFER; :LINE :=L_LINE; END;    
HR          SELECT           SELECT FIRST_NAME FROM HR.EMPLOYEES WHERE LAST_NAME=:"SYS_B_0"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
HR          EXECUTE          BEGIN DBMS_OUTPUT.ENABLE (?); END;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
HR          EXECUTE          DECLARE SQLDEVBIND1Z_1 VARCHAR2 (?) :=:SQLDEVBIND1ZINIT1; BEGIN BEGIN HR.EMPNAME (TO_CHAR (SQLDEVBIND1Z_1)); END; :AUXSQLDBIND1 :=SQLDEVBIND1Z_1; END;                                                                                                                                                                                                                                                                                                                                                                                                                            
HR          ALTER SESSION    ALTER SESSION SET TIME_ZONE=?                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
HR          SELECT           SELECT DBTIMEZONE FROM DUAL                                                                                                                                                                                                                          
     
         

Na sessão do FWALLADDMIN volto a criar as listas de autorização de SQL.

           
SQL> exec dbms_sql_firewall.stop_capture('HR');
SQL> exec dbms_sql_firewall.generate_allow_list('HR');
SQL> exec dbms_sql_firewall.enable_allow_list('HR',dbms_sql_firewall.enforce_all,true);     
         

 E em seguida vou testar as novas listas criadas, na sessão do HR

        
           
SQL> set serveroutput on
SQL> exec hr.empname('Whalen');
Primeiro Nome := Jennifer


PL/SQL procedure successfully completed.

SQL>  exec hr.empname('x'' union select job_id from employees--');

Error starting at line : 1 in command -
BEGIN hr.empname('x'' union select job_id from employees--'); END;
Error report -
ORA-47605: SQL Firewall violation     
         

Tudo continua a ter o mesmo tipo de bloqueio na chamada ao plsql.
A questão seguinte é se poderei executar fora do procedimento o SQL capturado dentro dele.

        
           
SQL> select first_name, salary from hr.employees;

Error starting at line : 1 in command -
select first_name, salary from hr.employees
Error at Command Line : 1 Column : 1
Error report -
SQL Error: ORA-47605: SQL Firewall violation     
         

Verifica-se que o sql não é autorizado, mesmo estando na lista de SQL autorizado
Se consultar as views dba_sql_firewall_allowed_sql e a dba_sql_firewall_violations existe a indicação de que o SQL não é top level e na violação ele é top level. Poderá ser por aí que o bloqueio acontece.
 

        
           
SQL> select sql_signature, sql_text, top_level from dba_sql_firewall_capture_logs where sql_text='SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES';

SQL_SIGNATURE                                                       SQL_TEXT                                      TOP_LEVEL    
___________________________________________________________________ _____________________________________________ ____________ 
8E511416970900FA69A608752AB3C81BA4DF7496D356BE5CAA14AB50FBA243A3    SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES    N            

SQL> select sql_signature, sql_text, top_level from dba_sql_firewall_violations where sql_text='SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES';

SQL_SIGNATURE                                                       SQL_TEXT                                      TOP_LEVEL    
___________________________________________________________________ _____________________________________________ ____________ 
8E511416970900FA69A608752AB3C81BA4DF7496D356BE5CAA14AB50FBA243A3    SELECT FIRST_NAME,SALARY FROM HR.EMPLOYEES    Y            
     
         

 Confesso que esperava que o SQL pudesse ser executado. Não sendo, é um mistério para mim a utilidade da opção top_level_sql a false. O SQL está na lista mas não pode ser usado directamente.
Talvez haja alguma lógica de que não me tenha apercebido e que faça esta opção ter alguma utilidade.

Comentários

Mensagens populares deste blogue

DBMS_SQL_FIREWALL - Firewall dentro da Base de Dados (Parte I)

DBMS_SQL_FIREWALL - Firewall dentro da Base de Dados (Parte III)