CVE-2020–4280 — IBM QRadar Java Deserialization Anlysis (and bypass)

CVE-2020–4280 — IBM QRadar Java Deserialization Anlysis (and bypass)


11 min read

[Tiếng Việt phía dưới]

It’s been a while since last blog post, Partly because of work, another is I have lost motivation to write :(, so there is no more writing since the project until now. Last week, there was an IBM Qradar SIEM Java Deser bug released — CVE-2020–4280 (details at: ). Right into the product I am using for several months now, so let take a look at it!

#SETUP Previously, I mistakenly thought that Qradar only had Enterprise version, until I read the detailed blog about this bug that it actually has a Community Edition.

The CE version can be downloaded at: If it required login, you can use the shared credentials at: (I also use it with

There is only version 7.3.3 for Community Edition, as far as I experience and known, Enterprise Edition is currently using version 7.4.1. After logging in, IBM will download an ova file to import into VMWare / Virtual Box.

The install is also quite simple, only takes a little time, you can refer to the setup guide at: virtualbox /

  • During the setup process, sometimes there will be some minor errors, if you stuck, you can inbox to exchange!

Similar to what I did with weblogic or liferay, to debug Qradar, I find all the libraries in WEB-INF and import into IntellIJ. There is a deadly note that Qradar is running on IBM Java, completely different from the usual Oracle Java. In order to debug, you must choose the correct Project SDK, which is IBM Java 1.8 (can be downloaded here: (If you do not select the correct SDK, when debugging will jump around without understanding why this happen …=))) )

#Root Cause

Based on the author’s blog post, we can identify the vulnerability located in the RemoteJavaScript servlet with the servlet-name as remoteMethod.

In qradar, the following url-patterns are handled by remoteMethod:

  • /remoteJavaScript

  • /remoteMethod


  • /JSON-RPC/*

This remoteMethod feature works like this:

  • From the method body or the query param, this entrypoint takes in the parameters “method”, “params”, “QRadarCSRF”, ….

  • From the parameter “method”, appName and methodName are extracted as follows: appName + “.” + methodName = method, for example: Qradar.getSavedSearch

  • If the methodName of appName exists the method will be called!

Rough speaking, a sample request looks like this:

The method is recognized here:

At line 309/311, the method is called with the passed parameters:

At, the parameters continue to be passed to ReflectionUtils.stringsToObjects() for further processing,

The chain behind ReflectionUtils.stringsToObjects() is long, but in short this is where the data is deserialized! To sum up, the Chain from source to sink as follows (T_T hope you can recognize my writing):

However, not all params reach SerializationUtils.deserialize(). For parameters with simple types such as String, integer, long, …, there will be separate processing parts, not going to the deserialize() branch. Only parameters with complex types, eg: Map<HostInfo>, are passed deserialize() for further processing. These exported-methods are declared in /opt/qradar/conf/appconfig/<app>-exported_methods.xml together with classes handling that method, method name, params name.

For example, with the method that the author used to attack in the PoC of CVE-2020–4280 is:

<exportedMethod exposeToJMX="false" readOnly="true" parameterNames="**changedSettings**" className="com.q1labs.assetprofilerconfiguration.ui.util.AssetProfilerConfig" methodCode="974337442" methodName="**validateChangesAssetConfiguration**" appName="**qradar**"/>

In the processing class, we can see that the method Qradar.validateChangesAssetConfiguration() has changedSettings parameter. This parameter is of type List<AssetProfilerChanges>:

In the author’s original PoC, there’s a somewhat cumbersome handle: Use the Jython1 gadget to enable the console.enableExecuteCommand -> property then call Qradar.executeCommand().

I have another way to deal more succinctly that is to use a custom version of ROME gadget to execute and response to the body, the PoC result is as follows:

#The Patch

After testing the PoC, I also started trying to patch it too. The patch can be downloaded here:

Applying the patch is also quite simple, IBM already has a guide, just follow it and finish, although it takes a little while. It is true that after the patch, the exploit is not successful anymore,

¯_(ツ)_/¯ of course. Getting all the new library and doing some diff, there is a few big changes in the library, eg: ReflectionUtils.stringToObject() has been changed, adding a few lines to check the deserialized data, Old ReflectionUtils.stringToObject() :


New ReflectionUtils.stringToObject():


The change can be seen quite clearly, there has been a class ValidatingObjectInputStream to filter the classes that are allowed to deserialize, with the following whitelist:

  • org.postgresql.util.*

  • com.q1labs.*

  • java.lang.*

  • java.util.*

  • gnu.trove.*

However, this branch only handles parameters of type other than Map:

In the last else branch of the processing code, the data continues to be decode base64 and deserialized as usual:

That means, if you find a method with param type = Map, then you can bypass this patch and deserialize to RCE! (1) Leave it here, and continue looking at the IBM patch, another big change can be seen in /opt/qradar/appconfig/qradar-exported_methods.xml. Some exported methods have been removed in new config file:

The exported methods that have been deleted are:

  • qradar.validateChangesAssetConfiguration() -> in original PoC

  • qradar.getSecurityState()

  • qradar.getVulnerabilityState()

  • qradar.getNetworkSecuritySummary()

  • qradar.getRssFeedItem()

  • qradar.getDataPoints()

  • qradar.executeCommand()

After checking again, most of these methods have params that can be used to deserialize, That explains why when using the old PoC to exploit, the result will be a blank page with response = 200, which means the method no longer exists:

Set the breakpoint on line 195 of RemoteJavaScript to see more clearly, the method now returns null because it is no longer declared in the config file:

That is what IBM did to fix this vulnerability, however there are still 439 exported methods in the config file:

Combined with the necessary condition (1), just find a new method, and easily bypass the patch of CVE-2020–4280 and get RCE!

Here is the PoC of the bypass:



That’s all

Thanks for reading!


[Vietnamese version]

Cũng đã khá lâu kể từ lần cuối viết lách,

Phần là vì công việc khá nhiều, phần nữa là dạo này bị mất cảm hứng để viết :(, nên là ko còn viết gì nữa từ lúc xong đồ án tới giờ.

Tuần vừa rồi có 1 bug Java Deser của IBM Qradar SIEM được release — CVE-2020–4280 (chi tiết tại: Lại đúng vào product mấy tháng nay mình hay dùng nên tiện tay mổ xẻ xem sao!


Trước đây, mình đã tưởng lầm rằng Qradar chỉ có Enterprise version, tới khi đọc blog chi tiết về bug này thì mới biết thực ra nó còn có 1 Community Edition nữa.

Bản CE các bạn có thể download tại:

Nếu như yêu cầu login, các bạn có thể sử dụng các shared credential tại: (mình cũng dùng với

Chỉ có phiên bản 7.3.3 cho Community Edition, theo như mình trải nghiệm và được biết thì Enterprise Edition hiện đang sử dụng version 7.4.1.

Sau khi login vào thì IBM sẽ cho down 1 file ova để về import vào VMWare/Virtual Box.

Việc install cũng khá đơn giản, chỉ hơi tốn thời gian chút, có thể tham khảo setup guide tại:

  • Trong quá trình setup đôi khi cũng sẽ xảy ra 1 số lỗi vặt, nếu các bạn stuck thì có thể inbox mình để trao đổi!

Tương tự như đã làm với weblogic or liferay, để debug Qradar thì mình tìm tất cả các library có trong WEB-INF và import vào IntellIJ.

Có 1 lưu ý chết người đó là Qradar đang chạy trên IBM Java, hoàn toàn khác với Oracle Java thông thường vẫn sử dụng.

Để việc debug được suôn sẻ, trong setting của project đang debug phải chọn đúng Project SDK là IBM Java 1.8 (có thể download tại đây:

(Nếu không chọn đúng SDK thì khi debug sẽ bị nhảy lung tung mà không hiểu tại sao =))) )

#Root Cause

Dựa vào blog post của tác giả, ta có thể xác định lỗ hổng nằm tại servlet RemoteJavaScript với servlet-name là remoteMethod

Trong qradar, các url-pattern sau được handle bởi remoteMethod:

  • /remoteJavaScript

  • /remoteMethod


  • /JSON-RPC/*

Tính năng remoteMethod này hoạt động như sau:

  • Từ method body hoặc query param, entrypoint này nhận vào các tham số “method”, “params”, “QRadarCSRF”, ….

  • Từ tham số “method”, appName và methodName được tách ra như sau: appName + “.” + methodName = method, ví dụ: Qradar.getSavedSearch

  • Nếu methodName của appName tồn tại method sẽ được gọi!

Nói thì dài, 1 request mẫu có dạng như sau:

Method được xác định tại đây:

Tại dòng 309/311, method được call với các param đã truyền vào

Tại, các param tiếp tục được truyền vào ReflectionUtils.stringsToObjects() để xử lý,

Chain phía sau ReflectionUtils.stringsToObjects() còn dài, nhưng có thể nói ngắn gọn đây chính là nơi dữ liệu được deserialize!

Chốt lại là Chain từ source tới sink như sau (T_T chữ xấu, các bạn thông cảm):

Tuy nhiên, không phải param nào cũng tới được SerializationUtils.deserialize().

Đối với các param có type đơn giản như: String, integer, long, … thì sẽ có các phần xử lý riêng, không đi vào nhánh deserialize().

Chỉ với các param có type phức tạp, ví dụ: Map<HostInfo>, mới được truyền vào deserialize() để xử lý tiếp.

Các exported-method này được khai báo trong /opt/qradar/conf/appconfig/<app>-exported_methods.xml kèm với các class xử lý method đó, method name, params name. Ví dụ như với method mà tác giả đã sử dụng để attack trong PoC của CVE-2020-xxxx là:

<exportedMethod exposeToJMX="false" readOnly="true" parameterNames="**changedSettings**" className="com.q1labs.assetprofilerconfiguration.ui.util.AssetProfilerConfig" methodCode="974337442" methodName="**validateChangesAssetConfiguration**" appName="**qradar**"/>

Mò vào class xử lý, có thể thấy method Qradar.validateChangesAssetConfiguration() có param là changedSettings. Param này có kiểu List<AssetProfilerChanges>:

Trong PoC gốc của tác giả thì có xử lý hơi cồng kềnh:

Dùng gadget Jython1 để enable property console.enableExecuteCommand -> sau đó gọi Qradar.executeCommand().

Mình có cách khác xử lý ngắn gọn hơn đó là sử dụng custom lại gadget ROME để execute và response luôn ra body, kết quả PoC được như sau:

Và đương nhiên là PoC cũng sẽ ko đc cung cấp theo bài này ¯_(ツ)_/¯, có làm mới có ăn …

#The Patch

Sau khi thử nghiệm xong PoC thì mình cũng bắt đầu thử vá luôn xem sao.

Bản vá có thể down tại đây:

Apply patch thì cũng khá đơn giản, IBM đã có sẵn guide tại đây, chỉ việc làm theo là xong, tuy hơi có lâu chút.

Đúng là sau khi patch, exploit không còn hoạt động nữa, ¯_(ツ)_/¯ đương nhiên rồi.

Kéo lib về để diff patch thì thấy có 1 số thay đổi lớn như sau, ReflectionUtils.stringToObject() đã có thay đổi, thêm 1 vài dòng để kiểm tra dữ liệu được deserialize,

ReflectionUtils.stringToObject() cũ:


ReflectionUtils.stringToObject() mới:


Có thể thấy thay đổi khá rõ ràng, đã có thêm class ValidatingObjectInputStream để filter các class được phép deserialize, với whitelist là:

  • org.postgresql.util.*

  • com.q1labs.*

  • java.lang.*

  • java.util.*

  • gnu.trove.*

Tuy nhiên nhánh này chỉ xử lý các param có kiểu không phải là Map:

Trong nhánh else cuối cùng của đoạn code xử lý, dữ liệu vẫn tiếp tục được decode base64 và deserialize như bình thường:

Điều đó có nghĩa là, nếu như tìm được 1 method có param type = Map, thì tiếp tục có thể bypass bản vá này và deserialize to RCE! (1)

Tạm thời dừng ở đó, tiếp tục soi bản vá của IBM, có thể thấy sự thay đổi lớn trong /opt/qradar/appconfig/qradar-exported_methods.xml.

Một vài exported methods đã bị xóa bỏ trong file config mới:

Các exported methods đã bị xóa là:

  • qradar.validateChangesAssetConfiguration() -> trong PoC gốc của tác giả

  • qradar.getSecurityState()

  • qradar.getVulnerabilityState()

  • qradar.getNetworkSecuritySummary()

  • qradar.getRssFeedItem()

  • qradar.getDataPoints()

  • qradar.executeCommand()

Sau khi check lại 1 lượt thì đa số các method này đều có param có thể bị lợi dụng để deserialize,

Điều đó lý giải tại sao khi sử dụng PoC cũ để exploit, kết quả trả về sẽ là 1 trang trắng với response = 200, nghĩa là method không còn tồn tại nữa:

Đặt breakpoint tại dòng 195 của RemoteJavaScript để thấy rõ hơn, method lúc này đã trả về null do không còn được khai báo trong file config nữa:

Đó là những gì IBM đã làm để vá lại lỗ hổng này, tuy nhiên vẫn còn tới 439 exported methods trong file config

Kết hợp với điều kiện cần (1) thì chỉ cần tìm ra 1 method nào đó mới,

  • Được khai báo trong qradar-exported_methods.xml

  • Không yêu cầu cấp quyền cao

  • Có method param là dạng Map<>

=> Bypass CVE-2020–4280

Mình đã tìm ra 1 vài method thỏa mãn điều kiện và bypass thành công bản vá:

Hôm trước mình có report cho vendor nhưng có vẻ như họ không quan tâm lắm, do vậy mình quyết định public bài này với dạng nửa vời.

Phần PoC bypass CVE-2020–4280 này dành lại cho các bạn researcher quan tâm có thể tự tìm ra, mình tin là với những dữ liệu đã cung cấp thì việc tìm ra nó chỉ trong vài giờ đồng hồ setup lab thôi.

Cảm ơn các bạn đã đón đọc!