学习抽象语法树分析寻找FastJSON的Gadgets

博客地址:
https://www.freebuf.com/articles/web/213327.html
项目地址:
https://github.com/Lonely-night/fastjson_gadgets_scanner
修改里面的反编译之后的存放java源代码的路径;以及fernflower的路径,
先反编译,

python3.6 decomplie_jar.py

在这里插入图片描述
然后使用扫描器进行扫描:

python3.6 scanner.py
/home/77/repos/tmp/source/HikariCP-2.7.9/com/zaxxer/hikari/HikariConfig.java getObjectOrPerformJndiLookup
/home/77/repos/tmp/source/commons-configuration-1.10/org/apache/commons/configuration/JNDIConfiguration.java containsKey
/home/77/repos/tmp/source/commons-configuration-1.10/org/apache/commons/configuration/JNDIConfiguration.java getProperty
43641

在这里插入图片描述
总共304个jar包,43641个Java源文件,工找到三个方法。其实可以继续更细致。

现在对别人写的这个使用AST扫描目标目录下jar包里的gadget的方法。

抽象语法树分析寻找FastJSON的Gadgets代码分析

首先认为fastjson/jackson中的gadget的sink点为:

Object object = context.lookup(name);

其中context对象为javax.naming.Context或其子类的实例,而name为一个jnid的url。

先是把~/.m2/respository目录下的jar包拿到,然后用fernflower反编译,得到压缩的java源码包,然后解压,
在这里插入图片描述

首先从这个scanner函数开始:

from javalang.parse import parse
from javalang.tree import *

# 传入的是一个目录下的java源代码文件
def scanner(filename):
    file_stream = open(filename, 'r')
    _contents = file_stream.read()
    file_stream.close()

    # 字符串判断快速过滤
    #(如果文件里根本没有InitialContext(相关的内容,则直接返回False,不用浪费时间了)
    if "InitialContext(" not in _contents:
        return False

    try:
    	# 使用javalang库解析源代码,得到抽象语法树AST
        root_tree = parse(_contents)
    except:
        return False
       
    # 拿到满足那三个条件的类声明,可能不止一个,是一个list
    class_declaration_list = get_class_declaration(root_tree)
    
    # 遍历类声明
    for class_declaration in class_declaration_list:
    	# 遍历方法声明
        for method_declare in class_declaration.methods:
            if ack(method_declare) is True:
                string = "{file} {method}".format(file=filename, method=method_declare.name)
                print string
                write_file("./result.txt", string)

由于FastJSON的checkAutoType方法对反序列化的类有三点限制:

  • 1、不能继承 Classloader;
  • 2、不能实现 DataSource 和 RowSet 接口(在黑名单中);
  • 3、必须有一个无参的构造函数。

于是这里使用get_class_declaration()用于找出符合这种特征的类,看一下这个函数内容:

def get_class_declaration(root):

    class_list = []
    black_interface = ("DataSource", "RowSet")
    for node in root.types:
        # 非类声明都不分析(类声明被映射为ClassDeclaration 对象)
        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

拿到类声明列表之后,遍历得到类声明(ClassDeclaration),然后再对这个类声明遍历方法声明(class_declaration.methods)。
对于每个方法声明,再使用ack方法最后确认,

def ack(method_node):
    """
    1、是否调用的lookup 方法,
    2、lookup中参数必须是变量
    3、lookup中的参数必须来自函数入参,或者类属性
    :param method_node:
    :return:
    """
    target_variables = []
    for path, node in method_node:
        # 是否调用lookup 方法
        # (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:
        parameter_name = parameter.name
        if parameter_name in target_variables:
            return True
    return False

第一个for循环,拿到lookup方法调用的参数,判断是否可控;(TODO:判断这个lookup方法是否是javax.naming.Context类及其子类的实例调用的)
第二个for循环,判断这个方法本身的参数与这个方法里调用lookup的参数是一样的。

学习javalang的API的笔记

node类

node.extends.name:类继承的类的名字
node.implements:类实现的接口(list)
node.implements.name: 类实现的接口的名字
node.constructors:类的构造器(list)
for constructor in constructors: 类的构造器的参数列表(list)
constructor.parameters

node方法

isinstance(node, MethodInvocation):判断某method节点是否是MethodInvocation类型
node.member:方法名
node.arguments:方法的参数列表(list)
isinstance(arg, Cast):arg为变量 类型强转
isinstance(arg, MemberReference):arg为变量引用
isinstance(arg, This):arg为this的属性

检测算法

    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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章