网上关于codeql的文章并不多,国内现在对codeql的研究相对比较少,可能是因为codeql暂时没有中文文档,资料也相对较少,需要比较好的英语功底,但是我认为在随着代码量越来越多,传统的自动化漏洞挖掘工具的瓶颈无法突破的情况下,codeql相当于是一种折中的办法,通过codeql的辅助,来减少漏洞挖掘人员的工作,更加关注漏洞的发现和利用过程
之所以选ofcms,是因为有p0desta师傅之前的审计经验,而且使用codeql审计cms尚属第一次,所以选用了ofcms审计
在ql中,漏洞挖掘是根据污点追踪进行的,所以我们需要知道我们的挖掘的cms的source点在哪里,sink点在哪里,相对来说,source点比较固定,一般就是http的请求参数,请求头这一类的
但是sink比较难以确定,由于现在的web应用经常使用框架,有些文件读取,html输出其实是背后的框架在做,所以这就导致了我们的sink定义不可能是一成不变的,要对整个web应用有一个大致的了解,才能定义对应的sink
source点很清楚,对于一个web应用来说,http请求参数,http请求头,我们关注ofcms中对请求参数的获取方式:
ofcms使用了jfinal这个框架,而ofcms继承了jfinal的controller来获取参数,在整个ofcms中大体有三种类型来获取请求参数:
所以我们的source都是根据这几个类展开的,在观察这几个类之后很容易发现,所有的获取http参数的方法都是getXXX()这样的命名方式,所以我们可以这样定义source的ql语法:
class OfCmsSource extends MethodAccess{ OfCmsSource(){ (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.admin.controller", "BaseController") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("com.jfinal.core", "Controller") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("javax.servlet.http", "HttpServletRequest") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.api", "ApiBase") and (this.getMethod().getName().substring(0, 3) = "get")) } }
到这一步,我们的source就算定义完了,接下来就是定义对应的sink了
相对于source的固定,sink就很不固定了,常见的web漏洞一般来说都可以作为sink,而且因为框架的不同,同一种漏洞在不同框架下的ql都是不一样的,所以我们需要略微分析一下整个web应用在做文件读取,模版渲染等操作的时候一般都用的是什么方法
Jfinal中对模版渲染有一系列的render方法:
可以看到,所有都是render开头,所以我们对方法名的判断很简单,截取前面6个字符,判断是否为render,随便找一个项目使用render的地方,可以发现render其实是在com.jfinal.core.Controller里面定义的方法,所以现在我们唯一确定了模版渲染的方法,所以我们的sink也就呼之欲出了,也就是这些render方法的参数,所以构造ql:
class RenderMethod extends MethodAccess{ RenderMethod(){ (this.getMethod().getDeclaringType*().hasQualifiedName("com.jfinal.core", "Controller") and this.getMethod().getName().substring(0, 6) = "render") or (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.core.plugin.freemarker", "TempleteUtile") and this.getMethod().hasName("process")) } }
在上面的ql中我添加了TempleteUtile这个类,因为这个类的process第一个参数可控的话也会造成模版的问题,所以我们可以随时去到ql中添加我们认为可能出现问题的模版渲染方法
在ofcms中,文件的创建一般都是new File()这种形式创建的,所以我们的sink点应该为new File的参数为我们的sink点,所以构造ql:
class FileContruct extends ClassInstanceExpr{ FileContruct(){ this.getConstructor().getDeclaringType*().hasQualifiedName("java.io", "File") } }
codeql提供了几种数据流的查询:
local data flow基本是用在一个方法中的,比如想要知道一个方法的入参是否可以进入到某一个方法,就可以用local data flow
global data flow是用在整个项目的,也是我们做污点追踪用的最多的
简单解释一下taint和非taint有什么区别:taint的dataflow会在数据流分析的基础上加上污点分析,比如
String a = "evil"; String b = a + a;
在使用taint的dataflow中,b也会被标记为被污染的变量
class OfCmsTaint extends TaintTracking::Configuration{ OfCmsTaint(){ this = "OfCmsTaint" } override predicate isSource(DataFlow::Node source){ source.asExpr() instanceof OfCmsSource } override predicate isSink(DataFlow::Node sink){ exists( FileContruct rawOutput | sink.asExpr() = rawOutput.getAnArgument() ) } }
当我们需要去做污点分析的时候,我们需要继承TaintTracking::Configuration这个类,来重写两个方法isSource和isSink,在这里,dataflow中的Node节点和我们直接使用的节点是不一样的,我们需要使用asExpr或者asParamter来将其转换为语法节点
这里可以看到,我们的source为我们之前定义的http参数的输入地方,sink为我们之前定义的new File的这种实例化
codeql只能给出从source到sink的一条路径,但是这条路径中的一些过滤和条件是无法被判断的,这也就需要一部分的人工成本,让我们来运行一下我们刚刚写的ql:
import ofcms from DataFlow::Node source, DataFlow::Node sink, OfCmsTaint config where config.hasFlow(source, sink) select source, sink
最后的查询结果:
可以看到找到了11个可能存在问题的地方,我们来依次看一看是否有问题:
第一个在ReprotAction这个类的expReport方法中:
可以很明显看到,在获取j参数之后,对jrxmlFileName没有任何的校验,导致我们可以穿越到其他目录,但是文件后缀名必须为jrxml,而且在JasperCompileManager的compileReport函数中,对xml文档没有限制实体,导致可以造成XXE漏洞,这里很尴尬的利用点是:
在TemplateController这个类的getTemplates方法中:
在这里对获取的参数没有任何的校验,导致可以跨越目录列文件并且修改文件,但是在后面的实现中,我们只能修改和查看特定的文件
假设我们在tmp目录下有着a.html和a.xml文件,我们可以跨越到tmp目录下读取并修改这两个文件
还有一个地方就是save函数,这个函数在p0desta师傅的博客中也挖掘出了任意文件上传漏洞:
很明显的一任意文件上传,文件名,路径,文件内容全部可控,直接getshell
剩下的一个并不能造成影响,就不多说了
在render的sink定义中,如果运行可以发现很多地方的前台的一个小问题,也就是我们可以指定模版文件,ofcms使用了freemarker模版引擎,如果可以包含到我们自定义的模版文件,即可导致RCE,但是并没有发现有一个文件上传的点可以上传文件到模版目录下(除了上面的一个任意文件上传),所以不太好前台RCE
顺手测了下发现前台评论地方有存储XSS,但是和codeql无关就不多说了
整个ql:
ofcms.qll
import java import semmle.code.java.dataflow.TaintTracking class OfCmsSource extends MethodAccess{ OfCmsSource(){ (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.admin.controller", "BaseController") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("com.jfinal.core", "Controller") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("javax.servlet.http", "HttpServletRequest") and (this.getMethod().getName().substring(0, 3) = "get")) or (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.api", "ApiBase") and (this.getMethod().getName().substring(0, 3) = "get")) } } class RenderMethod extends MethodAccess{ RenderMethod(){ (this.getMethod().getDeclaringType*().hasQualifiedName("com.jfinal.core", "Controller") and this.getMethod().getName().substring(0, 6) = "render") or (this.getMethod().getDeclaringType*().hasQualifiedName("com.ofsoft.cms.core.plugin.freemarker", "TempleteUtile") and this.getMethod().hasName("process")) } } class SqlMethod extends MethodAccess{ SqlMethod(){ this.getMethod().getDeclaringType*().hasQualifiedName("com.jfinal.plugin.activerecord", "Db") } } class FileContruct extends ClassInstanceExpr{ FileContruct(){ this.getConstructor().getDeclaringType*().hasQualifiedName("java.io", "File") } } class ServletOutput extends MethodAccess{ ServletOutput(){ this.getMethod().getDeclaringType*().hasQualifiedName("java.io", "PrintWriter") } } class OfCmsTaint extends TaintTracking::Configuration{ OfCmsTaint(){ this = "OfCmsTaint" } override predicate isSource(DataFlow::Node source){ source.asExpr() instanceof OfCmsSource } override predicate isSink(DataFlow::Node sink){ exists( FileContruct rawOutput | sink.asExpr() = rawOutput.getAnArgument() ) } }
test.ql
import ofcms from DataFlow::Node source, DataFlow::Node sink, OfCmsTaint config where config.hasFlow(source, sink) select source, sink
太菜了,有个点的任意文件读取写不出来ql,2333
师傅们教教我