一个单引号打穿十年ORM:Exposed全版本SQL注入(CVE-2026-108474附PoC)
原创
赤霄攻防实验室
赤霄攻防实验室
赤霄攻防实验室
2026年10月11日 09:28
河北
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
JetBrains 昨晚(10 月 9 日)发布了 Exposed 1.5.1,同一天一个编号悄悄进入 NVD:CVE-2026-108474,CVSS 9.8 严重级。这不是某个小众组件的边角料——Exposed 是 JetBrains 官方维护的 Kotlin ORM 框架,大量 Spring/ktor 后端拿它写数据库访问层。问题一句话讲完:多个常用 SQL 函数的字符串参数被当成字面量直接拼进 SQL 文本,单引号不做转义。只要业务代码把用户可控的字符串传给这些函数,一个单引号就能改写整条 SQL 的结构。受影响范围横跨整个项目历史:从 0 起所有版本,修复版本只有 1.5.1。
TL;DR 四行速览
· 是什么:JetBrains Exposed(Kotlin 官方 ORM)SQL 函数字符串实参未转义注入,CVE-2026-108474,CVSS 9.8;
· 影响:locate()/concat()/groupConcat()/STRING\_AGG/JSON path 等函数 + 用户可控字符串 = 可 UNION、可盲注、可改写整条语句,全部历史版本受影响;
· 修复:升级 exposed-core/exposed-jdbc 至 1.5.1(实参改参数绑定),API 不变、业务代码零改动;
· 看点:我们在 H2 + Exposed 1.5.0/1.5.1 双版本上实测——同一段恶意输入,旧版跨表泄出令牌,新版零泄露;附非破坏性验证 PoC。
封面:全版本受影响的 9.8 分
漏洞档案
CVE-2026-108474 漏洞档案信息卡
先看 CVE 记录原文的措辞:“SQL injection was possible via unescaped string arguments of several SQL functions”——“若干 SQL 函数的未转义字符串实参”。这句话信息量很大:它把责任边界画在了框架侧。传统认知里,ORM 是 SQL 注入的“解药”:开发者用参数化查询,用户输入走绑定通道,天然隔离。但 Exposed 这次栽在了函数参数这个不起眼的角落:这些函数的字符串实参不是走 PreparedStatement 绑定,而是被序列化成带引号的字面量直接写进 SQL 文本。当输入里出现单引号,字面量提前闭合,后面的内容就从“数据”变成了“语法”。
根因:拼接了十年的字面量
对比 1.5.0 与 1.5.1 的源码 diff,修复 commit(4cf829b55,EXPOSED-1093)改写了所有方言的函数生成器。以最典型的 locate()(SQL 的 LOCATE/POSITION,判断子串位置)为例,PostgreSQL 方言在 1.5.0 里是这样拼接的:POSITION(' + substring + ' IN expr)——substring 原样进入单引号内。同理还有 MySQL 的 CONCAT\_WS('separator',…)、GROUP\_CONCAT(… SEPARATOR 'separator')、PostgreSQL 的 STRING\_AGG(expr, 'separator')、JSON path 的 '$.path' 等,遍布每个方言实现。
1.5.1 的修法很干净利落:新增 appendStringArgument 机制,把这些实参全部改为查询参数绑定(Redshift 等不支持绑定的场景退化为单引号转义)。这是 ORM 该有的样子——数据永远走数据通道。顺带一提,另一个修复 commit(36897852e,EXPOSED-909)专门处理了 locate 的转义残留,两个 commit 共同构成 1.5.1 的安全修复面。
攻击链实测:一个搜索框到全库数据
攻击链五步:从搜索框关键词到跨表数据泄露
光说不练不是我们的风格。我们搭了一个最小复现环境:H2 内存数据库 + 两张表——users(id, name)与 secrets(id, secret,存着一个模拟令牌 S3CRET-TOKEN-9831),再写一段最普通不过的业务代码:搜索框关键词直接传入 Users.name.locate(userInput)(等价于“名字里包含关键词的用户”)。这是无数后端每天都在写的查询。
恶意输入是一段精心构造的字符串:x', 'x') > 0 UNION ALL SELECT ID, SECRET FROM SECRETS UNION ALL SELECT ID, USERS.\"name\" FROM USERS WHERE LOCATE('x ——它的巧妙之处在于保持了占位符数量与列结构对齐,让注入后的 SQL 依然合法。在 1.5.0 上,这条查询返回了 [1:alice, 2:bob, 1:S3CRET-TOKEN-9831]:secrets 表的令牌混进了用户列表。同一份代码编译到 1.5.1 再跑,同一输入返回空列表——参数绑定让骨架不可改写。这就是“拼接 vs 绑定”一图胜千言的对照。
修复对照与自查
同一段恶意输入,两个版本的实测输出对照
哪些代码需要盯
结合 diff,受影响的函数族包括:locate()/substring 定位类、concat() 带 separator 变体、groupConcat() 的 SEPARATOR、PostgreSQL 方言的 STRING\_AGG 与 json path 系列(json\_extract/json\_extract\_path 等)、match() 全文匹配的 pattern。自查方式很直接:在代码库里 grep 这些函数名,检查实参是否来自用户输入(搜索词、排序字段、过滤条件、报表参数都是高危位)。另外两个容易被忽略的点:一,列注释(columnComment/COMMENT ON COLUMN)同样存在单引号拼接,若表结构里的注释含外部数据也在影响面内(1.5.1 一并改为转义);二,如果你的项目在 DSL 层之外还有自定义方言或 raw SQL 封装,升级后建议回归测试一遍 SQL 生成。
修复与缓解
唯一修复版本是 1.5.1,Maven 坐标 org.jetbrains.exposed:exposed-core / exposed-jdbc 升级即可,函数签名未变,业务代码零改动。无法立即升级时的缓解:在调用这些函数前对输入做白名单过滤或单引号拒绝(不是转义——转义责任这次在框架);更彻底的是把这类“函数+用户字符串”的调用点改为等价的参数化写法。另外提醒一句:9.8 是框架级上限评分,实际危害取决于你的业务里有多少调用点传了用户可控字符串——一个都没传,风险为零;有一个搜索框直传,就是全库可读。
非破坏性验证 PoC
我们按惯例写了一个非破坏性验证脚本(完整验证版):指纹识别(actuator/依赖树探测 Exposed 版本)→ 版本影响矩阵(<1.5.1 命中)→ 可选黑盒探测(对目标端点发良性探针与语法破坏探针,比对响应分叉,不读取任何数据)→ 输出 VULNERABLE / PATCHED / UNKNOWN 三态结论。默认目标 127.0.0.1,仅限授权测试。
PoC 地址:https://github.com/Rubby2001/CVE-POCS/tree/main/CVE-2026-108474
参考
· NVD:https://nvd.nist.gov/vuln/detail/CVE-2026-108474
· CVE 记录:https://www.cve.org/CVERecord?id=CVE-2026-108474
· Exposed 1.5.1 Release:https://github.com/JetBrains/Exposed/releases/tag/1.5.1
· 修复 diff(compare 1.5.0...1.5.1):https://github.com/JetBrains/Exposed/compare/1.5.0...1.5.1
· PoC:https://github.com/Rubby2001/CVE-POCS/tree/main/CVE-2026-108474
免责声明:本文仅用于防御研究与授权安全测试,任何人不得将文中技术用于非法用途。文中复现在隔离本地环境完成,未触及任何真实系统。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论