专栏名称: 安全客
打破黑箱 客说安全
51好读  ›  专栏  ›  安全客

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

安全客  · 公众号  ·  · 2020-04-02 18:00

正文

者: Longofo@ 道创宇404实验室

之前在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反序列化的情况。

JODD序列化与反序列化

参考 官方使用手册 ,先看下JODD的直接序列化与反序列化:
TestObject.java
package com.longofo;
import java.util.HashMap;
public class TestObject { private String name; private Object object; private HashMap 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 getHashMap() { System.out.println("TestObject getHashMap call"); return hashMap; }
public void setHashMap(HashMap 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(); 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(); 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 targetType) { return this._jsonDeserializer.parse(input, targetType);//调用JODD的JsonParser进行反序列化,可以指定根类型(rootType) }
public JSONDeserializer transform(JSONDeserializerTransformer jsonDeserializerTransformer, String field) {//反序列化时使用的转换器 ValueConverter valueConverter = new JoddJsonDeserializerTransformer(jsonDeserializerTransformer); this._jsonDeserializer.use(field, valueConverter); return this; }
public JSONDeserializer 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 的调用图(羡慕这样的工具):






请到「今天看啥」查看全文