ADAudit Plus 利用分析——数据加密分析

发布于 2024-08-31 15:10:52 字数 16310 浏览 28 评论 0

0x00 前言

在上篇文章《ADAudit Plus 漏洞调试环境搭建》介绍了漏洞调试环境的搭建细节,经测试发现数据库的部分数据做了加密,本文将要介绍数据加密的相关算法。

0x01 简介

本文将要介绍以下内容:

  • 数据加密的位置
  • 算法分析
  • 算法实现

0x02 数据加密的位置

测试环境同《ADAudit Plus 漏洞调试环境搭建》保持一致

数据库连接的完整命令: "C:\Program Files\ManageEngine\ADAudit Plus\pgsql\bin\psql" "host=127.0.0.1 port=33307 dbname=adap user=postgres password=Stonebraker"

查询加密口令的命令示例: SELECT * FROM public.aaapassword ORDER BY password_id ASC;

返回结果示例:

 password_id |                           password                           | algorithm |                       salt                                         | passwdprofile_id | passwdrule_id |  createdtime  | factor----------+--------------------------------------------------------------+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------+---------------+---------------+-----           1 | $2a$12$e.lzmqKXyh8m8KTd9WBkxeqr90qeTrDKwBhlSsdjMf7yIlj1ygbqm | bcrypt    | \xc30c040901028b43e9beabaec7d8d24e01623a1d3e917176f836602c04ee285d87c01c64174cf0bb7c11ab314b2cc8a8507cf04b297eb410d83ba29e6b3adc208fde8b7adfbc96be5dbf53485069cb4625a7ddf9d423b4c151c46367ba58 |                3 |             1 | 1680771305487 |     12
         301 | $2a$12$abyhGRg2fsQ27NoHsR2xae8vuOFVbONpayxfctUAFpSvbM68kL1q2 | bcrypt    | \xc30c04090102c69b4884afb59f93d24e01a926a128b8a91eb20272877d093518b7ef759db0e85117467c93244d3a832a517d34e1b120164bd6717e2d5aa07ec5d95c2f5ef1c6eaed91126b649d4ee06dba3f7233f61254d1b67f7e56903c |                3 |             1 | 1681264745532 |     12
(2 rows)

经测试,对应 Web 管理页面的位置为 Admin -> Technicians ,如下图

Alt text

点击 Add technicians 可以添加用户,这里可以选择添加自定义用户或者域用户

添加自定义用户需要输入口令,如下图

Alt text

添加域用户不需要输入域用户的口令,如下图

Alt text

0x03 算法分析

1.加密算法细节

经分析,加密算法细节位于 C:\Program Files\ManageEngine\ADAudit Plus\lib\AdvAuthentication.jar 中的 com.adventnet.authentication.util -> AuthUtil.class

添加用户的实现代码:

   public static DataObject createUserAccount(DataObject accountDO) throws DataAccessException, PasswordException {
        int workload = false;
        LOGGER.log(Level.FINEST, "createUserAccount invoked with dataobject : {0}", accountDO);
        Iterator accItr = accountDO.getRows("AaaAccount");
        int numOfAcc = getCount(accItr);
        Credential credential = getUserCredential();
        if (credential != null) {
            validateForAccountCreation(credential.getAccountId(), numOfAcc);
        }

        List requiredTables = Arrays.asList("AaaUser", "AaaLogin", "AaaAccount", "AaaPassword", "AaaAccPassword");
        List tablesFromDO = accountDO.getTableNames();
        if (!tablesFromDO.containsAll(requiredTables)) {
            throw new DataAccessException("In sufficient data for creating an account, required tables in dataobject " + requiredTables);
        } else {
            long now = System.currentTimeMillis();
            Row userRow = accountDO.getFirstRow("AaaUser");
            userRow.set("CREATEDTIME", new Long(now));
            Row userStatusRow = new Row("AaaUserStatus");
            userStatusRow.set("USER_ID", userRow.get("USER_ID"));
            userStatusRow.set("STATUS", "ACTIVE");
            userStatusRow.set("UPDATEDTIME", new Long(now));
            accountDO.addRow(userStatusRow);
            Row loginRow = accountDO.getFirstRow("AaaLogin");

            try {
                String domain = (String)loginRow.get("DOMAINNAME");
                loginRow.set("DOMAINNAME", domain != null && domain.trim().length() != 0 ? domain : (String)MetaDataUtil.getTableDefinitionByName("AaaLogin").getColumnDefinitionByName("DOMAINNAME").getDefaultValue());
            } catch (MetaDataException var28) {
                throw new DataAccessException("Exception occured while obtaining default value of [AAALOGIN.DOMAINNAME]" + var28);
            }

            Criteria c = new Criteria(Column.getColumn("AaaLogin", "NAME"), loginRow.get("NAME"), 0, false);
            c = c.and(new Criteria(Column.getColumn("AaaLogin", "DOMAINNAME"), loginRow.get("DOMAINNAME"), 0));
            DataObject dobj = DataAccess.get("AaaLogin", c);
            if (!dobj.isEmpty()) {
                LOGGER.log(Level.SEVERE, "Could not create new User :: {0}, as the user with given LoginName and DomainName already exists", new Object[]{loginRow.get("NAME")});
                throw new DataAccessException("Could not create new User ::" + loginRow.get("NAME") + ", as the given LoginName and DomainName :: " + loginRow.get("DOMAINNAME") + "already exists");
            } else {
                if (loginRow.get("USER_ID") == null) {
                    loginRow.set("USER_ID", userRow.get("USER_ID"));
                    accountDO.updateRow(loginRow);
                }

                accItr = accountDO.getRows("AaaAccount");
                Row accountRow = null;
                List serviceIds = new ArrayList();

                Row passwordRow;
                while(accItr.hasNext()) {
                    accountRow = (Row)accItr.next();
                    serviceIds.add(accountRow.get("SERVICE_ID"));
                    if (accountRow.get("LOGIN_ID") == null) {
                        accountRow.set("LOGIN_ID", loginRow.get("LOGIN_ID"));
                    }

                    accountRow.set("CREATEDTIME", new Long(now));
                    accountDO.updateRow(accountRow);
                    Row accOwnerProfileRow = null;

                    try {
                        accOwnerProfileRow = accountDO.getFirstRow("AaaAccOwnerProfile", accountRow);
                    } catch (DataAccessException var27) {
                    }

                    if (accOwnerProfileRow == null) {
                        accOwnerProfileRow = new Row("AaaAccOwnerProfile");
                        accOwnerProfileRow.set("ACCOUNT_ID", accountRow.get("ACCOUNT_ID"));
                        accOwnerProfileRow.set("ALLOWED_SUBACCOUNT", new Integer(0));
                        accountDO.addRow(accOwnerProfileRow);
                    }

                    if (!accountDO.containsTable("AaaAccountOwner") && credential != null) {
                        passwordRow = new Row("AaaAccountOwner");
                        passwordRow.set("ACCOUNT_ID", accountRow.get("ACCOUNT_ID"));
                        passwordRow.set("OWNERACCOUNT_ID", credential.getAccountId());
                        accountDO.addRow(passwordRow);
                    }

                    String accAdminProfile = (String)AuthDBUtil.getObject("AaaAccAdminProfile", "NAME", "ACCOUNTPROFILE_ID", accountRow.get("ACCOUNTPROFILE_ID"));
                    Row accStatusRow = constructAccStatusRow(accountRow, accAdminProfile);
                    accountDO.addRow(accStatusRow);
                }

                Iterator passItr = accountDO.getRows("AaaPassword");
                passwordRow = null;

                while(passItr.hasNext()) {
                    passwordRow = (Row)passItr.next();
                    Long passRuleId = (Long)passwordRow.get("PASSWDRULE_ID");
                    Long passProfileId = (Long)passwordRow.get("PASSWDPROFILE_ID");
                    if (passRuleId == null) {
                        String[] serviceNames = getServiceNames(serviceIds);
                        passRuleId = getCompatiblePassRuleId(serviceNames);
                        passwordRow.set("PASSWDRULE_ID", passRuleId);
                    }

                    Row passRuleRow = AuthDBUtil.getRowMatching("AaaPasswordRule", "PASSWDRULE_ID", passRuleId);
                    validateForPasswordRule((String)loginRow.get("NAME"), (String)passwordRow.get("PASSWORD"), passRuleRow);
                    String passwordProfile = (String)AuthDBUtil.getObject("AaaPasswordProfile", "NAME", "PASSWDPROFILE_ID", passProfileId);
                    Row passProfileRow = AuthDBUtil.getRowMatching("AaaPasswordProfile", "NAME", passwordProfile);
                    Object workFactor = passProfileRow.get("FACTOR");
                    if (!((String)passwordRow.get("ALGORITHM")).equalsIgnoreCase("bcrypt")) {
                        LOGGER.log(Level.INFO, "algorithm used to hash password should be bcrypt, hence updating algorithm as bcrypt");
                        passwordRow.set("ALGORITHM", "bcrypt");
                    }

                    int workload;
                    if (workFactor != null && Integer.parseInt(workFactor.toString()) > 0) {
                        workload = Integer.parseInt(workFactor.toString());
                    } else {
                        workload = PAM.workload;
                    }

                    String salt = BCrypt.gensalt(workload);
                    String encPass = getEncryptedPassword((String)passwordRow.get("PASSWORD"), salt, (String)passwordRow.get("ALGORITHM"));
                    passwordRow.set("PASSWORD", encPass);
                    passwordRow.set("FACTOR", workload);
                    passwordRow.set("ALGORITHM", passwordRow.get("ALGORITHM"));
                    passwordRow.set("CREATEDTIME", new Long(now));
                    passwordRow.set("SALT", salt);
                    accountDO.updateRow(passwordRow);
                    Row passStatusRow = constructPassStatusRow(passwordRow, passwordProfile);
                    accountDO.addRow(passStatusRow);
                }

                LOGGER.log(Level.FINEST, "account validated dataobject is : {0}", accountDO);
                DataObject addedDO = AuthDBUtil.getPersistence("Persistence").add(accountDO);
                LOGGER.log(Level.FINEST, "account added successfully");
                return addedDO;
            }
        }
    }

得到加密生成 Password 的代码:

String encPass = getEncryptedPassword((String)passwordRow.get("PASSWORD"), salt, (String)passwordRow.get("ALGORITHM"));

生成 salt 的代码:

String salt = BCrypt.gensalt(workload);

经动态调试,发现 workload 默认为 12 ,生成的 salt 格式示例: $2a$12$DVT1iwOoi3YwkHO6L6QSoe ,如下图

Alt text

具体加密算法 getEncryptedPassword() 的实现细节:

   public static String getEncryptedPassword(String password, String salt, String algorithm) {
        if (algorithm.equals("bcrypt")) {
            String hashedPassword = null;
            if (salt.matches("\\$2a\\$.*\\$.*")) {
                hashedPassword = BCrypt.hashpw(password, salt);
                return hashedPassword;
            } else {
                throw new IllegalArgumentException("Invalid Salt value.. To use Bcrypt hashing, salt should be generated through 'Bcrypt.genSalt(workload)' or in the form '$2a$(workload)$(22char)' ");
            }
        } else {
            byte[] password_ba = convertToByteArray(password);
            byte[] salt_ba = convertToByteArray(salt);

            try {
                MessageDigest md = MessageDigest.getInstance(algorithm);
                md.update(password_ba);
                md.update(salt_ba);
                byte[] cipher = md.digest();
                return convertToString(cipher);
            } catch (NoSuchAlgorithmException var7) {
                LOGGER.log(Level.SEVERE, "Exception occured when getting MessageDigest Instance for Algorithm {0}. Returning unencrypted Password", algorithm);
                return password;
            }
        }
    }

在此处下断点,经动态调试得出以下结论:

  • 如果是域用户,会使用默认口令 admin 作为明文,随机生成 salt ,算法使用 bcrypt ,通过固定算法计算得出密文,密文前 29 字节对应加密使用的 salt
  • 如果不是域用户,会使用用户口令作为明文去计算,例如默认用户 admin ,会使用实际的口令作为明文去加密得到密文

也就是说,在查询表 public.aaapassword 时,我们只需要取出 password 项前 29 字节作为加密使用的 salt ,不需要关注表 public.aaapassword 中的 salt 项

2.区分是否为域用户

查询命令示例: SELECT * FROM public.aaalogin ORDER BY login_id ASC;

返回结果示例:

 login_id | user_id |     name      |         domainname-------+---------+---------------+-------------------------        1 |       1 | admin         | ADAuditPlus Authentication
      301 |     301 | Administrator | TEST
      310 |     310 | test1         | TEST
      311 |     311 | testadmin     | ADAuditPlus Authentication
(4 rows)

其中, domainnameADAuditPlus Authentication 代表自定义添加的用户

这里使用 inner join 查询自动筛选出非域用户和对应的 hash,命令示例: SELECT aaalogin.login_id,aaalogin.name,aaalogin.domainname,aaapassword.password FROM public.aaalogin as aaalogin INNER JOIN public.aaapassword AS aaapassword on aaalogin.login_id=aaapassword.password_id WHERE aaalogin.domainname = 'ADAuditPlus Authentication';

返回结果示例:

 login_id|   name    |         domainname         |                           password------+-----------+----------------------------+-----------------------------------------------------------       1 | admin     | ADAuditPlus Authentication | $2a$12$1hKeH4aM2LY4BvYpKT9Z5.p9cD453FjBAPYjp0ek94n936WRRAYme
     311 | testadmin | ADAuditPlus Authentication | $2a$12$1hKeH4aM2LY4BvYpKT9Z5.6zPb3SEFvW6PbGrU11Ilc96/YqJAEua
(2 rows)

0x04 算法实现

测试参数如下:

  • 已知明文为 123456
  • 查询数据库得到的 password 项为 $2a$12$1hKeH4aM2LY4BvYpKT9Z5.p9cD453FjBAPYjp0ek94n936WRRAYme

从中可知 salt 为 password 项的前 29 字节,即 $2a$12$1hKeH4aM2LY4BvYpKT9Z5.

计算密文的测试代码如下:

import org.mindrot.jbcrypt.BCrypt;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static com.adventnet.authentication.util.AuthUtil.convertToByteArray;
import static com.adventnet.authentication.util.AuthUtil.convertToString;
public class test1 {

    public static String getEncryptedPassword(String password, String salt, String algorithm) {
        if (algorithm.equals("bcrypt")) {
            String hashedPassword = null;
            if (salt.matches("\\$2a\\$.*\\$.*")) {
                hashedPassword = BCrypt.hashpw(password, salt);
                return hashedPassword;
            } else {
                throw new IllegalArgumentException("Invalid Salt value.. To use Bcrypt hashing, salt should be generated through 'Bcrypt.genSalt(workload)' or in the form '$2a$(workload)$(22char)' ");
            }
        } else {
            byte[] password_ba = convertToByteArray(password);
            byte[] salt_ba = convertToByteArray(salt);

            try {
                MessageDigest md = MessageDigest.getInstance(algorithm);
                md.update(password_ba);
                md.update(salt_ba);
                byte[] cipher = md.digest();
                return convertToString(cipher);
            } catch (NoSuchAlgorithmException var7) {
                System.out.println("Exception occured when getting MessageDigest Instance for Algorithm "+ algorithm + ". Returning unencrypted Password");
                return password;
            }
        }
    }
    public static void main(String[] args) throws Exception, Exception {
        String password = "123456";
        String salt = "$2a$12$uVCggMRqSCSEwi6wh06Kd.";
        String encPass = getEncryptedPassword(password, salt, "bcrypt");
        System.out.println(encPass);   
    }
}

计算结果为 $2a$12$1hKeH4aM2LY4BvYpKT9Z5.p9cD453FjBAPYjp0ek94n936WRRAYme ,同数据库得到的 password 项一致

综上,根据以上算法可以用来对用户口令进行暴破

0x05 小结

本文分析了 ADAudit Plus 数据加密的算法,区分域用户,编写实现代码,后续根据算法可以用来对用户口令进行暴破。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

双马尾

暂无简介

文章
评论
26 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文