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