参考 CodeQL入门
CodeQL入坟篇
New dataflow API for writing custom CodeQL queries
CodeQL踩坑日记and规则分享
前言 下面为笔者初学codeql所记的笔记,涉及到的所有ql代码都是用于查询靶场中对应漏洞的
CodeQL 基本语法 QL 语法 用的是这个靶场 —— micro_service_seclab: ,同理其实 JoyChou93 师傅之前所设计的靶场,也是可以用来做 CodeQL 练习的。
1 codeql database create E:\mycode\codeql\CodeQL-Practice --language="java" --source-root=E:\safety\CodeAudit\micro_service_seclab-main --command ="mvn clean package -Dmaven.test.skip=true"
--command="mvn clean package -Dmaven.test.skip=true"
告诉 CodeQL 如何编译源码
mvn clean package
:用 Maven 清理并打包项目。
-Dmaven.test.skip=true
:跳过测试代码的编译和执行(节省时间,避免测试失败阻碍)。
CodeQL 在构建数据库时,需要项目能成功编译,这样它才能捕获所有类、方法、调用关系等语义信息。 这里它会调用 Maven 编译过程来生成字节码并收集编译信息。
CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。
然后我们需要编写QL查询语句来获取我们想要的数据。
正如这张图描述的,由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则,然后使用CodeQL引擎去跑我们的规则,发现靶场的安全漏洞。
我们来简单地介绍一下本案例涉及到的CodeQL的基本语法。
基本语法包含3个部分。
名称
解释
Method
方法类,Method method 表示获取当前项目中所有的方法
MethodCall
方法调用类,MethodCall call 表示获取当前项目当中的所有方法调用
Parameter
参数类,Parameter 表示获取当前项目当中所有的参数
结合 ql 的语法,我们尝试获取 micro-service-seclab
项目当中定义的所有方法:
1 2 3 4 import java from Method methodselect 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
这里这段代码的传参比较简单,但其实传参如果复杂,比如是一个类的情况下,也是类似的
在下面的代码中,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, MethodCall 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 4 5 module VulFlow = TaintTracking::Global<VulConfig>; from VulFlow::PathNode source, VulFlow::PathNode sink where VulFlow::flowPath(source, sink) select source, sink, "发现潜在的SQL注入漏洞"
我们传递给 VulFlow::flowPath(source, sink)
我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。
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.DataFlow import semmle.code.java.dataflow.TaintTracking import semmle.code.java.dataflow.FlowSources /** 定义数据流配置 */ module VulConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } } /** 用全局 taint tracking 建立路径 */ module VulFlow = TaintTracking::Global<VulConfig>; from VulFlow::PathNode source, VulFlow::PathNode sink where VulFlow::flowPath(source, sink) select source, sink, "发现潜在的SQL注入漏洞"
VulConfig
:自定义一个污点追踪配置。
extends TaintTracking::Configuration
:继承 CodeQL 提供的污点分析框架。
构造函数里 this = "SqlInjectionConfig"
:给这个配置起个名字。
误报解决 结果里面存在误报
这个方法的参数类型是List<Long>
,不可能存在注入漏洞。
这说明我们的规则里,对于List<Long>
,甚至List<Integer>
类型都会产生误报,source误把这种类型的参数涵盖了。
我们需要采取手段消除这种误报。
这个手段就是isSanitizer
。
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方法,即可消除这种误报。
在新版本里面isSanitizer方法已经全部统一到isBarrier方法下了
1 2 3 4 5 6 7 8 9 predicate isBarrier(DataFlow::Node node) { node.getType() instanceof PrimitiveType or // 基本类型(int, char等) node.getType() instanceof BoxedType or // 包装类型(Integer, Boolean等) node.getType() instanceof NumberType or // 数字类型 exists(ParameterizedType pt| // 泛型类型 node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType // 泛型参数为数字类型 ) }
以上代码的意思为:如果当前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() + "%'"; //String sql = "select * from students where username like ?"; 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()
就变得可控了。这样应该就能识别出这个注入漏洞了
在新版的ql中使用 isAdditionalFlowStep
而不是污点跟踪谓词 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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 /** * @id java/examples/demo * @name Sql-Injection * @description Sql-Injection * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.TaintTracking import semmle.code.java.dataflow.FlowSources predicate isTaintedString(Expr expSrc, Expr expDest) { exists(Method method, MethodCall call, MethodCall 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>" ) } /** 定义数据流配置 */ module VulConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } predicate isBarrier(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 型 } predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { isTaintedString(node1.asExpr(), node2.asExpr()) } } /** 用全局 taint tracking 建立路径 */ module VulFlow = TaintTracking::Global<VulConfig>; import VulFlow::PathGraph from VulFlow::PathNode source, VulFlow::PathNode sink where VulFlow::flowPath(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 # get a copy of lombok.jar wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar" # run "delombok" on the source files and write the generated files to a folder named "delombok" java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok" # remove "generated by" comments find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';' # remove any left-over import statements find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';' # copy delombok'd files over the original ones cp -r "delombok/." "./" # remove the "delombok" folder rm -rf "delombok"
没有特别明白这个应该在哪个目录下执行命令。
上面的代码,实现的功能是:去掉代码里的lombok注解,并还原setter和getter方法的java代码,从而使CodeQL的Flow流能够顺利走下去, 从而检索到安全漏洞。
持续工程化 到此为止,我们编写了SQL注入的查询语句,消除了误报和漏报问题。当前的规则已经能够适应micro-service-seclab项目啦。
因为我们的micro-service-seclab项目,是按照标准生成的微服务结构,那么我们可以使用这个ql规则去跑其他的项目,来自动化检测其它项目,从而做到自动化检测,提高安全检测效率。
CodeQL除了提供VSCode的检测插件,也提供了大量的命令行,来实现项目的集成检测。
比如:
1 codeql database create ~/CodeQL/databases/micro-service-seclab --language="java" --command="mvn clean install --file pom.xml -Dmaven.test.skip=true" --source-root="~/Code/micro-service-seclab/"
我们通过上面语句自动生成codeql的中间数据库(database)。
1 codeql database analyze /CodeQL/databases/micro-service-seclab /CodeQL/ql/java/ql/examples/demo --format=csv --output=/CodeQL/Result/micro-service-seclab.csv --rerun
我们通过上面的语句可以执行我们写好的QL文件,然后将结果输出到指定csv文件。
利用这两条命令,结合我们自己的程序,我们就能批量的对我们所有的项目做自动化检测了。
CodeQL进阶 上面我们完成了对一个简单的SQL注入漏洞的自动化检测工作。
如果你对上面的语法的一些东西还是有些不解,或者想去阅读SDK规则的代码,可以继续往下看,希望我对一些重点语法的总结 能够帮到你。
用 instanceof 替代复杂查询语句问题 我们在上面的案例当中看到了instanceof, 如果我们去看ql自带的规则库,会发现大量的instanceof语句。
instanceof是用来优化代码结构非常好的语法糖。
我们都知道,我们可以使用exists(|)这种子查询的方式定义source和sink,但是如果source/sink特别复杂(比如我们为了规则通用,可能要适配springboot, Thrift RPC,Servlet等source),如果我们把这些都在一个子查询内完成,比如 condition 1 or conditon 2 or condition 3, 这样一直下去,我们可能后面都看不懂了,更别说可维护性了。 况且有些情况如果一个子查询无法完成,那么就更没法写了。
instanceof给我们提供了一种机制,我们只需要定义一个abstract class,比如这个案例当中的:
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也会被加载。
我们在 RemoteFlowSource定义下面会看到非常多子类,就是这个道理,它们的结果都会被用and串联加载。
递归问题 递归调用可以帮助我们解决一类问题:就是我们不确定这个方法我们需要调用多少次才能得到我们的结果,这个时候我们就可以用递归调用。
CodeQL里面的递归调用语法是:在谓词方法的后面跟*或者+,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。 我们举一个例子:
在Java语言里,我们可以使用class嵌套class,多个内嵌class的时候,我们需要知道最外层的class是什么怎么办? 比如如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 from Class classes where classes.getName().toString() = "innerTwo" select classes.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) // 获取作用域
强制类型转换问题 在CodeQL的规则集里,我们会看到很多类型转换的代码,比如:
这里是对getType()的返回结果做强制类型转换。其实CodeQL当中的强制类型转换,理解成filter更贴切一点,它的意思是将前面的结果符合RefType的数据都留下,不符合的都去掉。
以上class 继承了Parameter,那么getType()目的就是获取项目中所有的参数的type信息。
我们用如下QL语句做个测试:
1 2 3 4 import java from Parameter param select param, param.getType()
以上代码的含义是打印所有方法参数的名称和类型。
我们看到一共有233条结果,并且结果当中含有String,int和其他自定义类型,这是我们没有做任何强制类型转换的结果。 然后我们试着执行:
1 2 3 4 import java from Parameter param select param, param.getType().(RefType)
强制转换成RefType,意思就是从前面的结果当中过滤出RefType类型的参数。RefType是什么?引用类型,说白了就是去掉int等基础类型之后的数据。
数据只有181条了。
更直观的测试,我们可以过滤保留所有的数值类型。
1 2 3 4 import java from Parameter param select param, param.getType().(IntegralType)
内存爆炸解决办法 如果你现在是 **新版本 CodeQL CLI (>=2.9.x)**:
数据库创建时:
1 2 export SEMMLE_JAVA_EXTRACTOR_JVM_ARGS="-Xmx4g" codeql database create db --language=java -s . -M 4000
查询时用:
1 codeql database analyze db query.ql --ram=4000
如果你还是老版本:
数据库创建时只能用:
1 codeql database create db --language=java -s . -J=-Xmx4g
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 34 35 /** * @id java/examples/fastjson * @name Fastjson * @description fastjson * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.TaintTracking import semmle.code.java.dataflow.FlowSources module FastjsonVulconfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("parseObject") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } } module fastjsonFlow = TaintTracking::Global<FastjsonVulconfig>; import fastjsonFlow::PathGraph from fastjsonFlow::PathNode source, fastjsonFlow::PathNode sink where fastjsonFlow::flowPath(source, sink) select source.getNode(), source, sink, "source"
Rce 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 /** * @id java/examples/rce * @name Rce * @description rce * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.TaintTracking import semmle.code.java.dataflow.FlowSources module RceVulconfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof ArgumentToExec //ArgumentToExec表示传递给命令执行的参数 } } module rceFlow = TaintTracking::Global<RceVulconfig>; import rceFlow::PathGraph from rceFlow::PathNode source, rceFlow::PathNode sink where rceFlow::flowPath(source, sink) select source.getNode(), source, sink, "source"
XXE 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/xxe * @name xxe * @description xxe-vul * @kind path-problem * @problem.severity warning */ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.ExternalFlow import DataFlow::PathGraph class XXEVulConfig extends TaintTracking::Configuration { XXEVulConfig(){ this = "XXEVulConfig" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call| method.hasName("parse") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } } from XXEVulConfig xxeVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink where xxeVulConfig.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 /** * @kind path-problem */ import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources module MyConfiguration implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(MethodCall methodCall | methodCall.getMethod().getName() = "methodName" and methodCall.getQualifier().getType().getName() = "ClassName" and sink.asExpr() = methodCall.getAnArgument() ) } } module Flow = DataFlow::Global<MyConfiguration>; import Flow::PathGraph from Flow::PathNode source, Flow::PathNode sink where Flow::flowPath(source, sink) select sink.getNode(), source, sink,"xxxInfo RCE"
Shiro 秘钥硬编码查询(支持新版语法) 语法已废弃,新版中默认已支持该规则。
shiro在早期版本中的用来序列化反序列化的key可以被定义,查询系统中是否存在默认密钥定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph //查询是否存在 Shiro 默认秘钥 class ShiroDefaultKey extends TaintTracking::Configuration { ShiroDefaultKey() { this = "ShiroDefaultKey" } override predicate isSource(DataFlow::Node src) { src.asExpr() instanceof StringLiteral } override predicate isSink(DataFlow::Node src) { exists(MethodCall methodCall | methodCall.getMethod().getName() = "setCipherKey" and methodCall.getQualifier().getType().getName() = "CookieRememberMeManager" and src.asExpr() = methodCall.getAnArgument() ) } } from DataFlow::PathNode source, DataFlow::PathNode sink, ShiroDefaultKey conf, MethodCall methodCall where conf.hasFlowPath(source, sink) and sink.getNode().asExpr() = methodCall.getAnArgument() // select source, sink, methodCall, methodCall.getEnclosingCallable() select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(), sink.toString(),sink.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "Shiro Default Key"
脚本更新为新版支持语法,减少waring报错。
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 /** * @name Apache Shiro Default Key * @description Apache Shiro Default Key * @kind path-problem * @problem.severity error * @security-severity 8.8 * @precision high * @id java/rce-shiro-default-key * @tags devrules */ import java import Flow::PathGraph import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking module RCEConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source = source } predicate isSink(DataFlow::Node sink) { exists(MethodCall methodCall, Class c | methodCall.getMethod().getName() = "setCipherKey" and methodCall.getQualifier().getType() = c and c.hasQualifiedName("org.apache.shiro.web.mgt", "CookieRememberMeManager") and sink.asExpr() = methodCall.getAnArgument() ) } } module Flow = DataFlow::Global<RCEConfig>; from Flow::PathNode source, Flow::PathNode sink where Flow::flowPath(source, sink) select sink.getNode(), source, sink, "Shiro Default Key"
ScriptEngine RCE规则查询(支持新版语法) Java默认的JavaScript解析器,可以解析Java代码造成任意代码执行。
漏洞代码:
1 2 3 4 ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension("js"); String command = "new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()"; scriptEngine.eval(command);
使用新版规则进行查询,减少控制台Warning警告。
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 /** * @name Script Engine RCE * @description Script Engine RCE * @kind path-problem * @problem.severity error * @security-severity 8.8 * @precision high * @id java/rce-script-engine * @tags devrules */ import java import ScriptEngineFlow::PathGraph import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources module ScriptEngineConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(MethodCall methodCall, Class c | methodCall.getMethod().getName() = "eval" and methodCall.getQualifier().getType().getName() = "ScriptEngine" and sink.asExpr() = methodCall.getAnArgument() ) } } module ScriptEngineFlow = DataFlow::Global<ScriptEngineConfig>; from ScriptEngineFlow::PathNode source, ScriptEngineFlow::PathNode sink where ScriptEngineFlow::flowPath(source, sink) select sink.getNode(), source, sink, "Script Engine RCE"
Apache Commons Text 远程代码执行漏洞(支持新版语法) 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 /** * @name Apache Commons Text Rce * @description Apache Commons Text Rce * @kind path-problem * @problem.severity error * @security-severity 8.8 * @precision high * @id java/rce-apache-commons-text * @tags devrules */ import java import CommonsTextRCEFlow::PathGraph import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking module CommonsTextRCEConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(MethodCall methodCall , Class c| methodCall.getMethod().getName().substring(0, 7) = "replace" and methodCall.getQualifier().getType() = c and c.hasQualifiedName("org.apache.commons.text", "StringSubstitutor") and sink.asExpr() = methodCall.getAnArgument() ) } } module CommonsTextRCEFlow = DataFlow::Global<CommonsTextRCEConfig>; from CommonsTextRCEFlow::PathNode source, CommonsTextRCEFlow::PathNode sink where CommonsTextRCEFlow::flowPath(source, sink) select sink.getNode(), source, sink, "Apache Commons Text Rce"
SpEL表达式注入漏洞(支持新版语法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources module SpELConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { exists(MethodCall methodCall | methodCall.getMethod().getName() = "parseExpression" and methodCall.getQualifier().getType().getName() = "SpelExpressionParser" and sink.asExpr() = methodCall.getAnArgument() ) } } module SpELFlow = DataFlow::Global<SpELConfig>; from DataFlow::Node source, DataFlow::Node sink where SpELFlow::flow(source, sink) select source,sink
小技巧 有时候获取到的可能并非源码,而是jar包,但是jar包反编译后的源码再次重编译会因为种种原因编译错误,在一次和一位高手沟通的过程中我了解到也可以不通过编译来构建codeql数据库
模拟大部分情况下获取jar包的情况,先打包后反编译
1 java -jar java-decompiler.jar D:\Downloads\micro_service_seclab\target\micro-service-seclab-0.0.1-SNAPSHOT.jar D:\Downloads\micro_service_seclab\decompile
反编译后得到一个jar包,不过解压后会发现里面的文件不再是class而是java
创建数据库
1 codeql database create D:\Downloads\micro_service_seclab\database --language java -s D:\Downloads\micro_service_seclab\decompile\micro-service-seclab-0.0.1-SNAPSHOT --overwrite --build-mode none
先记一下,还没试过
像你说的 Hessian 反序列化漏洞 这类场景,漏洞点往往存在于 业务代码调用外部依赖库(Hessian 框架) 的过程中,所以数据库必须包含:
项目自身的源码
项目依赖的三方库(jar 包)
否则 CodeQL 的数据流图不完整,就没法追踪到 HessianInput.readObject()
之类的危险调用。
正确的数据库创建流程(Java 项目) 1. 用构建工具生成数据库
Maven 项目 :
1 2 3 codeql database create mydb \ --language=java \ --command="mvn clean compile -DskipTests"
Gradle 项目 :
1 2 3 codeql database create mydb \ --language=java \ --command="./gradlew build -x test"
原理: CodeQL 的 Java extractor 会拦截 javac
,在编译过程中把源码和依赖关系收集到数据库里。 这样不仅有项目源码,还会自动把 依赖 jar 包里的类签名 收录进来(用于调用关系)。
2. 如果项目没有标准构建命令 有些项目是手工编译或者没用 Maven/Gradle。 这时你需要显式告诉 CodeQL 源码 + classpath :
1 2 3 4 codeql database create mydb \ --language=java \ --source-root . \ --command="javac -cp 'lib/*' -d build/classes $(find src -name '*.java')"
lib/*
:依赖 jar 包目录(例如包含 hessian.jar)
src
:源码目录
这样 extractor 在分析时就能看见 Hessian 的 API 并构建调用图。
3. 确认数据库里包含依赖 创建完成后,你可以检查数据库里是否包含依赖:
1 codeql database interpret-results mydb
或者在 .db
目录下找到 dbscheme
,确认是否解析了 Hessian 的类。