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

发布于 2024-08-31 15:10:52 字数 16310 浏览 12 评论 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技术交流群

发布评论

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

关于作者

双马尾

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

一梦浮鱼

文章 0 评论 0

mb_Z9jVigFL

文章 0 评论 0

伴随着你

文章 0 评论 0

耳钉梦

文章 0 评论 0

18618447101

文章 0 评论 0

蜗牛

文章 0 评论 0

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