CodeQL · 真入门
0x01 前言 在自己第一遍学完 CodeQL 之后还是感觉比较生疏,于是想找点靶场练手,于是就有了这篇文章。
0x02 CodeQL 基本语法 QL 语法 用的是这个靶场 —— micro_service_seclab: ,同理其实 JoyChou93 师傅之前所设计的靶场,也是可以用来做 CodeQL 练习的。
1 codeql database create E:\Coding\CodeQL\CodeQL-Practice --language="java" --source-root=E:\Coding\CodeQL\micro_service_seclab --command ="mvn clean package -Dmaven.test.skip=true"
CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。
然后我们需要编写QL查询语句来获取我们想要的数据。
正如这张图描述的,由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则,然后使用CodeQL引擎去跑我们的规则,发现靶场的安全漏洞。
我们来简单地介绍一下本案例涉及到的CodeQL的基本语法。
基本语法包含3个部分。
名称 解释 Method 方法类,Method method 表示获取当前项目中所有的方法 MethodAccess 方法调用类,MethodAccess call 表示获取当前项目当中的所有方法调用 Parameter 参数类,Parameter 表示获取当前项目当中所有的参数
结合 ql 的语法,我们尝试获取 micro-service-seclab
项目当中定义的所有方法:
1 2 3 4 import java from Method method select method
我们再通过 Method 类内置的一些方法,把结果过滤一下。比如我们获取名字为 getStudent
的方法名称。
1 2 3 4 5 import java from Method method where method.hasName("getStudent") select method.getName(), method.getDeclaringType()
1 2 method.getName() method.getDeclaringType() / 获取的是当前方法所属class的名称。
谓词 和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。
这个函数,就叫谓词。
比如上面的案例,我们可以写成如下,获得的结果跟上面是一样的:
1 2 3 4 5 6 7 8 9 import java predicate isStudent(Method method) { exists(|method.hasName("getStudent")) } from Method method where isStudent(method) select method.getName(), method.getDeclaringType()
语法解释
predicate
表示当前方法没有返回值。exists
子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回 true or false
,来决定筛选出哪些数据。
设置 Source 和 Sink 什么是source和sink
在代码自动化安全审计的理论当中,有一个最核心的三元组概念,就是(source,sink和sanitizer)。
source 是指漏洞污染链条的输入点。比如获取http请求的参数部分,就是非常明显的Source。
sink 是指漏洞污染链条的执行点,比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。
sanitizer又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer。
只有当source和sink同时存在,并且从source到sink的链路是通的,才表示当前漏洞是存在的。
在 micro_service_seclab 中,对应的 Source 举个例子,SQL 注入的代码
1 2 3 4 @RequestMapping(value = "/one") public List<Student> one (@RequestParam(value = "username") String username) { return indexLogic.getStudent(username); }
对应 CodeQL 当中的 Source
1 override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
RemoteFlowSource
类(在semmle.code.java.dataflow.FlowSources
)中定义)表示可能由远程用户控制的数据流源,这里这段代码的传参比较简单,但其实传参如果复杂,比如是一个类的情况下,也是类似的
在下面的代码中,source就是Student user
(user为Student类型,这个不受影响)。
1 2 3 4 @PostMapping(value = "/object") public List<Student> objectParam (@RequestBody Student user) { return indexLogic.getStudent(user.getUsername()); }
在本案例中,我们的sink应该为query
方法(Method)的调用(MethodAccess),所以我们设置Sink为:
1 2 3 4 5 6 7 8 override predicate isSink (DataFlow::Node sink) { exists(Method method, MethodAccess call | method.hasName("query" ) and call.getMethod() = method and sink.asExpr() = call.getArgument(0 ) ) }
注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
在靶场系统(micro-service-seclab
)中,sink就是:
1 jdbcTemplate.query(sql, ROW_MAPPER);
因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!
Flow数据流 在设置完 Source 和 Sink 之后,我们需要确认 Source 到 Sink 是能够走通的,这一段的连通工作就是 CodeQL 引擎本身来完成的。我们通过 config.hasFlowPath(source, sink)
方法来判断是否连通。
比如如下代码:
1 2 3 from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
我们传递给 config.hasFlowPath(source, sink)
我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。
0x03 CodeQL 语句优化 初步成果 经过整理之后的 ql 查询代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /** * @id java/examples/vuldemo * @name Sql-Injection * @description Sql-Injection * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.security.QueryInjection import DataFlow::PathGraph class VulConfig extends TaintTracking::Configuration { VulConfig() { this = "SqlInjectionConfig"} override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } } from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
CodeQL 在定义类上的语法和 Java 类似,其中 extends 的父类 TaintTracking::Configuration
是官方提供用来做数据流分析的通用类,提供很多数据流分析相关的方法,比如isSource(定义source),isSink(定义sink)
src instanceof RemoteFlowSource
表示src 必须是 RemoteFlowSource 类型。在RemoteFlowSource里,官方提供很非常全的source定义,我们本次用到的Springboot的Source就已经涵盖了。
注:上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中。
误报解决 扫描结果当中存在一项误报
这个方法的参数类型是 List<Long>
,不可能存在注入漏洞。
这说明我们的规则里,对于 List<Long>
,甚至 List<Integer>
类型都会产生误报,source 误把这种类型的参数涵盖了。
我们需要采取手段消除这种误报。
这个手段就是 isSanitizer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 isSanitizer是CodeQL的类TaintTracking::Configuration提供的净化方法。它的函数原型是: override predicate isSanitizer(DataFlow::Node node) {} 在CodeQL自带的默认规则里,对当前节点是否为基础类型做了判断。 override predicate isSanitizer(DataFlow::Node node) { node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType or node.getType() instanceof NumberType } 表示如果当前节点是上面提到的基础类型,那么此污染链将被净化阻断,漏洞将不存在。
由于 CodeQL 检测SQL注入里的 isSanitizer
方法,只对基础类型做了判断,并没有对这种复合类型做判断,才引起了这次误报问题。
那我们只需要将这种复合类型加入到 isSanitizer
方法,即可消除这种误报。
1 2 3 4 5 6 override predicate isSanitizer(DataFlow::Node node) { node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType or node.getType() instanceof NumberType or exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType ) // 这里的 ParameterizedType 代表所有泛型,判断泛型当中的传参是否为 Number 型 }
以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。
漏报解决 我们发现,如下的SQL注入并没有被CodeQL捕捉到。
1 2 3 4 5 public List<Student> getStudentWithOptional (Optional<String> username) { String sqlWithOptional = "select * from students where username like '%" + username.get() + "%'" ; return jdbcTemplate.query(sqlWithOptional, ROW_MAPPER); }
漏报理论上讲是不能接受的。如果出现误报我们还可以通过人工筛选来解决,但是漏报会导致很多漏洞流经下一个环节到线上,从而产生损失。
那我们如果通过CodeQL来解决漏报问题呢?答案就是通过 isAdditionalTaintStep
方法。
实现原理就一句话:断了就强制给它接上。
1 2 3 4 5 6 isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法,它的原型是: override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {} 它的作用是将一个可控节点 A强制传递给另外一个节点B,那么节点B也就成了可控节点。
这里由于 Optional 这种类型的使用没有在 CodeQL 的语法库里,我们需要强制让 username
流转到username.get()
,这样 username.get()
就变得可控了。这样应该就能识别出这个注入漏洞了。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * @id java/examples/vuldemo * @name Sql-Injection * @description Sql-Injection * @kind path-problem * @problem.severity warning */ // 解决 SQL 注入 QL 语句的漏保 import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.security.QueryInjection import DataFlow::PathGraph predicate isTaintedString(Expr expSrc, Expr expDest) { exists(Method method, MethodAccess call, MethodAccess call1| expSrc = call1.getArgument(0) and expDest = call and call.getMethod() = method and method.hasName("get") and method.getDeclaringType().toString() = "Optional<String>" and call1.getArgument(0).getType().toString() = "Optional<String>" ) } class VulConfig extends TaintTracking::Configuration { VulConfig() { this = "SqlInjectionConfig"} override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) // sink.asExpr() 是一个方法,用于将一个 sink 转换成一个表达式。这个方法通常用于在查询中使用 sink,因为查询需要将 sink 转换成表达式才能进行分析。 ) } override predicate isSanitizer(DataFlow::Node node) { node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType or node.getType() instanceof NumberType or exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType ) } override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { isTaintedString(node1.asExpr(), node2.asExpr()) } } from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
Lombok 插件漏报 Lombok 的注解并不会直接被 CodeQL 所解析,导致其中的中间链路会“中道崩殂”,我们用以下方法来解决。
解决方法 ① 使用 maven-delombok
,在 pom.xml
中添加以下代码,重新编译即可。(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <build > <sourceDirectory > target/generated-sources/delombok</sourceDirectory > <testSourceDirectory > target/generated-test-sources/delombok</testSourceDirectory > <plugins > <plugin > <groupId > org.projectlombok</groupId > <artifactId > lombok-maven-plugin</artifactId > <version > 1.18.20.0</version > <executions > <execution > <id > delombok</id > <phase > generate-sources</phase > <goals > <goal > delombok</goal > </goals > <configuration > <addOutputDirectory > false</addOutputDirectory > <sourceDirectory > src/main/java</sourceDirectory > </configuration > </execution > <execution > <id > test-delombok</id > <phase > generate-test-sources</phase > <goals > <goal > testDelombok</goal > </goals > <configuration > <addOutputDirectory > false</addOutputDirectory > <sourceDirectory > src/test/java</sourceDirectory > </configuration > </execution > </executions > </plugin > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build >
解决办法 ② CodeQL官方的issue里面,有人给出了这个问题的解决办法 https://github.com/github/codeql/issues/4984
1 2 3 4 5 6 7 8 9 10 11 12 wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar" java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok" find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';' find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';' cp -r "delombok/." "./" rm -rf "delombok"
没有特别明白这个应该在哪个目录下执行命令。
上面的代码,实现的功能是:去掉代码里的lombok注解,并还原setter和getter方法的java代码,从而使CodeQL的Flow流能够顺利走下去, 从而检索到安全漏洞。
持续工程化 到此为止,我们编写了SQL注入的查询语句,消除了误报和漏报问题。当前的规则已经能够适应micro-service-seclab项目啦。
因为我们的micro-service-seclab项目,是按照标准生成的微服务结构,那么我们可以使用这个ql规则去跑其他的项目,来自动化检测其它项目,从而做到自动化检测,提高安全检测效率。
CodeQL除了提供VSCode的检测插件,也提供了大量的命令行,来实现项目的集成检测。
比如:
1 codeql database create E:\Coding\CodeQL\CodeQL-Practice\database --language="java" --source-root=E:\Coding\CodeQL\micro_service_seclab --command ="mvn clean package -Dmaven.test.skip=true"
我们通过上面语句自动生成 codeql 的中间数据库(database)
这里是很容易踩坑的,因为前面的语句有个问题就是 select 的返回值并非是 string,所以就会报错,报错信息如下
1 Error was: Expected result pattern(s) are not present for problem query: Expected exactly one pattern.
参考 https://xz.aliyun.com/t/10852#toc-7
0x04 CodeQL 进阶 用 instanceof 替代复杂查询语句问题 我们在上面的案例当中看到了 instanceof
, 如果我们去看ql自带的规则库,会发现大量的 instanceof
语句。
instanceof
这个关键字是用来判断当前的对象,和后面的是否为同一类型。
instanceof
是用来优化代码结构非常好的语法糖。
这种方式的提出其实是用来优化之前使用 exists(|)
匹配对应 Source/Sink,如果我们需要写非常多的 exists(|)
,这会使得整个项目维护起来非常困难,于是就出现了这一种语法糖 instanceof
instanceof
给我们提供了一种机制,我们只需要定义一个 abstract class
,比如这个案例当中的 RemoteFlowSource
1 2 3 4 5 /** A data flow source of remote user input. */ abstract class RemoteFlowSource extends DataFlow::Node { /** Gets a string that describes the type of this remote flow source. */ abstract string getSourceType(); }
然后在 isSource
方法里进行 instanceof
,判断 src 是 RemoteFlowSource 类型就可以了。
1 2 3 override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
学过 java 的人可能会很费解,我们继承了一个 abstract 抽象类,连个实现方法都没有,怎么就能够达到获取各种 source 的目的呢?
CodeQL 和 Java 不太一样,只要我们的子类继承了这个 RemoteFlowSource 类,那么所有子类就会被调用,它所代表的 source 也会被加载(让我想起了超级版 Fastjson,这有没有存在 CodeQL 反制一说
我们在 RemoteFlowSource 定义下面会看到非常多子类,就是这个道理,它们的结果都会被用 and 串联加载。
递归问题 简单来说,有如此一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.l4yn3.microserviceseclab.service;public class StudentService { class innerOne { public innerOne () {} class innerTwo { public innerTwo () {} public String Nihao () { return "Nihao" ; } } public String Hi () { return "hello" ; } } }
我们想要根据 innerTwo
类定位到最外层的 StudentService
类,怎么实现?
按照非递归的写法,我们可以这样做:
1 2 3 4 5 import java from Class classes where classes.getName().toString() = "innerTwo" select classes.getEnclosingType().getEnclosingType() // getEnclosingType 获取作用域
我们通过连续 2 次调用 getEnclosingType()
方法是能够拿到最外层的 StudentService 的。
但是正如我们所说,实际情况是我们并不清楚一共有多少层嵌套,而且多个文件可能每个的嵌套数量都不一样,我们没法用确定的调用次数来解决此问题,这个时候我们就需要使用递归的方式解决。
我们在调用方法后面加 *
(从本身开始调用)或者 +
(从上一级开始调用),来解决此问题。
1 2 3 4 5 import java from Class classes where classes.getName().toString() = "innerTwo" select classes.getEnclosingType+() // getEnclosingType 获取作用域
我们也可以自己封装方法来递归调用。
1 2 3 4 5 6 7 8 9 import java RefType demo(Class classes) { result = classes.getEnclosingType() } from Class classes where classes.getName().toString() = "innerTwo" select demo*(classes)
强制类型转换问题(过滤需要类型) 强制类型转换这个名字有点拗口,且不太好理解,这里我更愿意把它理解成一种 filter
在 CodeQL 的规则集里,我们会看到很多类型转换的代码,比如:
我们用如下 QL 语句做个测试:
1 2 3 4 import java from Parameter param select param, param.getType()
以上代码的含义是打印所有方法参数的名称和类型。
如果其中我们想要单独某个类型的方法参数,这里就需要用到强制类型转换,或者说用到 filter
使用 RefType()
来测试
1 2 3 4 import java from Parameter param select param, param.getType().(RefType)
强制转换成 RefType
,意思就是从前面的结果当中过滤出 RefType
类型的参数。
RefType
是什么?引用类型,说白了就是去掉int等基础类型之后的数据。
同理这里肯定并不限于 RefType,也可以是其他的。比如这里保留所有的数值类型参数
1 2 3 4 import java from Parameter param select param, param.getType().(IntegralType)
0x05 关于其他漏洞点的 CodeQL 语句尝试 Fastjson 依样画葫芦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /** * @id java/examples/vuldemo * @name fastjson-vul * @description fastjson-vul * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.security.QueryInjection import DataFlow::PathGraph class FastjsonVulConfig extends TaintTracking::Configuration { FastjsonVulConfig() { this = "fastjson" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call| method.hasName("parseObject") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } } from FastjsonVulConfig fastjsonVul, DataFlow::PathNode source, DataFlow::PathNode sink where fastjsonVul.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
命令执行 写了我一会儿,经过查阅资料发现有直接现成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /** * @id java/examples/vuldemo * @name processBuilder-vul * @description processBuilder-vul * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph class RceVulConfig extends TaintTracking::Configuration { RceVulConfig() { this = "RceVulConfig" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof ArgumentToExec } } from RceVulConfig rceVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink where rceVulConfig.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
SSRF(重点关注) 这里的内容主要是参考于这篇文章 https://forum.butian.net/share/2117
还是觉得关于 SSRF 的 ql 规则这块儿,应该再记录一下,其实在之前看命令执行的 sink 的时候就没追踪到,但是那个时候并没有深入去看。
最开始我的 ql 语句是这样的(很嫩的 ql 语句)
1 2 3 4 5 6 7 8 9 10 11 12 override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call| method.hasName("openConnection") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) }
追踪 url.openConnection()
,但这很明显是追踪不到的,因为 url.openConnection()
是不传参的。那么这一条链路用图来表示的话,断在了这里
那么中间断的地方我们要想办法接上,这就回到了前文提到过的 isAdditionalTaintStep
方法。从应用角度来说代码应该如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /** * @id java/examples/vuldemo * @name processBuilder-vul * @description processBuilder-vul * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.security.QueryInjection import DataFlow::PathGraph import semmle.code.java.security.RequestForgeryConfig class SSRFVulConfig extends TaintTracking::Configuration { SSRFVulConfig() { this = "SSRFVulConfig" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgerySink } } from SSRFVulConfig ssrfVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink where ssrfVulConfig.hasFlowPath(source, sink) select source.getNode(), source, sink, "source"
运行结果
此处 import 了一个新的文件 semmle.code.java.security.RequestForgeryConfig
,这里匹配了对应的规则,和之前的命令执行接口是一样的。可以深入看一下对应的实现。
isSource 1 2 3 4 5 6 7 override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource and // Exclude results of remote HTTP requests: fetching something else based on that result // is no worse than following a redirect returned by the remote server, and typically // we're requesting a resource via https which we trust to only send us to safe URLs. not source.asExpr().(MethodAccess).getCallee() instanceof UrlConnectionGetInputStreamMethod }
RequestForgeryConfig.qll
规则中的 Source 匹配 RemoteFlowSource
,且限定了 java.net.URLConnection.getInputStream()
的输入不为漏洞。
这里的原因是此处的 getInputStream()
的输入不一定是可控的。
isSink 1 override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgerySink }
跟进 RequestForgerySink
1 2 3 4 5 6 7 8 9 10 /** A data flow sink for server-side request forgery (SSRF) vulnerabilities. */ abstract class RequestForgerySink extends DataFlow::Node { } private class UrlOpenSinkAsRequestForgerySink extends RequestForgerySink { UrlOpenSinkAsRequestForgerySink() { sinkNode(this, "open-url") } } private class JdbcUrlSinkAsRequestForgerySink extends RequestForgerySink { JdbcUrlSinkAsRequestForgerySink() { sinkNode(this, "jdbc-url") } }
两个类都在构造函数里面调用了 sinkNode()
,跟进去,对应的文件是 java.dataflow.ExternalFlow.qll
。 关注到它的注释内容大致意思是在说
仅供内部使用。这是一个实验API 提供用于处理指定的流模型的类和谓词 数据扩展和CSV格式
那么这里的内容,一定是从某个 CSV 文件里面去读取的,这就被定义为 ModelCsv
我们可以简化代码定义 sink、source、flow step,并通过kind
来使用它。
1 2 3 4 5 6 7 8 9 10 11 Source: (SourceModelCsv) package; type; subtypes; name; signature; ext; output; kind; provenance Sink: (SinkModelCsv) package; type; subtypes; name; signature; ext; input; kind; provenance Summaries: (SummaryModelCsv) package; type; subtypes; name; signature; ext; input; output; kind; provenance Neutrals: package; type; name; signature; provenan
每一个参数都代表一个含义,整体来说如下
package:包名 type:选择包中的某个类型 subtypes:布尔类型,指示是否跳转到子类 name:方法名 signature:签名 ext:类似于附加类 input:输入的位置 kind:当前 sink 的类型 provenance:来源验证
这么看的话比较抽象,下文会详细讲解 SSRF 漏洞中所对应的规则。
目前 CodeQL 官方还并未发布 SinkModelCsv 的一些官方规则,原因是此功能尚不稳定。使用需要开发者/安全人员自己定义类,此类需继承 xxxModelCsv
即可应用。
sinkModelCsv 谓词数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private predicate sinkModelCsv(string row) { row = [ // Open URL "java.net;URL;false;openConnection;;;Argument[-1];open-url", "java.net;URL;false;openStream;;;Argument[-1];open-url", "java.net.http;HttpRequest;false;newBuilder;;;Argument[0];open-url", "java.net.http;HttpRequest$Builder;false;uri;;;Argument[0];open-url", "java.net;URLClassLoader;false;URLClassLoader;(URL[]);;Argument[0];open-url", "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader);;Argument[0];open-url", "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader,URLStreamHandlerFactory);;Argument[0];open-url", "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader);;Argument[1];open-url", "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader,URLStreamHandlerFactory);;Argument[1];open-url", "java.net;URLClassLoader;false;newInstance;;;Argument[0];open-url", // Bean validation "javax.validation;ConstraintValidatorContext;true;buildConstraintViolationWithTemplate;;;Argument[0];bean-validation", // Set hostname "javax.net.ssl;HttpsURLConnection;true;setDefaultHostnameVerifier;;;Argument[0];set-hostname-verifier", "javax.net.ssl;HttpsURLConnection;true;setHostnameVerifier;;;Argument[0];set-hostname-verifier" ] }
如果直接跑 sinkNode()
代码,结果如下
也就是通过上述的 sinkModuleCsv
,其匹配所有 open-url 类型的数据类型。
但是在新版本的 CodeQL 当中是找不到这一个规则的,在搜索了一堆资料后发现在新版本中是这样的。
isAdditionalTaintStep 看一下它的规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) { any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ) } // 跟进 RequestForgeryAdditionalTaintStep class RequestForgeryAdditionalTaintStep extends Unit { /** * Holds if the step from `pred` to `succ` should be considered a taint * step for server-side request forgery. */ abstract predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ); } private class DefaultRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep { override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) { // propagate to a URI when its host is assigned to exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) or // propagate to a URL when its host is assigned to exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) } } private class TypePropertiesRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep { override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) { exists(MethodAccess ma | // Properties props = new Properties(); // props.setProperty("jdbcUrl", tainted); // Propagate tainted value to the qualifier `props` ma.getMethod() instanceof PropertiesSetPropertyMethod and ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "jdbcUrl" and pred.asExpr() = ma.getArgument(1) and succ.asExpr() = ma.getQualifier() ) } }
看注释是比较明确的,也就是通过 isAdditionalFlowStep()
,将 pred
和 succ
两个点连起来,这里的连接方式是通过污点传递来实现的。具体的匹配方式很容易理解,跟进一下 UriCreation
和 UrlConstructorCall
即可