前言
网上关于codeql的文章并不多,国内现在对codeql的研究相对比较少,可能是因为codeql暂时没有中文文档,资料也相对较少,需要比较好的英语功底,但是我认为在随着代码量越来越多,传统的自动化漏洞挖掘工具的瓶颈无法突破的情况下,codeql相当于是一种折中的办法,通过codeql的辅助,来减少漏洞挖掘人员的工作,更加关注漏洞的发现和利用过程
之所以选ofcms,是因为有p0desta师傅之前的审计经验,而且使用codeql审计cms尚属第一次,所以选用了ofcms审计
ql构造
在ql中,漏洞挖掘是根据污点追踪进行的,所以我们需要知道我们的挖掘的cms的source点在哪里,sink点在哪里,相对来说,source点比较固定,一般就是http的请求参数,请求头这一类的
但是sink比较难以确定,由于现在的web应用经常使用框架,有些文件读取,html输出其实是背后的框架在做,所以这就导致了我们的sink定义不可能是一成不变的,要对整个web应用有一个大致的了解,才能定义对应的sink
source点的ql
source点很清楚,对于一个web应用来说,http请求参数,http请求头,我们关注ofcms中对请求参数的获取方式:
ofcms使用了jfinal这个框架,而ofcms继承了jfinal的controller来获取参数,在整个ofcms中大体有三种类型来获取请求参数:
- BaseController
- Controller(Jfinal提供)
- ApiBase
所以我们的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了
sink点的ql
相对于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 taint data flow
- global data flow
- global taint data flow
local data flow基本是用在一个方法中的,比如想要知道一个方法的入参是否可以进入到某一个方法,就可以用local data flow
global data flow是用在整个项目的,也是我们做污点追踪用的最多的
简单解释一下taint和非taint有什么区别:taint的dataflow会在数据流分析的基础上加上污点分析,比如
String a = "evil";
String b = a + a;
在使用taint的dataflow中,b也会被标记为被污染的变量
构造configure
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
第一个在ReprotAction这个类的expReport方法中:
可以很明显看到,在获取j参数之后,对jrxmlFileName没有任何的校验,导致我们可以穿越到其他目录,但是文件后缀名必须为jrxml,而且在JasperCompileManager的compileReport函数中,对xml文档没有限制实体,导致可以造成XXE漏洞,这里很尴尬的利用点是:
- 需要一个文件上传
- 后缀名必须为jrxml
TemplateController
在TemplateController这个类的getTemplates方法中:
在这里对获取的参数没有任何的校验,导致可以跨越目录列文件并且修改文件,但是在后面的实现中,我们只能修改和查看特定的文件
假设我们在tmp目录下有着a.html和a.xml文件,我们可以跨越到tmp目录下读取并修改这两个文件
TemplateController
还有一个地方就是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
不足
- 感觉一个很大的问题是sink的定义,因为框架的变换以及一些开发者自己的工具类,以及一些漏洞可能根本不存在,导致sink的定义有时候挖不出来漏洞
- 像p0desta师傅测的CSRF漏洞,暂时想不到有什么好的办法来定义sink,人工可能很好去看出来,但是不好用codeql语言定义这种漏洞
- 太菜了,有个点的任意文件读取写不出来ql,2333
师傅们教教我
- 感觉在定义的时候要尽量找共性,但是也不能找太深
参考文章:
http://p0desta.com/2019/04/20/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8Bjava%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E7%B3%BB%E5%88%97(%E5%9B%9B)/
https://help.semmle.com/QL/learn-ql/java/ql-for-java.html