静态代码扫描工具之GadgetInspector探究

反序列化漏洞一直占据着JAVA安全的半壁江山,是每个JAVA安全研究者都绕不开的大山之一。从15年被 @frohoff and @gebl 公布利用到现在近8年的时间,反序列化漏洞在Oracle/Apache/vmware/IBM/国产等众多厂商的系统、组件、中间件中肆虐横行。笔者在之前文章分析的JNDI/RMI/JMX等均与此安全问题有千丝万缕的关系。链接直达:

Attack JMX Service的打开方式

论JAVA-RMI的攻防演进史

当我们谈论JNDI注入时,我们在谈论什么

反序列化漏洞利用的入口方法source及最终漏洞触发方法sink都容易确定,而寻找连接入口方法到漏洞触发方法中间的利用链路才是挖掘与利用的重点。在日常的审计挖掘过程中,日益感觉ysoserial、marshalsec中现存的利用链并不能百发百中,很多时候都需要我们根据现有环境寻找新的利用链。在寻找过程中也非常考验挖掘者的知识储备与耐心。那有没有一款自动化/半自动化的工具可以辅助我们去寻找呢?为了解决这个问题,Ian Haken在18年的blackhat大会推出了一款自研扫描工具 GadgetInspector,关于该款工具的原文介绍、源码及视频如下:

源码:https://github.com/JackOfMostTrades/gadgetinspector

文稿:https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf

视频:https://www.youtube.com/watch?v=fdctNIt8OIw

圈内的师傅也对此工具做过分析与完善,本文也参考了一些师傅们的文章,致敬感谢分享!

threedr3am-java反序列化利用链自动挖掘工具gadgetinspector源码浅析

Longofo-Java 反序列化工具 gadgetinspector 初窥

su18-高效挖掘反序列化漏洞——GadgetInspector改造

整体来看 GadgetInspector工具运行流程主要为对classpath中全部class进行信息收集,生成方法内、方法间的污点传递关系,最终找到从source可以通往sink的利用链。另外扫描针对的是java字节码,所以可以直接分析Jar包/War包而无需项目java源码。整个过程还是比较清晰的,其中涉及到JAVA ASM技术、JVM指令、逆拓扑排序、进出栈模拟等等知识。之前对这块内容了解不多,也想做个扫描工具辅助自己做一些基础审计的工作,争取早日解放双手!本文从所需的前置知识、分析扫描流程中的关键类及实际测试扫描三部分展开,如果对文章有疑问/建议或者想一起研究交流的师傅,欢迎私信😜

1、前置知识

前置知识部分主要有描述符、ASM技术及JVM相关的知识。万丈高楼平地起,晓得了基础知识方便后续展开分析

1.1 描述符

GadgetInspector最开始对类、方法信息搜集阶段会用到类型及方法描述符

1、类型描述符,JAVA原始类型的描述符均对应一个大写字母。非数组的引用类型使用:L+类全限定名称+; 、数组引用类型使用:[+数组内类型描述符+;

1
2
3
4
5
6
7
8
9
10
11
12
13
byte -> B
short -> S
int -> I
long -> J
float -> F
double -> D
char -> C
void -> V
boolean -> Z

java.lang.Runtime -> Ljava/lang/Runtime;
java.lang.Object[] -> [java/lang/Object;
int[][] -> [[I

2、方法描述符

字节码中保存参数类型列表及返回值类型时会用到方法描述符,规则:1、格式为(+参数列表+)+返回值;2、参数列表无需用逗号分割

1
2
3
4
String add(int a,int b) -> (II)Ljava/lang/String;
void sub(int a,int b) -> (II)V
int[] s(Object obj) -> (Ljava/lang/Object;)[I
void a() -> ()V

3、方法 access值

access值是按照方法的修饰符对应值做”按位或|”计算得来的,如main函数为public、static修饰,则access = ACC_PUBLIC(1) | ACC_STATIC(8) = 9,具体对应值的定义在org.objectweb.asm.Opcodes类中

access name desc
1 ()V
9 main ([Ljava/lang/String;)V

方法:java类加载初始化过程中,会将类中的类变量赋值、静态代码块中的语句集合为方法

1.2 ASM技术

GadgetInspector中主要利用了ASM的访问者模式对类/方法进行观察操作,ASM封装了对于class文件结构各项元素的操作,对类使用ClassVisitor实现类进行观察、对方法使用MethodVisitor实现类进行观察

ClassVisitor 用于观察类信息,GadgetInspector中使用的几个类观察类:MethodDiscoveryClassVisitor、MethodCallDiscoveryClassVisitor、PassthroughDataflowClassVisitor、ModelGeneratorClassVisitor。这些类都继承实现了父类ClassVisitor的visitXXX方法,如下是方法名称及触发调用的条件

1
2
3
visit 初始化被观察类的描述信息
visitField 观察到属性时触发此方法
visitMethod 观察到方法时触发此方法

MethodVisitor 用于观察方法信息,GadgetInspector中使用的几个方法观察类:MethodCallDiscoveryMethodVisitor、PassthroughDataflowMethodVisitor、ModelGeneratorMethodVisitor、TaintTrackingMethodVisitor。在这些类中涉及到几个比较重要且频繁调用的visitXXX方法,如下是方法名称及触发调用的条件

1
2
3
4
5
visitCode 进入方法时触发
visitMethodInsn 方法内调用子方法时触发
visitInsn 碰到无操作数指令时触发
visitFieldInsn 调用字段时触发
visitVarInsn 操作变量时触发

另外可以使用IDEA的ASM Bytecode Viewer插件查看类对应的字节码及ASM代码

ASM代码

1.3 JVM

在重点分析下节内容前,我们先引入一些JVM中的基础概念:变量表、操作数栈、JVM指令集

本地变量表 Local Variable Table :存储的是 方法参数和方法内定义的局部变量。容量以 变量槽 Variable Slot 为最小单位,一个槽可以放置32位以内的数据类型

操作数栈 Operand Stack :是一个后入先出栈LIFO(last in first out)。当一个方法开始执行前操作数栈是空的,随着方法执行,会从本地变量表、对象实例字段中加载变量/常量到操作数栈中,也会将栈中元素出栈到本地变量表或返回给方法调用者,即为出栈入栈操作。当方法执行完毕且有返回值赋值给变量时,需要通过 [type]store_[n]等指令将变量(类型type)存入本地变量表对应的位置(索引n)。而一次方法的执行往往包含多个出栈/入栈的过程

JVM指令集Java Virtual Machine Instruction Set:由操作码与操作数组成,一条JVM指令可以包含0个或多个操作数。大多数的JVM指令直接包含了操作对应的数据类型信息,比如iload、fload就表示从本地变量表中加载int数据、float数据到操作数栈。在GadgetInspector工具中 TaintTrackingMethodVisitor#visitInsn就模拟了无操作数的JVM指令操作,TaintTrackingMethodVisitor#visitIntInsn模拟了一个操作数的JVM指令操作(第一个int参数表示操作码、第二个表示操作数)

各指令代表的含义可参考oracle官方文档:https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-6.html

每个类对应的jvm指令集可以使用idea插件jclasslib很方便的查看:https://github.com/ingokegel/jclasslib 如使用jclasslib查看java.lang.Runtime#halt方法的指令集

1
2
3
4
5
6
7
8
9
//source
public void halt(int status) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkExit(status);
}
Shutdown.beforeHalt();
Shutdown.halt(status);
}

对应的JVM指令:

1
2
3
4
5
6
7
8
9
10
11
12
 0 invokestatic #203 <java/lang/System.getSecurityManager : ()Ljava/lang/SecurityManager;>
3 astore_2
4 aload_2
5 ifnull 13 (+8)
8 aload_2
9 iload_1
10 invokevirtual #191 <java/lang/SecurityManager.checkExit : (I)V>
13 invokestatic #194 <java/lang/Shutdown.beforeHalt : ()V>
16 iload_1
17 invokestatic #196 <java/lang/Shutdown.halt : (I)V>
20 return

方法体内每行代码的操作指令

结合使用插件ASM Bytecode Viewer中的ASM代码、插件jclasslib生成的JVM指令就可以很好的辅助我们理解GadgetInspector是如何利用ASM观察者模式、JVM指令操作、模拟java stack进出这些技术最终达到挖掘利用链的目的的

2、关键类

在整个项目中有一些关键类,承担了信息收集、方法内污点分析、方法间污点传递、逆拓扑排序、JVM 指令模拟、source方法查找、利用链整合等任务。我们按照扫描顺序重点分析下这几个关键类的代码

2.1 MethodDiscovery 类方法信息收集

MethodDiscovery类用于发现目标classpath中的的方法,但是实际扫描时会把gadgetinspector本身的类也收集进去 干扰我们的分析,我在getAllClasses中增加了逻辑判断来排除gadgetinspector本身的类

1
2
3
4
5
6
#ClassResourceEnumerator#getAllClasses
for(ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()){
if((!classInfo.getName().contains("gadgetinspector"))){
result.add(new ClassLoaderClassResource(classLoader,classInfo.getResourceName()));
}
}

用于观察的ASM类为MethodDiscovery.MethodDiscoveryClassVisitor,其继承ClassVisitor类:重写了visitField、visitMethod方法,在观察到目标类的属性、方法时,将其添加到待分析的members List与discoveredMethods List中

2.2 PassthroughDiscovery 单方法的污点分析

PassthroughDiscovery类是针对单个方法的局部污点分析,得到可影响(污染)返回值的参数索引,生成passthrough数据流。共有三步:1、分析调用关系,得到每个方法调用的方法集合;2、对所有方法的调用进行逆拓扑排序;3、分析数据流传递情况,生成passthroughDataflow数据流

PassthroughDiscovery#discover

如下是分析FnEval类的结果,org.example.FnEval#invokeCall方法内部调用了java.lang.Runtime#getRuntime、java.lang.Runtime#exec(java.lang.String)两个方法

2.2.1 分析每个方法的调用关系

第一步是分析2.1得到的方法集合,将每个方法内部调用的方法都存储到methodCalls Map中。分析的核心类是MethodCallDiscoveryClassVisitor与MethodCallDiscoveryMethodVisitor

PassthroughDiscovery.MethodCallDiscoveryClassVisitor 继承自ClassVisitor类:重写了visitMethod方法,方法体内使用MethodCallDiscoveryMethodVisitor进行方法级别的观察分析

PassthroughDiscovery.MethodCallDiscoveryMethodVisitor 继承自MethodVisitor类:重写了visitMethodInsn方法,方法体内每次方法调用都会触发该重写方法,然后会将被调用的方法信息(所属类信息、方法描述信息)添加到结果methodCalls Map中

PassthroughDiscovery.MethodCallDiscoveryMethodVisitor#visitMethodInsn

2.2.2 逆拓扑排序

将2.2.1得到的methodCalls Map经过逆拓扑排序得到sortedMethods List,PassthroughDiscovery#topologicallySortMethodCalls 排序方法如下

排序分析参考了Longofo师傅的 分析 PassthroughDiscovery#dfsTsort

  • dfsStack:用来分析方法调用顺序,保证在逆拓扑时候不形成环
  • visitedNodes:访问过的结点,在一条调用链出现重合的时候,不会造成重复的排序
  • sortedMethods:最终逆拓扑排序出来的结果

举例如下图为方法调用树,初始方法为med1,有6条方法调用链

最终经过逆拓扑排序后得到的顺序是:

1
med7、med8、med3、med6、med2、med4、med1

看到在gadgetinspector.PassthroughDiscovery#dfsTsort的开头有这样两行代码

第一个if语句(圈1)目的是处理”方法互相调用形成环”的问题,如果正在分析的dfsStack中包含med1,那么就不往dfsStack中再添加了。第二个if语句(圈2)是处理”方法重复调用”的问题,如果在分析med1->med3这条链时发现med3在med1->med2-med3链中已访问过,那么不再继续访问。策略非常粗暴,对于第一次已排序过的方法,第二次碰到时不再分析。虽然避免了重复扫描跟环的问题,但是造成了严重的漏报。如果med1->med3->med7 刚好可以走通source到sink,利用链也短,工具由于策略问题反而不会扫到。所以这一步需要改进策略以平衡”搜索广度与深度策略的平衡问题”

另外这里为什么要用逆拓扑排序DFS算法,而不是正向拓扑排序BFS呢?这个我们下一章节会碰到,是因为要确定的是父方法的返回值与哪个参数有关,而在方法执行过程中,返回值会受到子方法的影响,所以需要先判断子方法的参数与返回值的关联关系,进而才能得出父方法的参数污染情况。所以这里需要使用逆拓扑排序DFS算法

2.2.3 单个方法污点分析

依次分析2.2.2步得到的sortedMethods List后,可以得出每个方法的返回值受哪个参数的影响(也叫污染)。这一步是信息搜集的重点,有了每个方法的污染结果后就可以开始串联方法进行分析

PassthroughDiscovery.PassthroughDataflowClassVisitor 继承自ClassVisitor类,重了写visitMethod方法:对目标方法使用PassthroughDataflowMethodVisitor进行方法级别的观察分析

PassthroughDiscovery.PassthroughDataflowMethodVisitor继承自TaintTrackingMethodVisitor类;父类TaintTrackingMethodVisitor 继承自MethodVisitor类

TaintTrackingMethodVisitor#TaintTrackingMethodVisitor

这两者是父子类关系,子类PassthroughDataflowMethodVisitor实现了:visitCode、visitInsn、visitFieldInsn、visitMethodInsn等方法

父类实现了:visitCode、visitFrame、visitInsn、visitIntInsn、visitVarInsn、visitTypeInsn、visitFieldInsn、visitMethodInsn、visitInvokeDynamicInsn、visitJumpInsn、visitLabel、visitLdcInsn、visitIincInsn、visitTableSwitchInsn、visitLookupSwitchInsn、visitMultiANewArrayInsn、visitInsnAnnotation、visitTryCatchBlock、visitTryCatchAnnotation、visitMaxs、visitEnd等方法。这些是ASM观察类碰到各种JVM指令时会触发调用的方法

这对父子类是分析数据流的核心类,其使用JAVA ASM技术观察方法/属性/变量操作/子方法调用、模拟JVM Stack入栈出栈操作,相当于让静态代码”动”了起来,在动的过程中去观察参数的传递污染、方法的调用,进而找出可以从source通往sink之间的链路。gadgetinspector开发者在TaintTrackingMethodVisitor.SavedVariableState类中创建了localVars、stackVars用来对标本地变量表Local Variable Table及操作数栈 Operand Stack,在同类下利用ASM技术中visitXXX等观察方法模拟了JVM指令集调用push()、pop()对变量表及操作栈的进出操作

接着分析父子类实现的几个关键方法:

1、visitCode:观察方法时,首先会先触发visitCode,子类将 [[对象实例],arg1,arg2]按照先后顺序添加到本地变量表localVars中,用于后续出栈入栈的数据来源。而父类作用是初始化了本地变量表localVars、操作数栈stackVars

PassthroughDiscovery.PassthroughDataflowMethodVisitor#visitCode

TaintTrackingMethodVisitor#visitCode 用于清空本地变量表与操作数栈及占位

2、visitInsn:观察方法时,碰到无操作数指令时会触发visitInsn ,子类PassthroughDataflowMethodVisitor主要处理碰到return指令集时的参数污染情况,父类作用是模拟无操作数指令集对应的出栈入栈操作,如[type]CONST_[n]推送数据到栈顶、[type]ASTORE数组出栈到表、[type]ALOAD数组出表入栈等等

PassthroughDataflowMethodVisitor#visitInsn是操作中的重点,会将栈顶数据添加到污点结果集合returnTaint,此时栈顶元素可能为:1、对象实例/实例属性;2、被调用方法的返回值。ASM在碰到这两者时会调用visitFieldInsn、visitMethodInsn进行观察,这两个方法均会将污点参数索引添加至栈顶

PassthroughDiscovery.PassthroughDataflowMethodVisitor#visitInsn

TaintTrackingMethodVisitor#visitInsn

3、visitFieldInsn:观察方法时,每次对属性字段的操作都会触发visitFieldInsn,子类PassthroughDataflowMethodVisitor主要处理GETFIELD指令:对方法所属类使用决策器serializableDecider判断、对属性判断是否被transient修饰。所属类通过决策器且属性不被transient修饰就通过决策。父类模拟了 获取静态属性值GETSTATIC、设置静态属性值PUTSTATIC、获取非静态属性值GETFIELD、设置非静态属性值PUTFIELD4个JVM指令的出栈入栈操作

PassthroughDiscovery.PassthroughDataflowMethodVisitor#visitFieldInsn

TaintTrackingMethodVisitor#visitFieldInsn

4、visitMethodInsn:观察方法时,方法体内每次调用其它方法时都会触发visitMethodInsn。子类主要处理 init初始化方法污染情况、调用目标方法的污染情况。父类主要处理init初始化方法、白名单、调用目标方法的污染情况、Collection/Map子类的方法参数的污染情况。调用方法分为调用静态方法、实例方法、超类构造方法、接口方法,调用方法所属类为引用类型时会调用INVOKEVIRTUAL指令、为接口时调用INVOKEINTERFACE指令

visitMethodInsn先将实例、入参挨个添加到argTypes数组

假定被调用方法有Object、String 2个入参,那么argTypes为this、Object、String。接着将调用目标方法前放到栈中的参数(即argTypes),都复制一份到argTaint。如果被调用方法是init方法,那么this实例(索引为0)是可以进行污染传递的

由于方法调用顺序经过逆拓扑排序,所以在分析时其方法体内调用的子方法一定已经被分析过。这一步会分析调用方法的返回值是否会收到被调用方法参数的影响,如果影响,那么添加到污点集合returnTaint

子类判断之后,先将污点参数索引放到resultTaint,然后调用父类继续处理init方法、ObjectInputStream#defaultReadObject、白名单方法、Map/Collection子类方法、被调用方法的污染传递情况,最后把被调用方法的污染参数索引放入操作数栈再次回到子类。由于resultTaint是Set类型,所以添加时会自动对索引进行去重

TaintTrackingMethodVisitor#visitMethodInsn

另外在2.3 CallGraphDiscovery#ModelGeneratorMethodVisitor 中会判断调用方法的传入参数是否会影响被调用方法的传入参数,最后在GadgetChainDiscovery中判断两者参数索引是否一致,如果一致表明此链可以走通

5、visitVarInsn:观察方法时,方法体内每次对局部变量操作时都会触发visitVarInsn,此方法在父类中定义。 利用push、pop方法对本地变量表localVars、操作数栈stackVars操作模拟出栈入栈 [type]STORE、[type]LOAD等JVM指令

TaintTrackingMethodVisitor#visitVarInsn

经过如上分析,最后得到的分析结果都保存在passthrough.dat文件中,格式为:类 方法 方法描述 污点索引

2.3 CallGraphDiscovery 方法间的污点传递

2.2中的PassthroughDiscovery分析得到的是调用方法返回值与被调用方法参数传递的关系,如果想要知道方法链能否从source源走通到sink点,还需要分析每个调用方法入参与被调用方法入参的传递关系,CallGraphDiscovery类就是为了解决这个问题

这一步的核心使用了ModelGeneratorClassVisitor,其实现了visitMethod、visitOuterClass、visitInnerClass等方法,在visitMethod方法中使用ModelGeneratorMethodVisitor进行方法级别的观察分析。ModelGeneratorMethodVisitor类与PassthroughDataflowMethodVisitor一样,都是继承自TaintTrackingMethodVisitor类,父类我们在上一步2.2章已经分析过不再赘述。子类ModelGeneratorMethodVisitor主要方法有:visitCode、visitMethodInsn、visitFieldInsn,依次分析下:

1、利用visitCode() 往本地变量表添加调用方法入参(当方法存在2个形参时,非静态方法:[[0,arg0],[1,arg1],[2,arg2]] 静态方法:[[0,arg0],[1,arg1]]);

CallGraphDiscovery.ModelGeneratorMethodVisitor#visitCode

2、利用visitFieldInsn()将获取的field名称变为arg[n].[name]放入操作数栈供后面方法调用时使用,这一步主要作用是将方法体内操作的属性与方法入参做关联,用于判断调用方法入参与被调用方法入参的关系。而这里也使用了跟2.2章一样的判断逻辑:因为java里面规定Transient修饰属性不参与反序列化过程,所以不考虑Transient属性修饰的变量传递。但是在实际挖掘过程中还存在一种情况:虽然属性被Transient修饰,但是类自己实现了writeObject/writeExternal方法,方法体内会声明将Transient属性参与到序列化/反序列化过程中。这种情况在之前的gadget中也比较常见,所以这里需要额外补充完善下Transient属性的判断逻辑

CallGraphDiscovery.ModelGeneratorMethodVisitor#visitFieldInsn

3、visitMethodInsn()方法创建了arg[N]进行标记,分析调用方法与被调用方法间的参数传递关系,并最终将结果赋值discoveredCalls存储到callgraph.dat中,格式为调用者类名 | 调用者方法 | 调用者方法描述 | 被调用者类名 | 被调用者方法 | 被调用者方法描述 | 调用者方法参index | 调用者字段名 | 被调用者方法参数索引

4、举个例子,createMBean方法经过ModelGeneratorClassVisitor扫描后得到如下结果,调用方法createMBean参数索引(第2个形参name)与被调用方法cloneObjectName(第1个形参name)的参数传递。调用者字段名的值为空,说明从调用方法入参传递到被调用方法入参的是入参实例(arg1)而不是入参类实例的属性(arg1.name、arg1.pwd)

1
2
调用者类名 | 调用者方法 | 调用者方法描述 | 被调用者类名 | 被调用者方法 | 被调用者方法描述 | 调用者方法参index | 调用者字段名 | 被调用者方法参数索引
com/sun/jmx/mbeanserver/JmxMBeanServer createMBean (Ljava/lang/String;Ljavax/management/ObjectName;)Ljavax/management/ObjectInstance; com/sun/jmx/mbeanserver/JmxMBeanServer cloneObjectName (Ljavax/management/ObjectName;)Ljavax/management/ObjectName; 2 1

com.sun.jmx.mbeanserver.JmxMBeanServer#createMBean(java.lang.String, javax.management.ObjectName)

1
2
3
4
5
6
7
8
9
10
public ObjectInstance createMBean(String className, ObjectName name)
throws ReflectionException, InstanceAlreadyExistsException,
MBeanRegistrationException, MBeanException,
NotCompliantMBeanException {

return mbsInterceptor.createMBean(className,
cloneObjectName(name),
(Object[]) null,
(String[]) null);
}

2.4 SourceDiscovery Source源发现

SourceDiscovery是抽象类,其discover(classMap,methodMap,inheritanceMap)方法只提供了定义,具体的实现需要使用者自己扩展编写。而gadgetinspector内置了java原生反序列化、jackson反序列化 的SourceDiscovery实现类

该步骤主要查找反序列化入口方法,每种反序列化操作的入口方法不同,如fastjson/jackson的入口是getter/setter方法、java原生反序列化是readObject/readExterna方法。这里先以内置的jackson为例分析,jackson在对json字符串反序列化操作时,会调用类的无参构造方法进行类实例化,所以只有存在无参构造方法的类才满足jackson序列化类的要求,另外在反序列化过程中会根据具体情况调用getter/setter方法。所以在扫描jackson反序列化gadget时把这三个方法均添加到source里面进行扫描

最终把扫描的结果存储到source.dat文件中,格式如下

1
2
3
4
类名 方法名 方法描述 污染参数索引
java/awt/TextField <init> ()V 0
javax/swing/JTextField setActionCommand (Ljava/lang/String;)V 0
javax/swing/JEditorPane setPage (Ljava/lang/String;)V 0

2.5 GadgetChainDiscovery 信息整合为GadgetChain

GadgetChainDiscovery#discover 方法对上述搜集到的所有信息进行整合,然后将每个source作为利用链的起点寻找调用的子方法、能将参数污染传递的子方法,如果末节点调用到先前定义好的sink方法,说明利用链可以走通,那么就标记成功,添加到discoveredGadgets中。否则再次循环methodsToExplore获取方法进行分析直到方法循环完毕。主要看下对于重写方法、搜索策略的处理

1、InheritanceDeriver#getAllMethodImplementations搜集重写方法

将搜集到的重写方法的结果保存到methodimpl.dat,格式如下:

1
2
3
类 方法 方法描述
子类1 重写方法 方法描述
子类2 重写方法 方法描述

2、将调用的方法集合规整格式得到graphCallMap,key为调用方法、value为被调用方法Set(添加时自动抛弃重复方法)

如上面提到的com.sun.jmx.mbeanserver.JmxMBeanServer#createMBean方法调用了createMBean、cloneObjectName两个方法,这一步就是将两个子方法整合,最终得到[JmxMBeanServer#createMBean:[createMBean,cloneObjectName]]这种结构

1676531278617

3、加载2.4章中得到的sources.dat,将每个source做为GadgetChain链的第一个节点。程序循环将利用链GadgetChain的末节点做为起点方法查找可调用的子方法、可传递污染的参数索引,如果可以传递污染,那么就将被调用方法加到利用链。最后判断如果末节点调用到我们先前定义好的sink方法,说明利用链可以走通,那么就标记成功,添加到discoveredGadgets中。否则再次循环methodsToExplore进行查找直到方法循环完毕

如下图的圈1判断当”可传递污染给子方法的调用方法参数索引”与”调用方法可以控制的参数索引”不一致时,污染就无法传递,pass。圈2用于处理”静态分析无法确认程序运行时使用哪个实现方法”的问题,所以将所有实现方法都跑一遍。圈3判断如果新节点在之前访问过,则跳过检查。这里虽然可以避免环的问题,但是会造成漏报,需要修改下逻辑。圈4判断末节点的方法、污染的参数是否与定义好的sink一致,如果一致则添加到利用链discoveredGadgets

经过MethodDiscovery获取类/方法/继承关系信息、PassthroughDiscovery获取单个方法内的污染传递、CallGraphDiscovery获取方法间的污染传递、SourceDiscovery获取利用链的source源,最终通过GadgetChainDiscovery 分析前面得到的信息并整合为GadgetChain结果存储到gadget-chains.txt文件中

3、实际测试

了解了如上项目源码、工具运行流程之后,我们实际使用GadgetInspector扫描测试一个Demo jar包感受下

1
2
3
4
5
6
7
8
9
10
11
Demo代码
GadgetInspector-Test-Demo\out\artifacts\GadgetInspector_Test_Demo

编译gadget-inspector
GadGetResearch\gadgetinspector-master>gradlew.bat shadowJar --warning-mode all

扫描命令
java -Xmx10g -jar gadget-inspector-all.jar --config jserial E:\imgs-source\GadgetInspector-Test-Demo\out\artifacts\GadgetInspector_Test_Demo\GadgetInspector-Test-Demo.jar

项目文件生成位置
GadGetResearch\gadgetinspector-master\build\libs

1、枚举所有类及类的所有方法,输出类信息文件classes.dat

1
2
3
4
5
6
org/example/FnCompose	java/lang/Object	org/example/IFn,java/io/Serializable	false	f1!2!org/example/IFn!f2!2!org/example/IFn
org/example/FnConstant java/lang/Object org/example/IFn,java/io/Serializable false value!2!java/lang/Object
org/example/FnEval java/lang/Object org/example/IFn,java/io/Serializable false
org/example/IFn java/lang/Object true
org/example/model/AbstractTableModel java/lang/Object java/io/Serializable false __clojureFnMap!2!java/util/HashMap
org/example/TestDemo java/lang/Object false test!2!java/lang/String
类名 父类名 实现的所有接口 是否接口 成员变量(以!分割)
org/example/FnCompose java/lang/Object org/example/IFn,java/io/Serializable false f1!2!org/example/IFn!f2!2!org/example/IFn

成员变量f1!2!org/example/IFn!表示:字段名称为f1、modifiers为2(private修饰)、变量类型为org/example/IFn类

在gadgetinspector中的定义类为gadgetinspector.data.ClassReference、gadgetinspector.data.ClassReference.Member

输出方法信息文件methods.dat

1
2
3
4
5
6
7
8
9
10
11
12
org/example/FnCompose	<init>	(Lorg/example/IFn;Lorg/example/IFn;)V	false
org/example/FnCompose invokeCall (Ljava/lang/Object;)Ljava/lang/Object; false
org/example/FnConstant <init> (Ljava/lang/Object;)V false
org/example/FnConstant invokeCall (Ljava/lang/Object;)Ljava/lang/Object; false
org/example/FnEval <init> ()V false
org/example/FnEval invokeCall (Ljava/lang/Object;)Ljava/lang/Object; false
org/example/IFn invokeCall (Ljava/lang/Object;)Ljava/lang/Object; false
org/example/model/AbstractTableModel <init> (Ljava/util/HashMap;)V false
org/example/model/AbstractTableModel hashCode ()I false
org/example/TestDemo <init> ()V false
org/example/TestDemo pMethod (Ljava/lang/String;)Ljava/lang/String; false
org/example/TestDemo cMethod (Ljava/lang/String;)Ljava/lang/String; false
方法所在类 方法名 方法参数及返回值 是否静态方法
org/example/FnCompose invokeCall (Ljava/lang/Object;)Ljava/lang/Object; false

在gadgetinspector中的定义类为gadgetinspector.data.MethodReference

inheritanceMap.dat 类的继承关系

1
2
3
4
5
6
org/example/model/AbstractTableModel	java/lang/Object	java/io/Serializable
org/example/FnCompose java/lang/Object java/io/Serializable org/example/IFn
org/example/FnEval java/lang/Object java/io/Serializable org/example/IFn
org/example/TestDemo java/lang/Object
org/example/FnConstant java/lang/Object java/io/Serializable org/example/IFn
org/example/IFn java/lang/Object

2、生成passthrough数据流,输出信息文件passthrough.dat

1
2
3
org/example/TestDemo	cMethod	(Ljava/lang/String;)Ljava/lang/String;	1,
org/example/TestDemo pMethod (Ljava/lang/String;)Ljava/lang/String; 1,
org/example/FnConstant invokeCall (Ljava/lang/Object;)Ljava/lang/Object; 0, 0是实例
类名 方法名 方法描述 能污染的参数1索引,能污染的参数2索引
org/example/TestDemo cMethod (Ljava/lang/String;)Ljava/lang/String; 1,

3、生成passthrough调用图,输出信息文件callgraph.dat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
org/example/model/AbstractTableModel	hashCode	()I	org/example/IFn	invokeCall	(Ljava/lang/Object;)Ljava/lang/Object;	0	__clojureFnMap	0
org/example/model/AbstractTableModel hashCode ()I java/util/HashMap hashCode ()I 0 __clojureFnMap 0
org/example/TestDemo pMethod (Ljava/lang/String;)Ljava/lang/String; org/example/TestDemo cMethod (Ljava/lang/String;)Ljava/lang/String; 1 1
org/example/TestDemo pMethod (Ljava/lang/String;)Ljava/lang/String; org/example/TestDemo cMethod (Ljava/lang/String;)Ljava/lang/String; 0 0
org/example/TestDemo cMethod (Ljava/lang/String;)Ljava/lang/String; java/lang/String toUpperCase ()Ljava/lang/String; 1 0
org/example/FnCompose <init> (Lorg/example/IFn;Lorg/example/IFn;)V java/lang/Object <init> ()V 0 0
org/example/model/AbstractTableModel <init> (Ljava/util/HashMap;)V java/lang/Object <init> ()V 0 0
org/example/FnEval invokeCall (Ljava/lang/Object;)Ljava/lang/Object; java/lang/Runtime exec (Ljava/lang/String;)Ljava/lang/Process; 1 1
org/example/FnCompose invokeCall (Ljava/lang/Object;)Ljava/lang/Object; org/example/IFn invokeCall (Ljava/lang/Object;)Ljava/lang/Object; 1 1
org/example/model/AbstractTableModel hashCode ()I java/util/HashMap get (Ljava/lang/Object;)Ljava/lang/Object; 0 __clojureFnMap 0
org/example/TestDemo <init> ()V java/lang/Object <init> ()V 0 0
org/example/FnCompose invokeCall (Ljava/lang/Object;)Ljava/lang/Object; org/example/IFn invokeCall (Ljava/lang/Object;)Ljava/lang/Object; 0 f2 0
org/example/FnCompose invokeCall (Ljava/lang/Object;)Ljava/lang/Object; org/example/IFn invokeCall (Ljava/lang/Object;)Ljava/lang/Object; 0 f1 0
org/example/FnEval <init> ()V java/lang/Object <init> ()V 0 0
org/example/model/AbstractTableModel hashCode ()I org/example/IFn invokeCall (Ljava/lang/Object;)Ljava/lang/Object; 0 1
org/example/FnConstant <init> (Ljava/lang/Object;)V java/lang/Object <init> ()V 0 0

4、搜索可用的source,输出信息文件sources.dat

1
org/example/model/AbstractTableModel	hashCode	()I	0

5、搜索生成调用链,输出结果文件gadget-chains.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

//最终生成的反序列化链
org/example/model/AbstractTableModel.hashCode()I (0)
org/example/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (1)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)

4、总结

在花了几周时间分析完该项目后,对于利用ASM技术、JVM指令、模拟栈调用等解决静态扫描的问题有了一些认知了解。当然在分析过程中也发现该工具的一些弊端或者说是bug,如Transient修饰字段处理逻辑、广度优先的搜索策略、source源/sink源不完善等问题。而搞清楚问题之后下一步就是解决问题、完善代码逻辑。另外GadgetInspector直接分析war/jar的方式比较贴合平时做漏洞挖掘的工作场景,如在一线攻防演练时拿到了java war包,由于项目周期短 可能急需一个点撕开防守的口子,这时候扫描工具可以帮助我们快速做一些基础分析的工作,基于我们提前指定的source源、sink漏洞点分析拿到项目的所有路由、可能存在的攻击链路,而具体漏洞EXP利用的构造交给我们自己。相信随着规则的完善,也会很大的辅助增强我们审计挖掘的效率。所以在完善gadget的扫描逻辑后也考虑基于此工具二开以简化平时一些常规漏洞的挖掘审计工作。写到这里 成文已经又臭又长,不再赘述了。下一步的todo-list就是着重代码逻辑完善、规则优化、常规漏洞挖掘等相关问题。安全路漫漫~


静态代码扫描工具之GadgetInspector探究
https://pwnull.github.io/2023/Research-on-GadgetInspector-of-Static-Code-Scanning-Tool/
作者
pwnull
发布于
2023年2月28日
许可协议