使用codeql挖掘fastjson利用链

什么是codeql

codeql是github security lab开发的一种代码查询语言,可以利用codeql方便的进行代码的污点追踪分析,通过像SQL查询语言一样的对代码的查询方式,可以让使用者不用去过于关心污点追踪的实现细节,具体的codeql的语法和使用方法可以在官网上查看

https://securitylab.github.com/tools/codeql

利用codeql挖掘fastjson利用链

首先要清楚,fastjson的利用链主要集中在getter和setter方法中,如果getter或者setter的方法中存在一些危险操作,比如JNDI查询之类的调用的话,如果参数可控就可以导致JNDI注入,而且fastjson的防御方式为黑名单,所以会层出不穷fastjson的绕过gadgets

鉴于fastjson的漏洞原理较为简单,且source(用户输入的源头)和sink(危险的函数)较为明确,所以可以使用codeql对一些常见的库进行fastjson利用链的挖掘

定义fastjson的入口点

fastjson的source相对比较好定义,所有fastjson的入口函数都是getter和setter这些函数,所以对应的source就为这些getter和setter,在codeql查询中,其实相当于将所有的函数按照用户所需要的过滤规则拿出来,所以我们只需要定义过滤的规则

对于getter和setter的规则,这里其实并不是一定要有对应的属性,只要前三个字母开头是get,并且第四个字母大写即可

getter的规则:

  1. 以get开头
  2. 没有函数参数
  3. 是我们的code database中的函数
  4. 为public方法
  5. 函数名长度要大于3

setter的规则:

  1. 以set开头
  2. 函数参数为一个
  3. 是我们code database中的函数
  4. 为public方法
  5. 函数名长度大于3
  6. 返回值为void

所以我们可以通过这几个规则写出对应的fastjson gadgets入口点的ql描述为:

class FastJsonSetMethod extends Method{
    FastJsonSetMethod(){
        this.getName().indexOf("set") = 0 and
        this.getName().length() > 3 and
        this.isPublic() and
        this.fromSource() and
        exists(VoidType vt | 
            vt = this.getReturnType()
        ) and
        this.getNumberOfParameters() = 1
    }
}


class FastJsonGetMethod extends Method{
    FastJsonGetMethod(){
        this.getName().indexOf("get") = 0 and
        this.getName().length() > 3 and
        this.isPublic() and
        this.fromSource() and
        this.hasNoParameters()
    }
}

定义危险函数

这里危险函数不仅仅是JNDI注入的函数,也可以是DNS查询之类的函数

JNDI函数规则:

  1. 这个函数名为lookup
  2. 这个函数所在的类实现了"javax.naming.Context"接口

所以用ql语言描述为:

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

确定搜索方法

因为在fastjson中,有两个输入点,一个是get方法所在类的属性,一个是在fastjson触发的时候所传入的参数,为了方便起见,没有定义确定的source,一般来说能满足get方法最后到lookup的类相对较少,所以可以在查询结束以后再人工进行一次确认

这里没有用fastjson的全局的污点追踪,而是直接通过语法结构查找对应的利用链

先放一下代码,然后解释一下为什么这么写:

MethodAccess seekSink(Method sourceMethod){
    exists(
        MethodAccess ma, Method method|
        (ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*() and
        method = ma.getMethod()) or
        (ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*() and ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().isAnonymous() and method = ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().getAMethod())|
        if method instanceof JNDIMethod
        then result = ma
        else result = seekSink(method)
    )
}

基础版

首先,我们需要获取到所有getter方法内部的方法调用,这里使用了ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*()做选择,如果这个方法调用围绕的结构正是我们getter方法内部的一个子结构的话,那么证明这个方法调用是在getter中的

我们先跳过or这里的定义,来看后面:

后面对内部调用进行判断,看它是否是一个JNDI查询的方法,如果不是的话,因为还有内部调用,所以继续递归查询内部调用的内部调用,这样就可以获取到更深调用的JNDI查询

进阶版

也就是or后面的这一段,在fastjson 1.2.66中有这么一个gadgets:

入口点为:org.apache.shiro.jndi.JndiObjectFactory

用上面的ql是查不出来的,因为这里的lookup是在一个匿名类里面,并且是在函数参数中定义的,所以增加一个新的sink点

ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*() and ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().isAnonymous() and method = ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().getAMethod()),和前面一样,函数必须是内部调用的函数,并且函数的第一个参数为一个匿名类,之后获取到匿名类中的方法,即可获取到这种特殊的sink点

执行结果

shiro挖掘结果:

common-configuration挖掘结果:

改进点

现在的这个ql还是有很多问题的,还需要继续去完善

  1. 这种函数参数的匿名类,可能不在第一个索引,需要对所有的参数做判断
  2. 对应的sink可以继续去拓展,可能有其他的调用方式没有加入(例如lambda表达式)
  3. 危险方法不一定是JNDI查询,也可以是其他的方法
  4. 没有使用到污点追踪,所以需要一点人工成本

PS:shiro在编译的时候会报错,用mvn compile -fn可以忽略编译错误,成功构建database

fastjson.qll

import java
import semmle.code.java.dataflow.DataFlow

class FastJsonSetMethod extends Method{
    FastJsonSetMethod(){
        this.getName().indexOf("set") = 0 and
        this.getName().length() > 3 and
        this.isPublic() and
        this.fromSource() and
        exists(VoidType vt | 
            vt = this.getReturnType()
        ) and
        this.getNumberOfParameters() = 1
    }
}


class FastJsonGetMethod extends Method{
    FastJsonGetMethod(){
        this.getName().indexOf("get") = 0 and
        this.getName().length() > 3 and
        this.isPublic() and
        this.fromSource() and
        this.hasNoParameters()
    }
}

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

predicate isClassField(RefType classType, string fieldName){
    classType.getAField().hasName(fieldName)
}

MethodAccess seekSink(Method sourceMethod){
    exists(
        MethodAccess ma, Method method|
        (ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*() and
        method = ma.getMethod()) or
        (ma.getEnclosingStmt() = sourceMethod.getBody().getAChild*() and ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().isAnonymous() and method = ma.getArgument(0).(ClassInstanceExpr).getAnonymousClass().getAMethod())|
        if method instanceof JNDIMethod
        then result = ma
        else result = seekSink(method)
    )
}

fastjson.ql
import java
import semmle.code.java.dataflow.DataFlow
import fastjson


from FastJsonGetMethod getMethod
select getMethod, seekSink(getMethod)

点击收藏 | 7 关注 | 4
登录 后跟帖