从一道CTF题浅谈MyBatis与Ognl的那些事
字数 1535 2025-08-06 20:12:33
MyBatis与OGNL表达式安全风险深度分析
0x01 引言
本文通过分析一道CTF题目"ezsql",深入探讨MyBatis框架中OGNL表达式的使用及其潜在的安全风险。主要解决以下问题:
- 为什么SQL注入能解析OGNL表达式达到RCE的效果?
- Provider注解、XML配置和@Select配置的安全差异
- 预编译(#{})是否能规避此类风险
0x02 MyBatis封装SQL流程
2.1 核心解析过程
MyBatis工作流程分为构建和执行两个阶段:
- 构建阶段:解析XML/注解配置,转换为内部对象
- 执行阶段:通过配置信息执行SQL,与JDBC交互
关键解析流程:
// Mapper代理入口
org.apache.ibatis.binding.MapperProxy#invoke
↓
// 执行Mapper方法
org.apache.ibatis.binding.MapperMethod#execute
↓
// 获取BoundSql
MappedStatement#getBoundSql
↓
// SQL源处理
SqlSource接口实现类:
- StaticSqlSource:静态SQL
- DynamicSqlSource:处理${}和动态SQL节点
- RawSqlSource:处理不包含${}和动态SQL节点
- ProviderSqlSource:处理@SelectProvider等注解
${}与#{}的区别
-
${}:直接替换,调用DynamicSqlSource解析
- 解析流程:
TextSqlNode.apply()→GenericTokenParser.parse()→BindingTokenParser.handleToken()→OgnlCache.getValue()
- 解析流程:
-
#{}:预编译处理,使用ParameterMappingTokenHandler替换为占位符?
0x03 安全风险分析
3.1 XML配置风险
理想情况下,如果XML中直接写入恶意OGNL:
<select id="test" parameterType="String" resultMap="User">
select * from users where username like ${@java.lang.Runtime@getRuntime().exec("calc")}
</select>
实际场景中,MyBatis设计已规避此风险:
- 仅解析原始
${username}表达式 - 用户输入值直接赋值,不进行二次OGNL解析
3.2 注解配置风险
3.2.1 普通注解(@Select等)
- 与XML配置类似,SQL语句固定
- 无法直接插入用户可控的OGNL表达式
3.2.2 Provider注解风险(@SelectProvider等)
关键风险点:
- 允许动态生成SQL字符串
- 用户输入可能直接拼接到SQL中
- 拼接后的SQL若包含${},会触发OGNL解析
漏洞利用示例:
// Controller
@GetMapping("/mybatis/ognl")
public List<User> mybatisOgnl(@RequestParam("name") String name) {
return userMapper.getUserByUserName(name);
}
// Mapper
@SelectProvider(type = FindUserByName.class, method = "getUser")
List<User> getUserByUserName(String name);
class FindUserByName {
public String getUser(String name) {
String s = new SQL() {
{
SELECT("*");
FROM("users");
WHERE("username='" + name+"'"); // 直接拼接用户输入
}
}.toString();
return s;
}
}
POC:
${@java.lang.Runtime@getRuntime().exec("open /System/Applications/Calculator.app")}
0x04 防御措施
4.1 MyBatis 3.5.4+的黑名单机制
在org.apache.ibatis.ognl.OgnlRuntime#InvokeMethod中:
_useStricterInvocation默认为true- 黑名单包含
Runtime和ProcessBuilder等危险类
4.2 修复建议
-
使用预编译:在Provider中采用#{}方式
WHERE("username = #{name}") -
输入过滤:必须拼接时,严格检查用户输入
-
升级MyBatis:使用3.5.4+版本利用内置黑名单
0x05 总结
风险场景矩阵:
| 配置方式 | SQL注入风险 | OGNL注入风险 | 备注 |
|---|---|---|---|
| XML + ${} | 高 | 低 | 需直接修改XML |
| @Select + ${} | 高 | 低 | 同XML |
| @SelectProvider | 高 | 高 | 用户输入可触发OGNL解析 |
| 任何方式 + #{} | 低 | 低 | 推荐使用 |
关键结论:
- Provider注解是OGNL注入的主要风险点
- 预编译(#{})可同时防范SQL注入和OGNL注入
- MyBatis 3.5.4+通过黑名单提供了额外防护