前几天渗透测试过程中遇到了个Openfire弱口令,通过后台管理系统的插件功能部署了个shell,获取了服务器权限。由于之前在后台可以看到数据库连接信息是使用MSSQL,且用户是sa,所以在后续信息搜集时候打算把sa密码搜集起来,但在查看openfire.xml
的时候发现数据库账号密码加密了,如下所示:
数据库连接字符串加密这种方式也只是稍微提高了下门槛而已,实际上应用程序在连接数据库的时候是需要提供明文账号密码的,这也就意味着应用在连接数据库之前必定要先解密。于是直接把服务器上的Openfire打包到本地分析下,找到解密相关函数然后自己写个方法调用即可。
为了快速定位对于的解密功能,我决定从URL路由开始,因为在后台的server-db.jsp
页面可以查看数据库连接信息,该页面展示的信息中数据库账号是经过解密的,所以该页面的后端代码一定有调用对象的解密模块。Openfire的后台管理功能基本都是以plugins
的方式,都位于/plugins/
下,我们自己安装的也在该目录下。对应找到admin目录即是后台管理功能插件,在其webapp/WEB-INF/web.xml
文件中可以找到/server-db.jsp
路由的servlet-mapping
为org.jivesoftware.openfire.admin.server_002ddb_jsp
类,如下图所示:
跟进对应代码:
con = DbConnectionManager.getConnection();
DatabaseMetaData metaData = con.getMetaData();
out.write("\n\n<p>\n");
if (_jspx_meth_fmt_005fmessage_005f1(_jspx_page_context))
return;
out.write("\n</p>\n\n<div class=\"jive-table\">\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n<thead>\n <tr>\n <th colspan=\"2\">");
if (_jspx_meth_fmt_005fmessage_005f2(_jspx_page_context))
return;
out.write("</th>\n </tr>\n</thead>\n<tbody>\n <tr>\n <td class=\"c1\">\n ");
if (_jspx_meth_fmt_005fmessage_005f3(_jspx_page_context))
return;
out.write("\n </td>\n <td class=\"c2\">\n ");
out.print(metaData.getDatabaseProductName());
out.write("\n ");
out.print(metaData.getDatabaseProductVersion());
out.write("\n </td>\n </tr>\n <tr>\n <td class=\"c1\">\n ");
if (_jspx_meth_fmt_005fmessage_005f4(_jspx_page_context))
return;
out.write("\n </td>\n <td class=\"c2\">\n ");
out.print(metaData.getDriverName());
out.write("\n </td>\n </tr>\n <tr>\n <td class=\"c1\">\n ");
if (_jspx_meth_fmt_005fmessage_005f5(_jspx_page_context))
return;
可见页面展示数据来源于DbConnectionManager
类,该类位于org.jivesoftware.database
下,该包则位于openfire的lib目录。继续反编译lib目录下的openfire.jar
文件找到对应类的getConnection
方法,代码如下:
private static void ensureConnectionProvider() {
if (connectionProvider != null)
return;
synchronized (providerLock) {
if (connectionProvider != null)
return;
String className = JiveGlobals.getXMLProperty("connectionProvider.className");
if (className != null) {
try {
Class<ConnectionProvider> conClass = ClassUtils.forName(className);
setConnectionProvider(conClass.newInstance());
} catch (Exception e) {
Log.warn("Failed to create the connection provider specified by connectionProvider.className. Using the default pool.", e);
setConnectionProvider(new DefaultConnectionProvider());
}
} else {
setConnectionProvider(new DefaultConnectionProvider());
}
}
}
public static Connection getConnection() throws SQLException {
ensureConnectionProvider();
Integer currentRetryCount = Integer.valueOf(0);
Integer maxRetries = Integer.valueOf(JiveGlobals.getXMLProperty("database.maxRetries", 10));
Integer retryWait = Integer.valueOf(JiveGlobals.getXMLProperty("database.retryDelay", 250));
SQLException lastException = null;
do {
try {
Connection con = connectionProvider.getConnection();
if (con != null) {
if (!profilingEnabled)
return con;
return new ProfiledConnection(con);
}
} catch (SQLException e) {
lastException = e;
Log.info("Unable to get a connection from the database pool (attempt " + currentRetryCount + " out of " + maxRetries + ").", e);
}
try {
Thread.sleep(retryWait.intValue());
} catch (Exception e) {}
Integer integer1 = currentRetryCount, integer2 = currentRetryCount = Integer.valueOf(currentRetryCount.intValue() + 1);
} while (currentRetryCount.intValue() <= maxRetries.intValue());
throw new SQLException("ConnectionManager.getConnection() failed to obtain a connection after " + currentRetryCount + " retries. " + "The exception from the last attempt is as follows: " + lastException);
}
在getConnection
中首先调用了ensureConnectionProvider
方法,在ensureConnectionProvider
方法中从openfire.xml
文档中加载connectionProvider.className
节点值来实例化数据库连接类。这里值为org.jivesoftware.database.DefaultConnectionProvider
,继续跟进该类。
private void loadProperties() {
this.driver = JiveGlobals.getXMLProperty("database.defaultProvider.driver");
this.serverURL = JiveGlobals.getXMLProperty("database.defaultProvider.serverURL");
this.username = JiveGlobals.getXMLProperty("database.defaultProvider.username");
this.password = JiveGlobals.getXMLProperty("database.defaultProvider.password");
String minCons = JiveGlobals.getXMLProperty("database.defaultProvider.minConnections");
String maxCons = JiveGlobals.getXMLProperty("database.defaultProvider.maxConnections");
String conTimeout = JiveGlobals.getXMLProperty("database.defaultProvider.connectionTimeout");
this.testSQL = JiveGlobals.getXMLProperty("database.defaultProvider.testSQL", DbConnectionManager.getTestSQL(this.driver));
this.testBeforeUse = Boolean.valueOf(JiveGlobals.getXMLProperty("database.defaultProvider.testBeforeUse", false));
this.testAfterUse = Boolean.valueOf(JiveGlobals.getXMLProperty("database.defaultProvider.testAfterUse", false));
Log.info("dirver:{}", this.driver);
Log.info("serverUrl:{}", this.serverURL);
Log.info("username:{}", this.username);
Log.info("password:{}", this.password);
if (this.serverURL != null && this.serverURL.contains("amp;"))
this.serverURL = this.serverURL.replace("amp;", "");
System.out.println("driver=" + this.driver + ",serverUrl=" + this.serverURL + ",username=" + this.username + ",password=" + this.password);
this.mysqlUseUnicode = Boolean.valueOf(JiveGlobals.getXMLProperty("database.mysql.useUnicode")).booleanValue();
try {
if (minCons != null)
this.minConnections = Integer.parseInt(minCons);
if (maxCons != null)
this.maxConnections = Integer.parseInt(maxCons);
if (conTimeout != null)
this.connectionTimeout = Double.parseDouble(conTimeout);
} catch (Exception e) {
Log.error("Error: could not parse default pool properties. Make sure the values exist and are correct.", e);
}
}
在该类的loadProperties
方法中可见从xml文件读取账号密码信息,继续跟进JiveGlobals.getXMLProperty
,最终在XMLProperties
类的getProperty
方法中找到对应的解密操作,代码如下:
public synchronized String getProperty(String name, boolean ignoreEmpty) {
String value = this.propertyCache.get(name);
if (value != null)
return value;
String[] propName = parsePropertyName(name);
Element element = this.document.getRootElement();
for (String aPropName : propName) {
element = element.element(aPropName);
if (element == null)
return null;
}
value = element.getTextTrim();
if (ignoreEmpty && "".equals(value))
return null;
if (JiveGlobals.isPropertyEncrypted(name)) {
Attribute encrypted = element.attribute("encrypted");
if (encrypted != null) {
value = JiveGlobals.getPropertyEncryptor().decrypt(value);
} else {
Log.info("Rewriting XML property " + name + " as an encrypted value");
setProperty(name, value);
}
}
this.propertyCache.put(name, value);
return value;
}
该方法在加载xml文件信息的时候会检查节点是否有encrypted
属性,如果有则调用JiveGlobals.getPropertyEncryptor().decrypt
,继续跟进getPropertyEncryptor
方法:
public static Encryptor getPropertyEncryptor() {
if (securityProperties == null)
loadSecurityProperties();
if (propertyEncryptor == null) {
String algorithm = securityProperties.getProperty("encrypt.algorithm");
if ("AES".equalsIgnoreCase(algorithm)) {
propertyEncryptor = new AesEncryptor(currentKey);
} else {
propertyEncryptor = new Blowfish(currentKey);
}
}
return propertyEncryptor;
}
该方法从security.xml
文件中读取encrypt.algorithm
节点值确定加密方式及加密使用的key
,从目标的security.xml
中看到并未使用key
所以currentKey
的值默认为null
。
到此基本就搞清楚他自身的加密解密功能了,要解密就很简单了,可以直接加载openfire.jar
到classpath,写个方法调用对应的AesEncryptor
或是Blowfish
并传入key
即可,为了方便我把全部功能集中写了个简单的发布在GitHub上,地址:https://github.com/ca3tie1/OpenFireEncryptor