作
者:
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的直接序列化与反序列化:
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 +
'}';
}
}
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;
}
}
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 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”)这种方式。
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;
public JSONSerializerImpl() {
if (JavaDetector.isIBM()) {
SystemUtil.disableUnsafeUsage();
}
this._jsonSerializer = new JsonSerializer();
}
public JSONSerializerImpl exclude(String... fields) {
this._jsonSerializer.exclude(fields);
return this;
}
public JSONSerializerImpl include(String... fields) {
this._jsonSerializer.include(fields);
return this;
}
public String serialize(Object target) {
return this._jsonSerializer.serialize(target);
}
public String serializeDeep(Object target) {
JsonSerializer jsonSerializer = this._jsonSerializer.deep(true);
return jsonSerializer.serialize(target);
}
public JSONSerializerImpl transform(JSONTransformer jsonTransformer, Class> type) {
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 {
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;
public JSONDeserializerImpl() {
if (JavaDetector.isIBM()) {
SystemUtil.disableUnsafeUsage();
}
this._jsonDeserializer = new PortalJsonParser();
}
public T deserialize(String input) {
return this._jsonDeserializer.parse(input);
}
public T deserialize(String input, Class targetType) {
return this._jsonDeserializer.parse(input, targetType);
}
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);
return this;
}
}
能看出也是设置了JODD JsonParser在反序列化时的一些功能。
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
的调用图(羡慕这样的工具):