CodeQL 提升篇

如果已经了解CodeQL基础知识相信如下内容对各位有一定的帮助

功能

编译

闭源项目创建数据库,可以使用该工具:https://github.com/ice-doom/codeql_compile

历史查询

在VSCode左侧可以的QUERY HISTORY可以点击切换历史查询内容,也可以右键比对查询结果等功能

图片[1]-CodeQL 提升篇-NGC660 安全实验室


查看AST

在VSCode左侧选中要查看的java文件之后,点击View AST即可查看,并且鼠标点击到java文件中的类、方法等,AST VIEWER中会自动帮助我们定位到该项

图片[2]-CodeQL 提升篇-NGC660 安全实验室


快速查询

在我们编写的一些谓词上方有个快速查询按钮,点击之后可以快速查询当前谓词的结果。

图片[3]-CodeQL 提升篇-NGC660 安全实验室


语法

列出个人经常用到的语法和一些注意事项

获取具体QL类型

不确定使用什么方式获取目标时,除了通过查看AST,还可以通过词getAQlClass()获取调用它实体的具体QL类型。

from Expr e, Callable c
where e.getEnclosingCallable() = c
select e, e.getAQlClass()

尽可能缩小范围

如下定义,如果项目代码量很大,则非常耗时

override predicate isSink(DataFlow::Node sink) {
    sink.asExpr().getParent() instanceof ReturnStmt
}

可以设置return语句在哪个函数中调用来缩小范围,乃至其Type的全限定名

override predicate isSink(DataFlow::Node sink) {
    sink.asExpr().getParent() instanceof ReturnStmt
    and sink.asExpr().getEnclosingCallable().hasName("xxxxx")
}

个人使用的几个规则

// 以某个方法的参数作为source (添加了几种过滤方式,第一个参数、该方法当前类的全限定名为xxxx)
override predicate isSource(DataFlow::Node source) {
    exists(Parameter p |
        p.getCallable().hasName("readValue") and
        source.asParameter() = p and
        source.asParameter().getPosition() = 0
        and p.getCallable().getDeclaringType().hasQualifiedName("com.service.impl", "xxxxx")
    )
}

// 以某个实例的所有参数作为source(`X1 x1 = new X1(a,b)`,这里a、b作为source),过滤:调用该实例的方法名称为`Caller`,实例类型名称为`X1`
override predicate isSource(DataFlow::Node source) {
    exists(ClassInstanceExpr ma |
        source.asExpr() = ma.getAnArgument()
        and ma.getTypeName().toString() = "X1"
        and ma.getCaller().hasName("Caller")
    )
}

调用端点路径

比如我们想知道方法A到方法G之间调用端点路径,则可以使用edges谓词,编写如下所示,如果也想找覆写的某个方法(如:接口实现类中的方法)可以将calls替换为polyCalls

import java

class StartMethod extends Method {
  StartMethod() { getName() = "main" }
}

class TargetMethod extends Method {
  TargetMethod() { getName() = "vulMain" }
}

query predicate edges(Method a, Method b) { a.calls(b) }

from TargetMethod end, StartMethod entryPoint
where edges+(entryPoint, end)
select end, entryPoint, end, "Found a path from start to target."

得到的结果如图所示

图片[4]-CodeQL 提升篇-NGC660 安全实验室


对某接口实现

主要是通过codeql自带谓词overridesOrInstantiates判断该函数是否进行了重写。
如下,就能获取实现JSONStreamAware接口,重写的方法

class JsonInterface extends Interface{
    JsonInterface(){
        this.hasQualifiedName("com.alibaba.fastjson", "JSONStreamAware")
    }

    Method getJsonMethod(){
        result.getDeclaringType() = this
    }
}

class CMethod extends Method{
    CMethod(){
        this.overridesOrInstantiates*(any(JsonInterface i).getJsonMethod())
    }
}

from CMethod m select m, m.getDeclaringType()
图片[5]-CodeQL 提升篇-NGC660 安全实验室
图片[6]-CodeQL 提升篇-NGC660 安全实验室

查询Select

如果编写查询不规范可能会经常碰到类似如下错误
Showing raw results instead of interpreted ones due to an error. Interpreting query results failed: [xxxxx] Exception caught at top level: Could not process query metadata. Error was: Expected result pattern(s) are not present for problem query: Expected exactly one pattern. [INVALID_RESULT_PATTERNS]

这种情况的注意事项如下:
在不使用path查询时,元数据为@kind problem,并且也别导入path相关内容,如:import DataFlow::PathGraph,否则查询时会一直产生失败日志,而且当string中使用了$@占位符时会一直失败使其当作正常字符串展示在结果中。
这种查询由两列组成select element, string

使用path查询时,元数据为@kind path-problem,查询模板为select element, source, sink, string
element指定为source节点时最先显示的是source

图片[7]-CodeQL 提升篇-NGC660 安全实验室


element指定为sink节点时最先显示的是sink

图片[8]-CodeQL 提升篇-NGC660 安全实验室


AdditionalTaintStep

在为一些项目编写规则查询时,经常碰到数据流中断的情况,下面列出经常碰到中断的情况和解决方案。

setter和getter

场景1:在做GitHub CTF案例时这块有体会,CodeQL为减少误报很多地方都需要我们根据相应场景自己连接数据流,比如getter。
这种情况需要将调用方法的对象(通过getQualifier谓词获取限定符)和调用方法的返回值连接起来。如下操作就是从get%方法访问到它的限定符作为附加步骤重新连接起来。

class GetSetTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
        exists(MethodAccess ma |
            (ma.getMethod() instanceof GetterMethod or ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("get%") or ma.getMethod().getName().matches("set%"))
            and
             src.asExpr() = ma.getQualifier()
            and sink.asExpr() = ma
            )
    }
}

mapper

场景2:使用mybatis通常将接口命名为xxxxMapper或者xxxxDao这种形式,在xml配置文件中通过namespace指定其全限定名,当数据流需要经过数据库查询到这里会断开,那么需要手动将其连接起来。

如下我们使用普通查询从接收请求到return语句结束

图片[9]-CodeQL 提升篇-NGC660 安全实验室


最后会在此处中断

图片[10]-CodeQL 提升篇-NGC660 安全实验室


对应xml配置

图片[11]-CodeQL 提升篇-NGC660 安全实验室


那么需要添加AdditionalTaintStep将中断进行拼接。这里将污染源查询的id和某个方法连接(该方法的对象类型名称是xxxxDao),当然有的可能名称是xxxxMapper,根据情况而定

class MapperTaintStep extends TaintTracking::AdditionalTaintStep{

    override predicate step(DataFlow::Node src, DataFlow::Node sink){
        exists(MethodAccess ma |
            (ma.getQualifier().getType().getName().matches("%Dao") or ma.getQualifier().getType().getName().matches("%Mapper"))
            // and (src.asExpr() = ma.getAnArgument() or src.asExpr() = ma.getAnArgument().getAChildExpr())
            and src.asExpr() = ma.getAnArgument()
            and sink.asExpr() = ma
        )
    }
}

最后查询结果:

图片[12]-CodeQL 提升篇-NGC660 安全实验室


污染源作为参数传入

场景3:如下图所示,instance作为污染源,workNode也被污染,将其传入t.setSceneKeyt对象的sceneKey属性赋值,那么这里t对象理应也是被污染的。但当我们将instance作为sourcereturn t作为sink是获取不到路径的,需要加上额外步骤。

代码如下,将调用方法的所有参数作为source(图中setSceneKey方法的workNode.getSceneKey()参数),将调用方法的对象作为sink(图中的t对象)

class SrcTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
        exists(MethodAccess ma |
            (ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("set%"))
            and
                src.asExpr() = ma.getAnArgument()
            and sink.asExpr() = ma.getQualifier()
            )
    }
}

可以猜猜上图中总共需要添加几个额外步骤(3个,第一:刚刚讲的;第二:instance的getter;第三:workNodeMapper

实例化

场景4:如下图,将req传入UploadFile中创建UploadFile对象,再将其传入systemService.uploadFile方法中,这种情况,uploadFile对象应该是受污染的,但是默认情况下,我们像让数据流进入systemService.uploadFile中是不行的,因为在new UploadFile就已经断开了。那么就需要将其连接起来

图片[13]-CodeQL 提升篇-NGC660 安全实验室


代码如下,如果已经知道当前查询大概断的位置,可以缩小范围,这里将所有的都会连接起来

class InstanceTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
      exists(ClassInstanceExpr cie | 
        // cie.getTypeName().toString() = "UploadFile"
         src.asExpr() = cie.getAnArgument()
          and sink.asExpr() = cie)
    }
}

之前有位师傅提及了实例化断开问题,当时回答存在误导,这里算是重新讲清。

Partial flow

对于数据流中断时候如何去解决确定中断位置在哪,官方提供了Partial flow方式,也就是查询到中断前的部分流,对于某些场景是有帮助的。如果想了解的话可以阅读官方描述Debugging data-flow queries using partial flow¶

使用:
先导入PartialPathGraph,这里需要注意不能和PathGraph共存,也就是使用PartialPathGraph则不能导入import DataFlow::PathGraph

import DataFlow::PartialPathGraph

TaintTracking::Configuration配置中添加一个谓词,表示探索深度

override int explorationLimit() { result = 5 }

查询如下,注:hasPartialFlow是和PartialPathGraph匹配,hasFlowPathPathGraph匹配,导入的时候一定要注意,否则会导致查不出来内容。

from MyTaintTrackingConfiguration conf, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
where conf.hasPartialFlow(source, sink, _)
select sink, source, sink, "Partial flow from unsanitized user data"

当整个调用链非常长的时候又不知道具体断掉的位置,然后使用Partial flow会导致查询结果内容非常多,更不好排查了。官方提供了2种解决方式

图片[14]-CodeQL 提升篇-NGC660 安全实验室


大概的意思也就是,比如:a-b-c-d-e-f-g,不知道哪个位置中断了,那么就先查a-b-c-d,将sink从g修改为a。或者说是将source的大范围修改为确定的单个source来减少输出方便排查。还有就是可以使用sanitizer来清洗掉其他数据。

官方规则-path-injection

path-injection

用于检测文件相关,可以是文件上传、文件读取。主要判断逻辑是对与传入文件操作时文件名是否可控

打开CEW-022,官方对于此漏洞的简要说明:java-path-injection

图片[15]-CodeQL 提升篇-NGC660 安全实验室


TaintedPathConfig污点跟踪分析的配置如下

图片[16]-CodeQL 提升篇-NGC660 安全实验室


source

使用了RemoteFlowSource,其中定义了用户输入可控的常见源。

sink

sink定义中使用了陌生的谓词和类,先看看PathCreation

override predicate isSink(DataFlow::Node sink) {
    exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getAnInput() and not guarded(e))
}

跟进 PathCreation.qll 包,获取用于创建路径的输入,定义了常见用法。使用方式通过调用getAnInput()谓词获取方法内的所有参数,也就是将sink定义为传入的文件名。

图片[17]-CodeQL 提升篇-NGC660 安全实验室


再跟进 TaintedPathCommon.qll 查看guarded谓词

图片[18]-CodeQL 提升篇-NGC660 安全实验室


了解下ConditionBlock,可以使用下面查询内容

from ConditionBlock cb select cb, cb.getCondition(),cb.getCondition().getAChildExpr()

cb获取的是整个块,如:方法开始{}整个内容、if (tree.getId() != null)if (tree.getId() == null)
cb.getCondition()表示获取此基本块最后一个节点条件,如:comboTree.getId() != nullsalary.equalsIgnoreCase("null")
cb.getCondition().getAChildExpr()表示获取子表达式,如:tree.getId()nullsalary"null"

public void demo() {
    if (tree.getId() != null) {
        cq.eq("id", tree.getId());
    }
    if (tree.getId() == null) {
        cq.isNull("Depart");
    }
    cq.add();
    ......
    data.setFooter("salary:"+(salary.equalsIgnoreCase("null")?"0.0":salary)+",age,email:合计");
}

回到guarded谓词中,
exists(PathCreation p | e = p.getAnInput())再次强调变量调用为文件名。
cb.getCondition().getAChildExpr*() = c将块的子表达式和表达式c匹配
c = e.getVariable().getAnAccess()文件名的所有调用和表达式c匹配
cb.controls(e.getBasicBlock(), true)注释意为:如果传入的e.getBasicBlock()是由该条件控制的基本块,即条件为true的基本块,则保持成立。
比如通过controls查询,结果如下图,只有当dirNameif判断语句为true才能将dirName传入File中。

图片[19]-CodeQL 提升篇-NGC660 安全实验室


将传入controls谓词中的true修改为false,则能匹配到如下图所示。进行判断的是!后面内容,所以可以得到该项

图片[20]-CodeQL 提升篇-NGC660 安全实验室


not inWeakCheck(c)最后一个过滤条件
inWeakCheck谓词中定义调用方法的方法名等于startsWith等,传入表达式等于调用方法的对象。
EqualityTest表示使用==或者!=的表达式,getAnOperand()谓词获取左边和右边的操作表达式,判断其中一个为null

private predicate inWeakCheck(Expr e) {
  // None of these are sufficient to guarantee that a string is safe.
  exists(MethodAccess m, Method def | m.getQualifier() = e and m.getMethod() = def |
    def.getName() = "startsWith" or
    def.getName() = "endsWith" or
    def.getName() = "isEmpty" or
    def.getName() = "equals"
  )
  or
  // Checking against `null` has no bearing on path traversal.
  exists(EqualityTest b | b.getAnOperand() = e | b.getAnOperand() instanceof NullLiteral)
}

总结:
经过比对sink是否使用guarded谓词的结果如下,左边是没有使用guarded谓词

图片[21]-CodeQL 提升篇-NGC660 安全实验室


如下图,没有将文件名传入startsWith等方法,并且没有使用==或者!=null进行判断,只有当文件名的判断条件为true才能将其传入File中,那这种情况则不能当作sink。其实官方使用guarded谓词加入判断的这种情况有点不太理解,暂时没有想到哪些场景这种情况是适用的。可能我个人使用的话会将该项注释掉。

图片[22]-CodeQL 提升篇-NGC660 安全实验室


isSanitizer

如果数据类型是基本类型或者是其包装类则清洗掉

override predicate isSanitizer(DataFlow::Node node) {
    exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType)
}

isSanitizerGuard

这里也是起到清洗作用,当调用方法为contains并且其参数值为..,对表达式e的判断为false则条件成立。

class ContainsDotDotSanitizer extends DataFlow::BarrierGuard {
    ContainsDotDotSanitizer() {
        this.(MethodAccess).getMethod().hasName("contains") and
        this.(MethodAccess).getAnArgument().(StringLiteral).getValue() = ".."
}

    override predicate checks(Expr e, boolean branch) {
        e = this.(MethodAccess).getQualifier() and branch = false
    }
}

override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
    guard instanceof ContainsDotDotSanitizer
}

上面的内容以案例来看是容易理解些的,如下图,只有当sourceFilename.contains("..")的判断语句为false才能进入File中,那么这种情况则将其清洗掉。
也就是代码中如果对文件名内容进行..检测则清洗掉,不展示该数据。

图片[23]-CodeQL 提升篇-NGC660 安全实验室


以上就是path-injection内容的讲解,不考虑guarded谓词情况,其实挺容易理解的,将常见用户输入可控的位置作为source,将常见文件操作方法的参数即文件名作为sink,清洗掉那些类型是基本类型等、如果对文件名进行..检测则也清洗掉。

应用到真实场景

当我们查询,可以看到这里查询到了一个上传的工具类中,source是multipartRequest.getFileMap()方法。如果稍微往前根据可以看到这里multipartRequest对象应该就是controller中传入进来的request对象,那么这里需要重新找到具体是哪个controller调用到这里

图片[24]-CodeQL 提升篇-NGC660 安全实验室
图片[25]-CodeQL 提升篇-NGC660 安全实验室
图片[26]-CodeQL 提升篇-NGC660 安全实验室



重新将config编写如下,这里只将source查到RequestMapping,如果要考虑全可以有GetMapping等。但只修改为如下是还不能查到内容的。

class TaintedPathConfig extends TaintTracking::Configuration {
  TaintedPathConfig() { this = "TaintedPathConfig" }

  override predicate isSource(DataFlow::Node source) {

    exists( Method m, Parameter p| 
    m.getAnAnnotation().getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping")
    and m.hasAnnotation()
    and m.getAParameter() = p
    and source.asParameter()=p
    and p.getType().hasName("HttpServletRequest")
    )
   }

  override predicate isSink(DataFlow::Node sink) {
    exists( Method m, Parameter p| m.hasName("uploadFile") and
    m.getDeclaringType().hasQualifiedName("org.xxxx.core.common.dao.impl", "xxxxx")
    and m.getAParameter() = p
    and sink.asParameter()=p
    and p.getType().hasName("UploadFile")
    )
  }
}

原因就是UploadFile实例时这里中断了

图片[27]-CodeQL 提升篇-NGC660 安全实验室


将其连接起来后即可

class InstanceTaintStep extends TaintTracking::AdditionalTaintStep{
  override predicate step(DataFlow::Node src, DataFlow::Node sink){
      exists(ClassInstanceExpr ma | 
         sink.asExpr() = ma
        and src.asExpr() = ma.getAnArgument())
  }
}

本文作者:Ironf4

本文转载于先知社区,如有侵权请联系删除

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享