[CVE-2019-16891] Liferay RCE via JSON Deserialization
Description
1.Liferay
Liferay Portal là giải pháp Cổng điện tử được thiết kế phù hợp với các mô hình ứng dụng trong các cơ quan, tổ chức và doanh nghiệp có nhu cầu phát triển hệ thống thông tin trên môi trường web nhằm thực hiện các giao dịch trực tuyến và sử dụng Intranet/Internet như một công cụ thiết yếu trong các hoạt động, cung cấp thông tin, giao tiếp, quản lý và điều hành, trao đổi và cộng tác.
2.CVE-2019-16891
Liferay Portal từ phiên bản 7.1.0 trở về trước bị dính lỗi RCE thông qua lỗ hổng liên quan tới json deserialization. Đây là lỗ hổng có thể khai thác mà không cần có tài khoản.
Setup Debug
1.Server
Tải Liferay 6.2.5 : https://sourceforge.net/projects/lportal/files/Liferay%20Portal/6.2.5%20GA6/liferay-portal-tomcat-6.2-ce-ga6-20160112152609836.zip/download
Giải nén file trên rồi config trong thư mục
liferay-portal-tomcat-6.2-ce-ga6-20160112152609836\liferay-portal-6.2-ce-ga6\tomcat-7.0.62\bin
như sau:
set JPDA_ADDRESS=8000
set JPDA_TRANSPORT=dt_socket
set JRE_HOME=%ProgramFiles%\Java\jre1.8.0_202 (*thay đổi đường dẫn tương ứng)
catalina.bat jpda start
2.Client Ở đây mình dùng Intellij để remote debug.
Mở project -> Edit configuration -> Add new config -> Remote JVM Debug -> config port 8000
Analysis
Entrypoints
JSONFactoryImpl
là class sẽ handle hầu hết các tác vụ liên quan tới json (convert,deserialize,...).
Có rất nhiều method ở đây nên mình sẽ rải breakpoint đồng thời fuzz các entrypoint cho tới khi request trigger được breakpoint.Sau một hồi thì thử đến phần login thì đã trigger được breakpoint.
Quan sát stacktrace thì tìm được Chain từ HttpServlet qua tới đoạn call deserialize JSON.
HttpServlet.service() -> PollerServlet.service() -> PollerServlet.getContent() -> PollerRequestHandlerUltil.getPollerHeader() -> PollerRequestHandlerImpl.getPollerHeader() -> PollerRequestHandlerImpl.parsePollerRequestParameters() -> JSONFactoryImpl.deserialize()
Tóm tắt
Endpoint login : /web/guest/home
Endpoint triggers bug : /poller/receive
Method thực hiện deserialize : JSONFactoryImpl.deserialize(String)
Chain này cần phải được Authenticate
JSON Deserialize
Method thực hiện deserialize ở đây là JSONFactoryImpl.deserialize(String) từ đây call tới org.jabsorb.JSONSerializer.fromJSON() rồi lại call tới JSONSerializer.unmarshall() để tiếp tục deserialize object
Có thể thấy method này thực hiện lấy một loại serializer(class) thích hợp cho đoạn json string và thực hiện deserialize(unmarshall) nó theo cái serializer đó.
Set breakpoint tại method getSerializer để tìm các class thực hiện việc unmarshall
Đây là list những class có thể sẽ unmarshall object JSON truyền vào, được xét từ trên xuống dưới:Locale,Liferay,Primitive,... Trong list này thì thấy org.jabsorb.serializer.impl.BeanSerializer
là đáng nghi nhất. Method BeanSerializer.unmarshall() có invoke Setter Method của cái object được khởi tạo từ JSON String ra:
public Object unmarshall(SerializerState state, Class clazz, Object o) throws UnmarshallException {
...
instance = clazz.newInstance();
...
while(i.hasNext()) {
String field = (String)i.next();
Method setMethod = (Method)bd.writableProps.get(field);
if (setMethod != null) {
Object fieldVal;
try {
Class[] param = setMethod.getParameterTypes();
fieldVal = this.ser.unmarshall(state, param[0], jso.get(field));
} catch (UnmarshallException var13) {
throw new UnmarshallException("could not unmarshall field \"" + field + "\" of bean " + clazz.getName(), var13);
} catch (JSONException var14) {
throw new UnmarshallException("could not unmarshall field \"" + field + "\" of bean " + clazz.getName(), var14);
}
if (log.isDebugEnabled()) {
log.debug("invoking " + setMethod.getName() + "(" + fieldVal + ")");
}
invokeArgs[0] = fieldVal;
try {
setMethod.invoke(instance, invokeArgs);
} catch (Throwable var19) {
Throwable e = var19;
if (var19 instanceof InvocationTargetException) {
e = ((InvocationTargetException)var19).getTargetException();
}
throw new UnmarshallException("bean " + clazz.getName() + "can't invoke " + setMethod.getName() + ": " + e.getMessage(), e);
}
}
...
Với field là param key, dòng này sẽ lấy setter method của field từ class bd (class thỏa mãn method canSerialize() của BeanSerializer). Nếu setMethod tồn tại thì sẽ lấy param types và value của field sau đó invoke invokeArgs[0] = fieldVal;
=> Để trigger được unmarshall() bằng BeanSerializer thì class để trigger setter method không được Serializable, nếu class này Serializable thì nó sẽ dừng ngay ở LiferaySerializer khi gọi method getSerialize(), method này không invoke setter method.
Tóm tắt
Source class có thể trigger được BeanSerializer này là 1 class có dạng:
Có public constructor không có arg
Không implement Serializable
Có setter method có thể lợi dụng được!
Exploit
Class thỏa mãn các điều kiện trên là com.mchange.v2.c3p0.mbean.C3P0PooledDataSource
Class này có emty constructor, không implement Serializeable,có setter method có thể lợi dụng để deserialize object qua rmi BeanSerializer.unmarshall -> C3P0PooledDataSource.setJndiName.invoke(instance, jndiName) -> C3P0PooledDataSource.rebind(jndiName, obj) => rmi
POC
Authenticated endpoint:
https://github.com/DuyVuong/CVE-Analysis/assets/125139802/3ff2521f-563c-4308-a7b5-7f3a274fb162
Unauthenticated endpoint (https://dappsec.substack.com/p/an-advisory-for-cve-2019-16891-from) :
POST /c/portal/portlet_url HTTP/1.1
Host: liferay.victim.example.com
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
parameterMap={"Json payload for deserialization in here!"}
https://github.com/DuyVuong/CVE-Analysis/assets/125139802/21490733-4e5c-455a-893f-1f7efb715b20