博客地址:
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