
最近在做一个授权渗透测试项目目标是一个基于Java技术栈的电商后台系统。最初的发现非常常规某个查询接口存在SQL注入通过报错回显能直接拿到数据库版本信息。这类问题放在平时可能拖出几张表就写报告结束了。但那天我多问了自己一句这条注入链路还能不能再往前走一步结果一路向下探最后触达的竟然是一个SpEL表达式注入点实现了从数据库到应用层再到命令执行层的完整跳转。整条链路走通之后我才真正意识到从SQL注入到SpEL表达式注入这条组合路线远比单点漏洞的报告价值高得多它本质上是在用一条攻击链测试一个系统从数据层到执行层的信任边界。这篇文章不打算讲那种一个漏洞搞定一切的童话故事。真实场景下单点漏洞的价值往往是有限的组合拳的价值才真正反映出一个系统在面对有耐心、有思路的攻击者时到底能扛到哪一层。适合谁看做渗透测试、漏洞挖掘、安全开发的人以及那些想理解为什么修了一个SQL注入系统还是被打穿的甲方同学。1. 为什么把SQL注入和SpEL组合在一起1.1 单一漏洞的局限性先说一个很现实的问题SQL注入在今天的渗透测试里已经不算什么稀罕物了。很多初学安全的人觉得发现一个SQL注入就等于拿下了数据库但实际情况往往没那么乐观。现在的数据库账号分权做得普遍比以前好很多应用连接数据库用的账号只有增删改查权限连LOAD FILE、INTO OUTFILE这些操作都没有。你折腾了半天最多把某张业务表拖出来拿到一些用户手机号、订单数据然后呢写报告结束。但从攻击者的视角看SQL注入其实不是终点而是一个入口。它的真正价值取决于你能从数据库这层拿到什么以及这些信息能不能帮你在应用层找到下一个突破口。如果只把SQL注入看作拖数据的手段那它确实局限但如果你把它看作读取系统配置、探测应用链路、触发更深层漏洞的通道它的效力会被放大很多倍。这个思路上的转变就是组合拳打法的起点。1.2 组合拳的核心思想组合拳的本质是把彼此独立、看似不相关的漏洞通过业务逻辑串联起来形成一条从外部到内部、从低权限到高权限的攻击路径。拿这次的项目举例SQL注入负责提供数据库层的访问通道而数据库里往往藏着大量应用运行期的秘密——配置表、管理员令牌、第三方密钥、甚至整个application.yml的内容SpEL表达式注入则负责在应用层打开命令执行的口子。两个漏洞单独看可能分别都是高危但串起来以后就有机会拿到服务器控制权。这就像闯一栋楼SQL注入是帮你混进大厅的手段SpEL注入才是那个能直接打开保险柜的钥匙。大厅里什么都有——门禁卡、楼层平面图、保安交班表关键在于你有没有意识到这些东西能用来找到那把钥匙。组合拳打的不是某个漏洞本身而是系统内部信息孤岛之间的信任缺口。2. SQL注入先拿下第一个突破点2.1 注入点存在性的判断这次的SQL注入出现在一个订单查询接口上。参数是订单编号后端大概拼接了类似这样的语句SELECT * FROM orders WHERE order_no {user_input}接口返回很直接单引号会直接触发语法错误并且在错误信息里完整回显了SQL语句片段。这种报错回显型注入在当前环境里算是友好型了连盲注都不用打。判断注入点的时候我习惯先用三类输入交叉验证单引号、布尔条件、时间函数。单引号看有没有语法错误布尔条件看返回内容是否随条件变化时间函数看响应延迟。三者都命中才下结论说这里能注入。要注意的是有些系统接入WAF之后报错信息会被吞掉但响应时间不会撒谎。所以即使当时没有报错回显我也会把时间盲注的payload先在本地准备好确保随时能切换到静默模式继续探测。实际测试中很多新手一看到报错就兴奋忽略了对注入类型的完整判断这会导致后面利用时选错payload方向。2.2 数据库权限探测与利用面确认注入点之后第一步不是急着脱库而是测权限。很多人忽略这一步直接union select拖数据但权限决定了你后续能走多远。我用了一条非常简单的payload来探测当前数据库用户和权限-- 查询当前数据库用户及权限仅在授权环境测试 SELECT current_user(); SELECT * FROM information_schema.user_privileges WHERE GRANTEE LIKE %testuser%;结果非常有意思目标数据库账号是testuser但居然拥有FILE权限这意味着可以通过LOAD_FILE()读取数据库所在服务器的本地文件。大多数数据库账号不会开这个权限一旦开了利用面立刻就宽了。我马上尝试读取应用配置文件这一步是整个组合拳的转折点。这里补充一个细节LOAD_FILE()能读文件的前提是数据库进程对目标文件有读权限且文件在当前服务器上。如果数据库和应用不在同一台机器读到的就是数据库服务器的文件不是应用服务器的。这次的架构里数据库和应用虽然分库部署但配置文件中明文保存了应用服务器的连接信息和管理员接口地址已经足够往下走了。2.3 读取配置文件的关键步骤因为我需要的不是用户数据而是应用层的地图所以读取目标直接锁定在常见的配置文件路径上。Java应用通常会把配置放在application.yml或application.properties里路径一般在应用安装目录下。我用文件读取的方式拿到了这样一段核心配置出于安全考虑这里只展示结构真实内容已脱敏spring: datasource: url: jdbc:mysql://10.0.0.x:3306/appdb username: appuser password: xxxx server: address: 192.168.x.x management: endpoints: web: exposure: include: *很多人看到配置文件的反应是哦拿到数据库密码了然后转头去连数据库。但我的关注点落在了management.endpoints.web.exposure.include: *这一行上。这是Spring Boot Actuator的配置*意味着所有执行器端点全部暴露。结合之前读取到的管理员接口地址一个更大胆的想法冒了出来如果应用端有表达式解析的点配合暴露出来的执行器端点是不是可以直接往命令执行的方向打这就是组合拳的关键所在SQL注入只是让你拿到了地图而地图上标注的密道入口可能藏在完全不同的技术栈里。3. SpEL表达式注入Java世界的命令执行入口3.1 SpEL的运行机制与危险特性SpELSpring Expression Language是Spring框架提供的表达式语言在Java生态里极其常见常被用于配置注入、权限校验、动态路由等场景。它能访问对象属性、调用方法、执行静态方法甚至能通过T()运算符引用任意类。而StandardEvaluationContext模式下表达式解析是全功能的——这意味着如果能控制传递给解析器的表达式内容就相当于在目标JVM进程里获得了一个代码执行点。举一个最典型的危险写法。假设某接口接收一个keyword参数后端代码做了这样的处理// 反例直接对用户输入做表达式求值仅用于测试演示 ExpressionParser parser new SpelExpressionParser(); StandardEvaluationContext context new StandardEvaluationContext(); String result parser.parseExpression(userInput).getValue(context, String.class);这时传入T(java.lang.Runtime).getRuntime().exec(id)就足够在服务器上执行命令了。由于SpEL表达式可以用T()引用任意类理论上能达到的效果和反射调用差不多。这不是某个框架的隐藏漏洞而是允许不可信输入进入全功能表达式解析器这个错误写法的必然结果。3.2 什么场景会出现SpEL注入在实际代码里SpEL注入的入口往往藏得比SQL注入深。最常见的几种场景配置中心动态更新配置配置项直接被解析为SpEL表达式规则引擎或策略模式用SpEL做复杂条件判断接口层直接接收表达式参数用于自定义排序、过滤AOP切面里对注解参数做表达式解析缓存key的生成逻辑包含可变量拼接在这次项目中SpEL注入点正好落在一个规则配置接口上。业务方说这个接口是为了让运营人员自定义促销活动的计算规则比如当订单金额大于X时折扣为Y后端的实现是把规则字符串直接丢进ExpressionParser做求值。从业务上讲这个功能设计得很聪明从安全上讲这是一颗定时炸弹。3.3 深入从表达式解析到命令执行SpEL注入和SQL注入有个很大的不同SQL注入通常需要绕一堆语法限制而SpEL注入一旦触发基本就是直接的代码执行。原因在于StandardEvaluationContext本身没有做能力裁剪它把整个Java类型系统都暴露给了表达式。我的实测过程是这样的先确认接口对异常信息有回显输入一个简单的非法表达式看错误信息里是否暴露了解析器的类型然后尝试一个无副作用的表达式比如T(java.lang.Math).random()看响应里有没有返回随机数最后才替换成命令执行表达式。这一套渐进式验证可以最大限度避免在确认漏洞存在之前就触发危险操作。// 用于验证命令执行的回显表达式 T(java.lang.Runtime).getRuntime().exec(new String[]{/bin/sh, -c, id})从表达式到系统命令执行中间隔着的只是解析器配置。如果业务代码里用的是SimpleEvaluationContext那很多危险操作会被拦下来但只要用了StandardEvaluationContext攻击者就拥有了一把万能钥匙。这也是我后来在报告中反复强调的一点SpEL本身不可怕可怕的是拿全功能上下文去解析不可信输入。4. 组合拳完整链路与实操4.1 链路设计从数据库到应用层现在把前面的发现串起来整条组合拳链路是这样的SQL注入 → 获取数据库FILE权限 → 读取应用配置文件配置文件 → 暴露Spring Boot Actuator端点、应用地址、管理员接口Actuator端点 → 查看环境详情确认SpEL相关组件的使用情况规则配置接口 → 确认SpEL注入点 → 构造表达式执行命令这条链路的每一步都是上一步的凭证没有SQL注入读不到配置文件没有配置文件不一定能快速定位到Actuator暴露和规则接口没有Actuator的信息SpEL注入的利用方向就不够清晰。从攻击者视角看这四步是环环相扣的。4.2 具体步骤拆解第一步通过SQL注入确认数据库权限。这一步要记录好current_user()和version()的结果后面排查时要用。第二步读取配置文件。服务端支持LOAD_FILE()的情况下优先尝试绝对路径读取。常见的路径包括/opt/app/config/application.yml、/etc/app/application.properties。读不到就换目录利用报错信息中的路径提示来缩小范围。第三步根据配置文件中的server.address访问应用管理端口。这里要特别提醒在授权测试中任何跨系统的横向动作都必须严格遵循测试范围和授权书。我在这一步只做了本系统的验证性访问没有做任何横向移动。# 查看Actuator端点是否可访问仅限授权环境 curl -s http://192.168.x.x:8080/actuator第四步寻找SpEL注入入口。规则配置接口通常是POST类型参数会包含规则表达式或condition关键字。构造一个探测表达式确认解析器存在再渐进式升级为命令执行验证。4.3 关于组合拳的适用边界并不是所有系统都能走通SQL注入→SpEL注入这条路线。它有几个前置条件目标技术栈必须是Java系且用了Spring生态SQL注入点必须能读取到有效配置文件或者能间接暴露应用层信息应用层必须存在一个可被外部调用的SpEL表达式解析点各环节之间必须有信息关联否则就只是两个孤立漏洞如果前置条件不满足组合拳就要换成别的组合比如SQL注入加文件上传、SQL注入加反序列化等等。思路永远是通用的先拿到信息再放大信息最后用信息去触发一个更致命的漏洞。5. 实操中的常见问题与避坑经验5.1 问题排查速查表这条路走下来我踩了不少坑也总结了几个高频问题的排查方向列在下面供参考。现象可能原因排查方向SQL注入报错但无法读文件数据库进程缺少文件读权限改用LOAD DATA INFILE、尝试备份文件路径配置文件读到但内容为空LOAD_FILE()对特殊编码文件可能返回NULL确认文件路径是否正确、文件是否为文本格式Actuator端点无法访问管理端口与应用端口分离或网络策略限制查看配置文件中management.server.port项SpEL表达式无回显接口存在统一异常处理吞掉了错误信息改用时间延迟表达式如T(java.lang.Thread).sleep(5000)命令执行不成功执行环境可能没有/bin/sh换用cmd.exe /c或Java反射方式再试5.2 几个容易被忽略的细节第一个细节SQL注入读文件不只局限于配置文件/etc/passwd、历史命令文件、临时目录下的上传文件、日志文件都可能包含有价值的信息。不过在实际测试中我还是建议把精力集中在跟应用运行相关的配置上信息利用率最高。第二个细节SpEL表达式注入探测阶段避免用过重的payload。先用无副作用的表达式确认解析行为再考虑是否执行命令。这样既能确认漏洞又能把测试风险控制在合理范围内。第三个细节组合拳中的每个环节都要保留证据。包括注入payload、配置文件截图、Actuator端点返回内容、SpEL验证表达式和响应结果。写报告时这些证据能把整条链路讲得非常扎实也有助于甲方理解为什么单修一个SQL注入远远不够。第四个细节很多SpEL注入点藏在看似人畜无害的业务功能里。促销规则、数据筛选、动态表单校验这些地方一旦实现偷懒用了parseExpression就是现成的入口。挖洞时看到这种灵活配置类功能值得多花时间盯一下。6. 从攻击链反推防御策略6.1 SQL注入侧的防御要点SQL注入的修复其实没有太多新东西预编译语句、参数化查询、最小权限原则这些都做到了绝大部分注入点能被挡掉。但这次案例中有一个特殊教训即使应用本身对大部分SQL都做了预编译那个订单查询接口只要有一处手写拼接前面所有防御都归零。所以代码审计里比起到处查注入更重要的是建立所有SQL语句只能通过统一数据访问层出库的纪律。数据库权限的最小化这次也体现得很明显。如果当时数据库账号没有FILE权限哪怕SQL注入成功也没办法直接读取配置文件组合拳在第一环就会断掉。建议所有业务环境自查一遍生产数据库账号是否具备FILE、SUPER这类高危权限如果没有合理业务诉求全部回收。6.2 SpEL注入侧的防御要点SpEL注入修复的核心是把解析能力降到最低限度。能用SimpleEvaluationContext就不用StandardEvaluationContext后者连类引用都开放了跟直接交给攻击者一个Java反射接口没什么区别。还有一个容易被忽视的点对用户输入做黑名单过滤不是有效的SpEL注入修复手段。表达式语言的语法灵活到离谱黑名单永远追不上语法变体。正确做法是把表达式明确当成代码来管理不允许业务用户直接提交表达式字符串改为提交结构化数据由后端用预定义好的逻辑去解释。6.3 纵深防御的工程化落地从这次组合拳案例可以看出单点防御再强也扛不住信息关联带来的放大效应。真正有效的防御体系应该是分层的网络层限制管理端口的访问来源应用层对执行器端点做认证和授权数据层控制数据库账号的文件读写权限代码层杜绝危险表达式解析模式。四层各守一段攻击者就算打通了其中一环也未必能顺着链路走到头。我后来在做复盘报告时给甲方提了一个很具体的建议把配置文件中的敏感信息和接口能力的暴露面做成联动清单每次配置变更时自动触发一轮轻量级安全扫描。这个建议的本意就是要切断攻击链中信息获取→信息利用的关键环节。毕竟漏洞的组合拳再厉害只要信息传不出去后面的拳法就全都打空了。写在最后的个人体会这条组合链路走通之后我最大的感受是漏洞挖掘真的不是找一个洞就交差的活。SQL注入太常见了常见到很多人会下意识地低估它——可只要你愿意多想一步愿意从数据库层的角落里翻出那份配置文件后面等着你的可能就是一片完全不同的攻击面。反过来SpEL注入也没有很多人想象中那么神秘它就是不可信输入进入了强大的解析引擎这件事的必然结果。两者的结合点正是现代Web系统里数据层与应用层之间那条往往没人检查的信任链。愿每一次测试都能守好授权的边界也愿每一份报告都能让系统的下一层防御变得更扎实。