作者:安华金和 思成
数据库攻击的目的最终是为要获取数据库中有价值的数据,而获取数据最有效的方法就是直接获取DBA权限。本文通过Oracle数据库中的一个经典漏洞,演示从普通用户提权到DBA权限的过程,DBSec Labs数据库安全实验室给出针对性的防护建议。
CTXSYS.driload.validate_stmt是一个Oracle的经典漏洞。出现在Oracle9i中,从10g开始被修复。这个漏洞是直接注入形漏洞的代表。漏洞发生在CTXSYS创建的driload包中的存储过程validate_stmt中。首先我们通过解压的方式打开validate_stmt观察源码。
validate_stmt结构如下:
CREATE OR REPLACE PACKAGE BODY DRILOAD IS
PROCEDURE VALIDATE_STMT( SQLSTMT IN VARCHAR2 )
IS
…
…
BEGIN
SRC := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE( SRC, SQLSTMT, DBMS_SQL.NATIVE );
RET := DBMS_SQL.EXECUTE_AND_FETCH( SRC );
DBMS_SQL.CLOSE_CURSOR( SRC );
…
…
END VALIDATE_STMT;
在源码中可以直观的看到存储过程中主要使用了DBMS_SQL包中的各种存储过程和函数。这说明validate_stmt的漏洞主要是DBMS_SQL的漏洞。
既然问题出在DBMS_SQL上,那么我们就有必要对它的功能和使用场景有一个初步的了解。DBMS_SQL主要被用来解决需要动态处理数据或表结构的问题。比如对一批表里的数据进行处理,或者批量创建表,索引,触发器等等,这时就可以通过DBMS_SQL包进行操作。下图来自Oracle官方揭示了DBMS_SQL运行时的整体逻辑。
通过对DBMS_SQL包中的存储过程和函数的分析得出结论,在函数中可以被注入超长参数的只有DBMS_SQL.PARSE。
在此函数中的参数STATEMENT应该是注入的突破点,这是因为此参数中的VARCHAR2类型可以容纳较长的字符串,这使在字符串中嵌入提权命令成为可能。
笔者构造提权语句如下:
SYS.DBMS_SQL.PARSE( SRC, 'GRANT DBA TO PUBLIC', DBMS_SQL.NATIVE
构造出提权语句,想要通过SYS.DBMS_SQL.PARSE去执行,还需要按照DBMS_SQL处理DLL的要求构造入侵函数块,示例如下:
DECLARE
SRC NUMBER;
RET NUMBER;
BEGIN
SRC := DBMS_SQL.OPEN_CURSOR;
SYS.DBMS_SQL.PARSE( SRC, 'GRANT DBA TO PUBLIC', DBMS_SQL.NATIVE );
RET := DBMS_SQL.EXECUTE_AND_FETCH( SRC );
DBMS_SQL.CLOSE_CURSOR( SRC );
END;
实际攻击效果如下:
提权语句的SQL块在低权限用户上执行,提权显示失败。究其原因,因为Oracle特别设置DBMS_SQL.PARSE为AUTHID CURRENT_USER(调用者权限)。调用者权限决定DBMS_SQL.PARSE在执行之前会判断当前执行它的用户是否具有调用他的权限(DBA权限)。
但是这里Oracle只是单纯的降低了CTXSYS的权限,并未对DBMS_SQL.PARSE做有效的防护处理。虽然DBMS_SQL.PARSE被调用者权限保护,不能被直接注入。但如果某个DBA账号建立一个定义者权限的存储过程或是函数(暂且叫这个存储过程为A)中调用了DBMS_SQL.PARSE。并且把A的执行权限赋予PUBLIC。那么低权限用户则有机会通过A去调用DBMS_SQL.PARSE。
以下测试在Oracle 11.2上进行
首先检查DBA用户数量,为最后作为对照组用
在DBA账号SYS下建立存储过程A,A中间调用了DBMS_SQL.PARSE,并把存储过程A的执行权限赋予PUBLIC。使所有用户都可以执行A。
切到低权限SCOTT上执行SYS.A并把参数改为提权语句GRANT DBA TO PUBLIC。
虽然程序报错但其实已经执行了提权命令。切回SYS用户再次查询DBA用户,发现多了一个DBA用户PUBLIC。PUBLIC正式通过GRANT DBA TO PUBLIC这个命令获得提权的。
至此提权到DBA成功。
以上Oracle调用者权限利用DBMS_SQL漏洞进行DBA提权的过程中存在四个关键点:
1. DBA用户创建定义者权限的函数或存储过程。(定义者权限可使任意用户可以在调用函数或存储过程的运行时中获得DBA权限。)例子中的存储过程A就是定义者权限。低权限用户SCOTT在调用A的运行时中具备了A的创建者SYS具有的DBA权限。致使DBMS_SQL的调用者权限保护机制失效。
2. DBA账号创建的函数或存储过程把执行权限赋予PUBLIC。如果A的执行权限不是PUBLIC,scott根本无法调用A,也不会出现后面的利用方式。
3. 对输入参数的限制和防守没有加强,使得某些非预计参数的执行。
4. 未对用户身份进行判断。加强对用户身份的识别可以在一定程度防止低权限用户进行非法提权操作。
针对数据库漏洞的防护要做到以下几点:
1.自己不制造漏洞,使用安全性更高的函数或存储过程。可以使用DBMS_SYS_SQL代替DBMS_SQL。
笔者在实践中发现DBMS_SYS_SQL.PARSE_AS_USER这个存储过程可以代替DBMS_SQL.PARSE这个危险的存储过程。分析调用关系不难发现DBMS_SQL.PARSE和DBMS_SYS_SQL.PARSE_AS_USER其实调用的都是DBMS_SYS_SQL中的ICD_PARSE。但不同的是调用的ICD_PARSE有一位参数不同。
DBMS_SQL.PARSE调用的是:
ICD_PARSE( C, STATEMENT, LANGUAGE_FLAG MOD PARSE_AS_USER_FLAG, NULL );
而DBMS_SYS_SQL.PARSE_AS_USER调用的是:
ICD_PARSE( C, STATEMENT, LANGUAGE_FLAG MOD PARSE_AS_USER_FLAG + PARSE_AS_USER_FLAG, USERID );
仔细比较两者不难发现最后一位参数USERID不同。DBMS_SQL.PARSE对应的取的是null,而DBMS_SYS_SQL.PARSE_AS_USER对应的取的是当前用户的USERID。通过USERID来判断当前用户的权限是否能解析语句,而并非是运行时权限来判断是否能解析语句。
对比说明,存储过程Q采用DBMS_SYS_SQL.PARSE_AS_USER,存储过程P则采用DBMS_SQL.PARSE。存储过程P和Q唯一的区别就是分别采用DBMS_SYS_SQL.PARSE_AS_USER和DBMS_SQL.PARSE来解析SQL语句。
存储过程P成功实现了DBA提前操作。存储过程Q采用了DBMS_SYS_SQL.PARSE_AS_USER,成功阻止了低权限用户的提权操作。
2、使用第三方的数据库安全加固产品及解决方案
应对已知数据库安全问题最佳方案是升级数据库,但升级数据库存在很多风险和复杂的准备工作。即便做了升级测试、最小化测试、功能测试、集成测试、性能测试、容量与负载压力测试等一系列复杂的测试依旧可能导致某些应用或组件出现各种难以预计的问题。
在不对数据库本身进行升级操作的情况下,可以通过虚拟补丁(Vpatch)技术来完成对数据库系统漏洞的防护。数据库虚拟补丁技术一般是集成在数据库防火墙产品中,能够有效防止通过数据库本身漏洞对数据库的攻击行为。有效的解决了生产场景下数据库升级不及时可能给用户的数据库带来的潜在威胁。并在无需对数据库做深度操作的前提下,进行漏洞的修补与防护,以期达到数据库的安全标准要求。