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
Enviar um comentário