пятница, 8 июня 2012 г.

Граница на замке

Сегодня я предлагаю поговорить о второй моей любимой теме после Айкидо, об Oracle :) Мы займемся безопасностью (но не просто безопасностью, а безопасностью своими руками). Конечно, можно сказать, что это  "Закат Солнца вручную", но, если в процессе мы узнаем что-то новое, то, возможно, это не совсем бесполезное занятие?

Граница на замке

Итак, ни для кого не секрет, что в рамках TNS-протокола (служащего для общения клиента Oracle с сервером) данные (в том числе и пароль, используемый при авторизации) передаются по сети в открытом виде. Конечно, Oracle подумал об этом и поставляет всякие забавные штуки, типа Advanced Security Option, но наша сегодняшняя задача - показать, что мы и сами не без усов.

Что мы можем предпринять, чтобы не допустить перехвата пароля авторизации снифером? Самый простой ответ - двухфазный протокол авторизации. Идея проста, вместо того чтобы передавать сам секрет (пароль) в открытом виде, мы можем его использовать для преобразования некоторых уникальных сеансовых данных, полученных с сервера, с последующей передачей результата этого преобразования на сервер. В свою очередь, сервер, зная наш секрет, проведет аналогичное преобразование и, сравнив результат с отосланным нами, определит, имеем ли мы право с ним работать.

Выглядит все гладко, но как нам получить сеансовые данные с сервера ДО установления соединения??? Ладно, понятно, что авторизоваться в какой-то схеме Oracle все равно придется, но пусть это будет пустая схема, не имеющая прав ни на что, кроме выполнения двухфазной авторизации. Таким образом, будем считать, что утечка пароля к этой схеме не нанесет ущерба нашей безопасности (на самом деле, еще как нанесет, но тут я возвращаю всех к ASO, уже упомянутой выше).

После завершения нашей авторизации, использованная нами для входа схема должна чудесным образом получать все необходимые для работы права, но до завершения авторизации (или в случае неуспешной авторизации) этих прав быть не должно. В том, чтобы этого добиться, нам поможет возможность включения ролей, предоставляемых Oracle.

Ну что-же, засучим рукава и создадим все что нам нужно:

connect sys/&&syspass@orcl as sysdba

create role ”STRT”; -- Минимальные права, для авторизации
create role ”DFLT”; -- Права авторизованного пользователя
create role ”ADMN”; -- Права авторизованного администратора

revoke ”STRT” from SYS;
revoke ”DFLT” from SYS;
revoke ”ADMN” from SYS;

create user MAIN    -- Схема владельца
identified by ”MAIN” account unlock;

grant CONNECT,RESOURCE to MAIN;
grant EXECUTE on DBMS_OBFUSCATION_TOOLKIT to MAIN;
grant EXECUTE on DBMS_APPLICATION_INFO to MAIN;
grant create any context to MAIN;
grant create any view to MAIN;
grant alter any role to MAIN;

create user ENTRY   -- Схема для выполнения первичного соединения
identified by ”&pass” account unlock;

grant CONNECT,STRT,DFLT,ADMN to ENTRY;
alter user ENTRY default role CONNECT,STRT;

Итак, у нас есть две схемы: ENTRY, используемая для первичного соединения и MAIN, собственно хранящая данные. Также определены три роли: STRT - используемая в процессе двухфазной авторизации, DFLT - определяющая права непривилегированного пользователя после завершения авторизации и ADMN - определяющая административные права. Создадим объекты в MAIN:


connect MAIN/MAIN@orcl

CREATE TABLE ROLE_LIST (
  ID  NUMBER NOT NULL,
  NM  VARCHAR2(10) NOT NULL,
  PS  VARCHAR2(30) NOT NULL
);

ALTER TABLE ROLE_LIST ADD (
  CONSTRAINT ROLE_LIST_PK PRIMARY KEY (ID));

create or replace view ROLES as
select ID,NM,'******************************' PS
from ROLE_LIST;

grant SELECT on ROLES to ADMN;

create sequence USERS_SEQ;

CREATE TABLE USER_LIST (
  ID  NUMBER        NOT NULL,
  NM  VARCHAR2(30)  NOT NULL,
  PS  VARCHAR2(30)  NOT NULL,
  TP  NUMBER        DEFAULT 0,
  SP  DATE          DEFAULT NULL
);

ALTER TABLE USER_LIST ADD (
  CONSTRAINT USER_LIST_PK PRIMARY KEY (ID));

ALTER TABLE USER_LIST ADD (
  CONSTRAINT USER_LIST_UK UNIQUE (NM));

create sequence LOGIN_SEQ
INCREMENT BY 1 START WITH 1 MAXVALUE 1000 CYCLE CACHE 5;

create sequence LOG_SEQ;

CREATE TABLE USER_LOG (
  ID  NUMBER        NOT NULL,
  DT  DATE          DEFAULT sysdate NOT NULL,
  NM  VARCHAR2(30)  NOT NULL,
  RS  NUMBER(1)     NOT NULL
);
 
ALTER TABLE USER_LOG ADD (
  CONSTRAINT USER_LOG_PK PRIMARY KEY (ID));

В таблице USER_LISTS будем хранить логины и пароли пользователей, имеющих право работать в нашей системе. ROLE_LIST будет использоваться для хранения секретных паролей, используемых для включения определенных нами ролей. Для выполнения авторизации, знать нам эти пароли не понадобиться, соответсвенно и доступа к ним, у пользователя, не будет .

create or replace package SYSAUTH as
    function  TestPass(P_US in varchar2,
                       P_PS in varchar2,
                       P_SS in varchar2) return varchar2;
    function md5(P_IN in varchar2) return varchar2;
end SYSAUTH;
/

create or replace package body SYSAUTH as

    function my_role(P_ID in number) return varchar2
    as L_SS varchar2(100) default NULL;
    begin
      select ’,”’||NM||’” identified by ”’||PS||’”’
      into L_SS from ROLE_LIST where ID = P_ID;
      return L_SS;
    exception
      when others then
        return NULL;
    end;

    procedure SET_APP(P_US in varchar2)
    as L_ID number default NULL;
    begin
      DBMS_APPLICATION_INFO.SET_MODULE('SEC','');
      DBMS_APPLICATION_INFO.SET_CLIENT_INFO(P_US);
      select max(ID) into L_ID from USER_LIST
      where NM = P_US and bitand(TP,1)+0>0;
      if not L_ID is NULL then
         DBMS_SESSION.SET_CONTEXT(’MYCTX’,’ID’,TO_CHAR(L_ID));
      end if;
    end;

    procedure Activate(P_US in varchar2,                       
                       L_TP in number,
                       O_SS out NOCOPY varchar2) as
    begin O_SS := ’CONNECT ’||my_role(0);
      if bitand(L_TP,1)+0>0 then
         O_SS := O_SS||my_role(1);
      end if;
      SET_APP(P_US);
    end;

    procedure Log(P_US in varchar2, P_RS in number)
    as pragma autonomous_transaction;
    begin
       insert into USER_LOG(ID,NM,RS)
       values (LOG_SEQ.nextval,P_US,P_RS);
       commit;
    exception
       when others then
         rollback;
    end;
    function md5(P_IN in varchar2) return varchar2
    as L_SS varchar2(100) default NULL;
       L_RR varchar2(200) default NULL;
       L_CC varchar2(1) default NULL;
    begin
      DBMS_OBFUSCATION_TOOLKIT.MD5(INPUT_STRING    => P_IN,
                                   CHECKSUM_STRING => L_SS);
      loop
        L_CC := substr(L_SS,1,1);
        L_RR := L_RR||Trim(TO_CHAR(ascii(L_CC),’XX’));
        exit when length(L_SS) = 1;
        L_SS := substr(L_SS,2,length(L_SS)-1);
      end loop;
      return L_RR;
    end;

    function TestPass(P_US in varchar2, P_PS in varchar2,
                      P_SS in varchar2) return varchar2
    as L_TP number default NULL;
       L_SS varchar2(2000) default NULL;
    begin
      select max(TP) into L_TP from USER_LIST
      where NM = P_US and nvl(SP,sysdate+1)>sysdate;
      if not L_TP is NULL and md5(P_SS) = P_PS then
         Activate(P_US,L_TP,L_SS);
         Log(P_US,1);
      else
         Log(P_US,0);
      end if;
      return L_SS;
    end;

end SYSAUTH;
/

create or replace context MYCTX using SYSAUTH
/

Этот пакет и контекст мы используем для проверки пароля. Пользователь не будет иметь доступа к этому пакету.

 
create or replace package SECAUTH as
    auth_application_error EXCEPTION;
    PRAGMA EXCEPTION_INIT(auth_application_error,-20101);
    function  GetSalt return varchar2;
    function  CheckPass(P_US in varchar2,
                        P_PS in varchar2) return varchar2;
end SECAUTH;
/

grant EXECUTE on SECAUTH  to STRT;

create or replace view USERS as
select ID,NM,TP,SP from USER_LIST
where nvl(TO_NUMBER(sys_context('MYCTX','ID')),ID) = ID;

grant SELECT on USERS to DFLT;

create or replace trigger USERS_MDF
instead of insert or update or delete
on "USERS"
begin
  raise SECAUTH.auth_application_error;
end;
/

create or replace package body SECAUTH as

    G_SALT varchar2(100) default null;

    function  GetSalt return varchar2
    as L_NN number default NULL;
    begin
      select LOGIN_SEQ.nextval into L_NN from dual;
      G_SALT:=SYSAUTH.md5(TO_CHAR(L_NN)||
                          TO_CHAR(sysdate,’MISS’));
      return G_SALT;
    end;

    function  CheckPass(P_US in varchar2,
                        P_PS in varchar2) return varchar2
    as L_SS varchar2(300) default NULL;
    begin
      select PS||G_SALT into L_SS
      from USER_LIST where NM = P_US;
      return SYSAUTH.TestPass(P_US,P_PS,L_SS);
    end;

end SECAUTH;
/

Пакет SECAUTH, напротив, будет использоваться при выполнении авторизации, что впрочем никак не поможет злоумышленнику авторизоваться, не зная пароля.

create or replace trigger ROLES_DEL
instead of delete
on "ROLES"
begin
  raise SECAUTH.auth_application_error;
end;
/

create or replace trigger ROLES_MDF
instead of insert or update
on "ROLES"
declare pragma autonomous_transaction;
begin
  if inserting then
     insert into ROLE_LIST(ID,NM,PS)
     values(:new.ID,:new.NM,Trim(:new.PS));
  else
     update ROLE_LIST set PS=Trim(:new.PS) where ID=:new.ID;
  end if;
  execute immediate 'alter role "'||:new.NM||
                    '" identified by "'||Trim(:new.PS)||'"';
  commit;
exception
  when others then
    rollback;
    raise;
end;
/

Эти триггеры помогут нам изменять пароли включаемых ролей.


create function Login(P_US in varchar2, P_PS in varchar2)
return number AUTHID CURRENT_USER
as pragma autonomous_transaction;
   L_SS varchar2(2000) default SECAUTH.CheckPass(P_US,P_PS);
begin
  if not L_SS is NULL then
    execute immediate ’set role ’||L_SS;
    execute immediate 'alter session set current_schema = MAIN';
    return 1;
  else
    return 0;
  end if;
end Login;
/

grant EXECUTE on Login to STRT;

insert into ROLES(ID,NM,PS) values(0,'DFLT','default');
insert into ROLES(ID,NM,PS) values(1,'ADMN','admin');

insert into USER_LIST(ID,NM,PS,TP)
values(USERS_SEQ.nextval,’admin’,’admin’,1);
commit;

connect sys/&&syspass@orcl as sysdba

alter user MAIN account lock;

Ну и наконец самое интересное. Функция Login будет доступна пользователю, зашедшему в схему ENTRY. Как нам все это использовать? Очень просто:

SQL> connect ENTRY/ENTRY@orcl
Connected.
SQL> select * from main.users;
select * from main.users
                   *
ERROR at line 1:
ORA-00942: table or view does not exist


SQL> select MAIN.SECAUTH.GetSalt from dual;

GETSALT
----------------------------------------------
791A86ECB24B3CC72E214AF874C8BDA6

SQL> select MAIN.Login('admin','58A283B2DDAC84C122965216410B27') from dual;

MAIN.LOGIN('ADMIN','58A283B2DDAC84C122965216410B27')
----------------------------------------------------
                                                   1

SQL> select * from users;

        ID NM                                     TP SP
---------- ------------------------------ ---------- ---------
         1 admin                                   1

Зайдя в ENTRY, наша программа запросит уникальный Salt, затем выполнит MD5 преобразование, аналогично тому, как это делается в серверном коде и передаст результат в функцию Login.Функция вернет код результата авторизации, предоставит необходимые права и переключит текущую схему на MAIN.

Комментариев нет:

Отправить комментарий