“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”
01
使用开源工具下载地址如下:https://github.com/Chanzi-keji/chanzi/releases 它也是没有发现sql注入。
用codeql生成原始的数据库(这里的原始指的是没有对其自己编写的jar包之类的进行反编译,可能会存在污点数据流跟进失败)
其实codeql也是无法识别出来的,这就很好奇对于代码审计这个赛道普遍无法处理的sql注入到底是怎样的
为什么都不能处理这个sql注入呢?所以想分析一下codeql对于mybatis场景下sql 的规则是怎么样的
codeql-mybatis-xml
codeql-mybatis-xml-sink
规则里对于sql注入的sink类型是 MyBatisMapperMethodCallAnArgument,其实看名字就可以知道他是用来查找 Mybatis 对应传入的参数的,他的实现如下图,下面的流程可以大致的理解为从使用 MyBatisMapperXmlFile 将 SQL 的 XML 文件找出来,利用 MyBatisMapperSqlOperation 将 MyBatisMapperXmlFile XML中的SQL操作与Java代码中的方法函数关联起来,获取到了方法函数后与 MethodCall 进行判断取出所有方法里的参数
所以简而言之,这个 SQL 注入的 Sink 非常泛指,获取到所有 Mybatis 的 XML 方法的参数,那这个参数就是 Sink。但是这就奇怪了,明明是 ${} 这样没有预编译的写法的才会有问题,但是为什么把所有的参数都认为是sink??这未免有看太广泛了?这样的话岂不是全是误报了?继续往下看
codeql-mybatis-xml-isAdditionalFlowStep
isAdditionalFlowStep 谓词用于识别那些可能被遗漏的数据流步骤,特别是在用户输入被转换成SQL查询的一部分时。例如,如果用户输入被存储在一个对象的属性中,然后这个属性被用来构建SQL查询,那么这个属性的访问就是一个额外的数据流步骤。
1.识别 toString 方法调用: 规则查找所有的 MethodCall其中方法名为 "toString",并且这个方法是对象 java.lang.Object 的方法
2.连接数据流节点:规则确保如果一个表达式 node1 通过 toString 方法调用转换成了另一个表达式 node2 ,那么这两个节点之间存在一个数据流步骤。这意味着,即使用户输入被转换成了字符串,CodeQL也能够继续追踪这个数据流
这个谓词在这里的作用是,当CodeQL追踪从用户输入到SQL查询的数据流时,在实际应用中,可能有一些自定义对象,它们内部保存着一些与数据库操作相关的数据(比如用户输入的数据被封装在对象属性里),当把这些对象转换为字符串用于日志记录、传递给其他组件或者参与到后续的数据库查询构建等操作时,就需要关注 toString 方法调用前后的数据变化以及其流向是否安全,这个谓词确保了即使在用户输入被转换成字符串之后,CodeQL也能够继续追踪这个数据流,尤其是当最终流向了可能引发 SQL 注入等安全隐患的位置时,这个谓词就能帮助 CodeQL 识别出这一完整的数据流转链路。
codeql-mybaits-xml-query
其实这里的where查询条件非常好理解。条件很好解释: 1. 首先用 flowPath 确保找到的sink和source在数据流上是可以连通的(当然这个数据流是codeql来说) 2. ma.getAnArgument() = sink.getNode().asExpr() :定义了 3. 使用 myBatisMapperXmlElementFromMethod 来确保获取到的方法为 Mybatis 里的sql执行方法 4. 从 Mybatis 中取出所有的参数并且是存在 ${}这样sql注入的参数来,从而认定这个 Mybatis里的执行方法是sink点# codeql
/**
* @name SQL injection in MyBatis Mapper XML
* @description Constructing a dynamic SQL statement with input that comes from an
* untrusted source could allow an attacker to modify the statement's
* meaning or to execute arbitrary SQL commands.
* @kindpath-problem
* @problem.severity error
* @precision high
* @id java/mybatis-xml-sql-injection
* @tags security
* experimental
* external/cwe/cwe-089
*/
import java
importMyBatisCommonLib
importMyBatisMapperXmlSqlInjectionLib
import semmle.code.xml.MyBatisMapperXML
import semmle.code.java.dataflow.FlowSources
privateimport semmle.code.java.security.Sanitizers
importMyBatisMapperXmlSqlInjectionFlow::PathGraph
privatemoduleMyBatisMapperXmlSqlInjectionConfigimplementsDataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceofThreatModelFlowSource }
predicate isSink(DataFlow::Node sink) { sink instanceofMyBatisMapperMethodCallAnArgument }
predicate isBarrier(DataFlow::Node node) { node instanceofSimpleTypeSanitizer }
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodCall ma |
ma.getMethod().getDeclaringType() instanceofTypeObject and
ma.getMethod().getName() = "toString" and
ma.getQualifier() = node1.asExpr() and
ma = node2.asExpr()
)
}
}
privatemoduleMyBatisMapperXmlSqlInjectionFlow =
TaintTracking::Global<MyBatisMapperXmlSqlInjectionConfig>;
from
MyBatisMapperXmlSqlInjectionFlow::PathNode source,
MyBatisMapperXmlSqlInjectionFlow::PathNode sink, MyBatisMapperXmlElement mmxe, MethodCall ma,
string unsafeExpression
where
MyBatisMapperXmlSqlInjectionFlow::flowPath(source, sink) and
ma.getAnArgument() = sink.getNode().asExpr() and
myBatisMapperXmlElementFromMethod(ma.getMethod(), mmxe) and
unsafeExpression = getAMybatisXmlSetValue(mmxe) and
(
isMybatisXmlOrAnnotationSqlInjection(sink.getNode(), ma, unsafeExpression)
or
mmxe instanceofMyBatisMapperForeach and
isMybatisCollectionTypeSqlInjection(sink.getNode(), ma, unsafeExpression)
)
select sink.getNode(), source, sink,
"MyBatis Mapper XML SQL injection might include code from $@ to $@.", source.getNode(),
"this user input", mmxe, "this SQL operation"
走了一遍 codeql 对于的 sink,发现其实它是把user作为了传入的 source 的,所以在获取到用户输入参数上面是没有问题的。
试试了sink能否获取到
那问题出在哪呢?我尝试着去调用它的 select 查询发现 MyBatisMapperXmlSqlInjectionFlow::PathNode source 这一段并没有找到对应的source
CodeQL 的数据流分析依赖于 DataFlow::Configuration 类来定义整个数据流规则,包括 Source(起点)、Sink(终点)、Barrier(阻断点)等。通过这些定义,CodeQL 能够查找潜在的从 Source 到 Sink 的数据流路径。如果路径没有被 Barrier 阻断,那么它将被标记为潜在的安全风险。
但是为什么 source instanceof ThreatModelFlowSource 可以匹配到 Source 点,MyBatisMapperXmlSqlInjectionFlow::PathNode source 却无法匹配到 Source 点呢?
source instanceof ThreatModelFlowSource
只是检查某个节点是否是 ThreatModelFlowSource 的实例。这种查询是 独立的判断,不依赖于任何数据流配置。换句话说,这个判断仅仅是静态地识别某种类型的节点,而不是在数据流上下文中查找 Source。
MyBatisMapperXmlSqlInjectionFlow::PathNode source
MyBatisMapperXmlSqlInjectionFlow::PathNode 是基于数据流配置的分析,它要求在定义的 DataFlow::Configuration 范围内查找 Source。这意味着要找到一个 PathNode source,它必须满足 MyBatisMapperXmlSqlInjectionConfig 中的所有配置(包括 isSource、isSink、isBarrier、isAdditionalFlowStep)仅仅匹配 isSource 是不够的,还需要在 整体数据流 中有路径能够从这个 Source 连接到一个 Sink,而路径中不能被 Barrier 阻断。
特性 |
source instanceof ThreatModelFlowSource |
MyBatisMapperXmlSqlInjectionFlow::PathNode source |
独立性 |
独立查询,忽略数据流配置 |
依赖于DataFlow::Configuration的整体配置 |
匹配的条件 |
只需满足ThreatModelFlowSource条件即可 |
必须符合isSource,并能到达isSink且不被isBarrier阻断 |
路径分析 |
不进行路径分析 |
需要有从 Source 到 Sink 的有效路径 |
用途 |
简单的节点类型匹配 |
复杂的数据流分析,用于查找潜在的漏洞 |
所以也就说问题出在了 MyBatisMapperXmlSqlInjectionConfig 的位置上,找到的 Source 点数据流是无法判断到 Sink 上的,其实现在也大概可以知道这是数据流的问题。
而在这里的代码中user 是 Source,而 exportUser 是 Sink。从 User 类型变为 QueryWrapper<User> 类型 CodeQL 默认情况下会认为这是一个新的数据节点,而不是数据流的延续。在 Condition.getQueryWrapper() 方法中,user 对象被封装成 QueryWrapper,这实际上是对数据的一种 重新包装。在这种情况下,CodeQL 认为封装后的数据与原始数据不再相关,因此数据流在这里被截断。所以我们需要拼接user到queryWrapper数据流。
如下就需要进行改进
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodCall ma |
ma.getMethod().getDeclaringType() instanceof TypeObject and
ma.getMethod().getName() = "toString"and
ma.getQualifier() = node1.asExpr() and
ma = node2.asExpr()
) or
// 新增 Condition.getQueryWrapper() 的追踪逻辑
exists(MethodCall mc |
mc.getMethod().getDeclaringType().hasQualifiedName("org.springblade.core.mp.support", "Condition") and
mc.getMethod().getName() = "getQueryWrapper"and
mc.getArgument(0) = node1.asExpr() and
mc = node2.asExpr()
)
}
•新增了对 Condition.getQueryWrapper() 方法的处理。通过 exists 语句,配置了对 Condition.getQueryWrapper(user, User.class) 方法的识别。
•通过匹配 getArgument(0)(即传入的 user 参数)和 mc = node2.asExpr() 来建立 Map 类型的 user 与返回的 QueryWrapper 对象之间的数据流关系。
这样就走找到了我们的。
这样就可以找到对应的sql注入的问题,且解决了这个场景下sql注入的漏报
原文始发于微信公众号(A9 Team):codeql在mybatis某漏报场景下的研究学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论