概述
Code White发现了多个严重的JSON严重反序列化漏洞,这些漏洞影响Liferay Portal 6.1、6.2、7.0、7.1和7.2版。它们允许通过JSON Web服务API进行未经身份验证的远程代码执行。固定的Liferay Portal版本是6.2 GA6、7.0 GA7、7.1 GA4和7.2 GA2。
相应的漏洞是:
- CST-7111:通过JSON反序列化进行RCE(LPS-88051 / LPE-16598 1)该JSONDeserializerFlexjson的允许任意类的实例化和任意setter方法调用。
- CST-7205:通过JSONWS执行未经身份验证的远程代码(LPS-97029 / CVE-2020-7961)该JSONWebServiceActionParametersMapLiferay门户的允许任意类和任意setter方法调用的实例。
两者都允许通过其无参数构造函数实例化任意类,并允许调用类似于JavaBeans约定的setter方法。这允许通过各种公共已知的小工具执行未经身份验证的远程代码执行。
本文出发点为根据官方披露的信息尝试复现漏洞的过程,文章有不准的地方轻拍.
基础信息部分为啰嗦点,多图预警,没兴趣的可以直接看“梳理关键点”部分。
环境准备
本文漏洞环境为:Liferay Portal 7
源码地址:
liferay-ce-portal-src-7.2.0-ga1-20190531153709761.zip
应用地址:
liferay-ce-portal-tomcat-7.2.0-ga1-20190531153709761.tar.gz
其他:
Idea、marshalsec、Python -m SimpleHTTPServer
基础信息
官网披露的该漏洞信息:
地址
英语不好,翻译文章看了以后有几个重点:
- 漏洞点存在位置为
/api/jsonws/invoke
- Liferay Portal 7的json库为
Jodd Json
- 如果传入的key为
parameterName:fully.qualified.ClassName
,那么将会载入后面的包。 - api方法中存在
java.lang.Object
参数的服务方法(重点,吃亏在此处..)
根据上面几个官方文章中披露的问题点,逐一进行分析
JoddJson
既然Liferay Portal 7使用的是joddjson的库,本次又是json反序列化的问题,那么就先了解下joddjson库。
从官方的介绍中,我们可以看到使用方式很简单
new JsonParser().parse(json, Book.class);
即可反序列化一段json数据,下面的代码执行后将调用mac下的calc。
单步调试
从文章中可以提取到,本次漏洞的url为/api/jsonws/invoke
,本地搭建起来环境并且初始配置以后访问http://192.168.77.18:8080/api/jsonws
,即可看到api列表
载入源码后定位到位置com.liferay.portal.jsonwebservice.JSONWebServiceServiceAction#getJSONWebServiceAction:163
先在入口打个debug断点,方便跟进,顺便在官方文章中提到的源码位置以及其方法进入的位置也打个debug断点。com.liferay.portal.jsonwebservice.JSONWebServiceActionParametersMap#put:63
src/com/liferay/portal/jsonwebservice/JSONWebServiceActionParametersMap.java#put:101
然后随便选一个API,先走一遍流程看看。
我使用的api是AssetCategory下的第一个search测试跟进的。
点击调用以后不出所料,在我们的断点停了下来。
一步步跟进下,接下来会调用jsonWebServiceAction.invoke()方法
跟进invoke,可以看到第一个json反序列化。
跟进JSONFactoryUtil.looseDeserialize看一下有点什么方法
其中looseDeserialize
有两个方法,一个的接参为String,另一个接参为String、CLass。
然后循环commond
的list,并且调用_executeStatement
方法,设置cmd中所对应的webservice
路径
继续跟下去,会在com.liferay.portal.jsonwebservice.JSONWebServiceActionParameters#collectAll
中调用_collectFromRequestParameters
方法,来对表单提交的参数进行遍历
然后将值放入_jsonWebServiceActionParameters
中.
跟进_jsonWebServiceActionParameters
的put
方法,可以看到我们就进入到了官方说的存在问题的方法。
如果传入的key为+
开头,则会进入else if (key.startsWith(StringPool.PLUS)) {
判断逻辑,
在此逻辑中,会将该值放入_parameterTypes
hashmap中,并将value改为void
跟进后发现并不会跳到官方说的问题点,因为pos不满足条件,不等于-1.
看一下决定pos是什么值的方法
key.indexOf(CharPool.COLON);
CharPool.COLON的值为":",那么也就应对官方文章中说的
请求参数名称包含:
,则其后面的部分将指定参数的类型
然而我们的请求中的参数并不包含:
,所以进入不到官方说的点也很正常。在所有参数都遍历且放入_jsonWebServiceActionParameters
后,会回到com.liferay.portal.jsonwebservice.JSONWebServiceActionsManagerImpl#getJSONWebServiceAction
方法。
jsonWebServiceActionConfig
会查找cmd
中指定的webservice
路径,然后将该类载入进来。
可以看到当前访问的url所对应的类以及方法还有入参值,
跟进JSONWebServiceActionImpl
,
其中会将jsonWebServiceActionParameters
的值赋值给_jsonWebServiceActionParameters
,此时跳回到了com.liferay.portal.jsonwebservice.action.JSONWebServiceInvokerAction#_executeStatement:379
.
跟进jsonWebServiceAction.invoke()
方法看看。
继续跟进
可以看到此处会调用_prepareParameters
,并且将actionClass
类传入进去。
跟进此处后
_jsonWebServiceActionConfig.getMethodParameters
将该类的方法入参获取出来
然后依次循环,根据指定类载入的参数名与请求包中的参数名进行赋值,然后根据载入类中参数的类型。
在此处有个判断,如果_jsonWebServiceActionParameters.getParameterTypeName(parameterName);
能获取到值,那么将使用classload载入类。其中就是从_parameterTypes
中获取值
继续往下跟发现抛出了异常
不要急,这是因为我们传入的obc的值为6,而6根本不是一个类..
我们将请求包中的6改为一个存在的类,然后在载入类的地方打个断点继续调试。
在过了classLoader.loadClass(parameterTypeName);
的问题后又有一个问题的产生,那么就是会对输入的类以及其webservice所对应参数的类进行对比,这边对比不成功,导致抛出异常。
那么假如此处过了以后,看看下面的代码会干什么。
_createDefaultParameterValue
会对类名进行判断,然后返回个newInstance
。
然而com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#_convertValueToParameterValue
会对其进行指定的类型转换,且最终会调用
JSONFactoryUtil.looseDeserialize(valueString, parameterType);
来完成序列化触发漏洞。
至此,一个大概的逻辑应该已经梳理完成(ps:这套代码有点太大了..其中有疏漏的地方轻拍砖.)
梳理关键点
根据上面走过的流程我们知道了问题点,以及核心利用的条件要求.
触发漏洞的核心点是:
- 执行命令的类不在cmd中,而是在参数中
- 如果参数key为aa:bb=ccc,那么也会将bb=ccc放入
_jsonWebServiceActionParameters
中 - web service的入参要为
java.lang.Object
- JSONFactoryUtil.looseDeserialize(valueString, parameterType);
- 循环调用
_jsonWebServiceActionParameters
首先我们找一个接收参数有java.lang.Object
的web services,直接打开网页/api/jsonws
,然后右键源代码,查找java.lang.Object
(我当时卡在怎么找这个.看到放出来的poc后心态蹦了,这么简单的方法居然没想到..)
由图可见,轻轻松松找到了有object参数的方法。
可以看到defaultData
的参数类型为object,根据上面梳理的几个点,我们将defaultData改为defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
在发送的包中为:
接着上面打的断点继续跟进,
此处看到parameterTypeName
为我们传入的com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
值。并且methodParameters[i].getType()
为java.lang.Object
,所有此处判断已经通过。
跟进下去后进入了_convertValueToParameterValue
方法,在_convertValueToParameterValue
中前面一大堆if判断匹配不上所以没有进去,最后进入了else判断。
在_convertType(value, parameterType);
中,由于抛出异常,被catch捕获到,进入到了catch逻辑。
在catch中,由于value不map
从而进入到else逻辑,else逻辑中调用JSONFactoryUtil.looseDeserialize(valueString, parameterType);
,从而导致反序列化成功。
至此,整个漏洞利用分析完成。
总结
总结一下此漏洞,在不熟悉这个框架的情况下,复现这个漏洞,以及过流水似的大概搞清楚逻辑还是坑点比较多的,因为整套源码体量挺大的,载入idea经常性卡死,最终载入以后很多问题。
其实说了这么长篇大论,因为对此框架不是很熟悉的原因,看的也是比较迷,在大概分析完以后,因为在第一次分析的时候已经知道了两个关键点:
- 执行漏洞点不在cmd
- 参数需要object
在解决那个url的入参可以为object的时候,看到/api/jsonws 一大堆api的时候无从下手,最终看到github上有人放出来的poc源码,在看到从网页中直接查找object的时候,瞬间泪崩+顿悟。
然后接着之前的自己的分析把object传入后,漏洞复现成功。
最后也理解了官方的文章中对于此漏洞的概述。
"以后的版本中JSONWebServiceActionImpl._prepareParameters(Class<?>),ReflectUtil.isTypeOf(Class, Class)用来检查指定的类型是否扩展了要调用的方法的相应参数的类型。由于存在带有java.lang.Object参数的服务方法,因此可以指定任何类型。"
好好学习,天天向上.