Castiel's Blog

Openfire加密解密数据库连接信息

Openfire加密解密数据库连接信息
2020-09-01 · 8 min read

0x00 写在前面

前几天渗透测试过程中遇到了个Openfire弱口令,通过后台管理系统的插件功能部署了个shell,获取了服务器权限。由于之前在后台可以看到数据库连接信息是使用MSSQL,且用户是sa,所以在后续信息搜集时候打算把sa密码搜集起来,但在查看openfire.xml的时候发现数据库账号密码加密了,如下所示:

数据库使用sa

0x01 分析

数据库连接字符串加密这种方式也只是稍微提高了下门槛而已,实际上应用程序在连接数据库的时候是需要提供明文账号密码的,这也就意味着应用在连接数据库之前必定要先解密。于是直接把服务器上的Openfire打包到本地分析下,找到解密相关函数然后自己写个方法调用即可。
为了快速定位对于的解密功能,我决定从URL路由开始,因为在后台的server-db.jsp页面可以查看数据库连接信息,该页面展示的信息中数据库账号是经过解密的,所以该页面的后端代码一定有调用对象的解密模块。Openfire的后台管理功能基本都是以plugins的方式,都位于/plugins/下,我们自己安装的也在该目录下。对应找到admin目录即是后台管理功能插件,在其webapp/WEB-INF/web.xml文件中可以找到/server-db.jsp路由的servlet-mappingorg.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

0x02 解密

到此基本就搞清楚他自身的加密解密功能了,要解密就很简单了,可以直接加载openfire.jar到classpath,写个方法调用对应的AesEncryptor或是Blowfish并传入key即可,为了方便我把全部功能集中写了个简单的发布在GitHub上,地址:https://github.com/ca3tie1/OpenFireEncryptor